From e0f343db32aa95b525455584d3b176c1d6f3a06d Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 24 Jun 2025 14:13:36 +0400 Subject: [PATCH 001/413] feat: split stable and crypto LP oracles --- .../lp-oracles/LPOracleCrypto.vy | 67 +++++++++++++++++++ .../LPOracleStable.vy} | 38 +++-------- 2 files changed, 77 insertions(+), 28 deletions(-) create mode 100644 contracts/price_oracles/lp-oracles/LPOracleCrypto.vy rename contracts/price_oracles/{LPOracle.vy => lp-oracles/LPOracleStable.vy} (69%) diff --git a/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy b/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy new file mode 100644 index 00000000..30fa4ca4 --- /dev/null +++ b/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy @@ -0,0 +1,67 @@ +# @version 0.4.1 +#pragma optimize gas +#pragma evm-version shanghai + +""" +@title LPOracleCrypto +@author Curve.Fi +@license GNU Affero General Public License v3.0 only +@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. +""" + + +interface CryptoPool: + def lp_price() -> uint256: view # Exists only for cryptopools + +interface PriceOracle: + def price() -> uint256: view + def price_w() -> uint256: nonpayable + + +POOL: public(immutable(CryptoPool)) +COIN0_ORACLE: public(immutable(PriceOracle)) + + +@deploy +def __init__(pool: CryptoPool, coin0_oracle: PriceOracle): + assert staticcall pool.lp_price() > 0 + if coin0_oracle.address != empty(address): + assert staticcall coin0_oracle.price() > 0 + assert extcall coin0_oracle.price_w() > 0 + POOL = pool + COIN0_ORACLE = coin0_oracle + + +@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: + return staticcall POOL.lp_price() + + +@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/LPOracle.vy b/contracts/price_oracles/lp-oracles/LPOracleStable.vy similarity index 69% rename from contracts/price_oracles/LPOracle.vy rename to contracts/price_oracles/lp-oracles/LPOracleStable.vy index 9626652b..cae26565 100644 --- a/contracts/price_oracles/LPOracle.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleStable.vy @@ -3,22 +3,18 @@ #pragma evm-version shanghai """ -@title LPOracle +@title LPOracleStable @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. +@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 ethereum.ercs import IERC20Detailed - -interface Pool: +interface StablePool: 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 @@ -27,28 +23,20 @@ interface PriceOracle: MAX_COINS: constant(uint256) = 8 -POOL: public(immutable(Pool)) +POOL: public(immutable(StablePool)) 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 +def __init__(pool: StablePool, coin0_oracle: PriceOracle): 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( @@ -60,12 +48,6 @@ def __init__(pool: Pool, coin0_oracle: PriceOracle): 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( @@ -73,13 +55,16 @@ def __init__(pool: Pool, coin0_oracle: PriceOracle): 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: + assert staticcall pool.price_oracle() > 0, "No price_oracle method" no_argument = True + if coin0_oracle.address != empty(address): + assert staticcall coin0_oracle.price() > 0 + assert extcall coin0_oracle.price_w() > 0 + POOL = pool COIN0_ORACLE = coin0_oracle - IS_CRYPTO = is_crypto NO_ARGUMENT = no_argument - PRECISIONS = precisions @internal @@ -102,9 +87,6 @@ def _coin0_oracle_price_w() -> uint256: @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 From 18d9c690e34a943be672c706a0d90e799414e993 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 24 Jun 2025 14:14:07 +0400 Subject: [PATCH 002/413] feat: LPOracleFactory --- .../lp-oracles/LPOracleFactory.vy | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 contracts/price_oracles/lp-oracles/LPOracleFactory.vy diff --git a/contracts/price_oracles/lp-oracles/LPOracleFactory.vy b/contracts/price_oracles/lp-oracles/LPOracleFactory.vy new file mode 100644 index 00000000..8d2f7df9 --- /dev/null +++ b/contracts/price_oracles/lp-oracles/LPOracleFactory.vy @@ -0,0 +1,134 @@ +# @version 0.4.1 +#pragma optimize gas +#pragma evm-version shanghai + + +event DeployOracle: + oracle: indexed(address) + pool: indexed(address) + coin0_oracle: address + implementation: address + +event SetAdmin: + admin: address + +event SetImplementations: + stable_implementation: address + crypto_implementation: address + + +struct OracleInfo: + pool: address + coin0_oracle: address + implementation: address + + +MAX_ORACLES: constant(uint256) = 50000 +n_oracles: public(uint256) +oracles: public(address[MAX_ORACLES]) + +oracle_map: HashMap[address, HashMap[address, HashMap[address, address]]] # oracle_map[pool][coin0_oracle][implementation] -> oracle +oracle_info: HashMap[address, OracleInfo] # oracle_info[oracle] -> OracleInfo + +admin: public(address) +stable_implementation: public(address) +crypto_implementation: public(address) + + +@deploy +def __init__(admin: address): + """ + @notice Factory which creates StablePool and CryptoPool LP Oracles from blueprints + @param admin Admin of the factory (ideally DAO) + """ + self.admin = admin + + +@external +@nonreentrant +def deploy_oracle(pool: address, coin0_oracle: address) -> address: + """ + @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 + @return Deployed oracle address + """ + implementation: address = self.stable_implementation + if self._is_crypto(pool): + implementation = self.crypto_implementation + + assert implementation != empty(address), "Oracle implementation is not set" + assert self.oracle_map[pool][coin0_oracle][implementation] == empty(address), "Oracle already exists" + + oracle: address = create_from_blueprint(implementation, pool, coin0_oracle, code_offset=3) + + N: uint256 = self.n_oracles + self.oracles[N] = oracle + self.n_oracles = N + 1 + self.oracle_map[pool][coin0_oracle][implementation] = oracle + self.oracle_info[oracle] = OracleInfo({ + pool: pool, + coin0_oracle: coin0_oracle, + implementation: implementation + }) + log DeployOracle(oracle, pool, coin0_oracle, implementation) + + return oracle + + +@external +@view +def get_oracle(pool: address, coin0_oracle: address, implementation: address = empty(address)) -> address: + _implementation: address = implementation + if _implementation == empty(address): + _implementation = self.stable_implementation + if self._is_crypto(pool): + _implementation = self.crypto_implementation + + return self.oracle_map[pool][coin0_oracle][_implementation] + + +@external +@view +def get_oracle_info(oracle: address) -> OracleInfo: + return self.oracle_info[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 + + +@external +@nonreentrant +def set_implementations(stable_implementation: address, crypto_implementation: address): + """ + @notice Set new implementations (blueprints) for stable and crypto oracles. Doesn't change existing ones + @param stable_implementation Address of the StablePool LP Oracle blueprint + @param crypto_implementation Address of the CryptoPool LP Oracle blueprint + """ + assert msg.sender == self.admin, "Admin only" + assert stable_implementation != empty(address) + assert crypto_implementation != empty(address) + self.stable_implementation = stable_implementation + self.crypto_implementation = crypto_implementation + log SetImplementations(stable_implementation, crypto_implementation) + + +@external +@nonreentrant +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, "Admin only" + self.admin = admin + log SetAdmin(admin) From 18e71eaa0a186cdd4c7bda1975622845663c4d02 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 24 Jun 2025 14:15:42 +0400 Subject: [PATCH 003/413] test: use factory to deploy LP oracles --- tests_forked/price_oracles/conftest.py | 25 +++++++++ .../test_lp_oracle_compare_to_spot.py | 55 ++++++++++++------- 2 files changed, 61 insertions(+), 19 deletions(-) diff --git a/tests_forked/price_oracles/conftest.py b/tests_forked/price_oracles/conftest.py index ca4fbcce..0bc0f435 100644 --- a/tests_forked/price_oracles/conftest.py +++ b/tests_forked/price_oracles/conftest.py @@ -12,3 +12,28 @@ def boa_fork(): @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 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 lp_oracle_factory(admin, stable_impl, crypto_impl): + with boa.env.prank(admin): + factory = boa.load("contracts/price_oracles/lp-oracles/LPOracleFactory.vy", admin) + factory.set_implementations(stable_impl, crypto_impl) + return factory 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 index 9accc4f5..e7ad18ef 100644 --- a/tests_forked/price_oracles/test_lp_oracle_compare_to_spot.py +++ b/tests_forked/price_oracles/test_lp_oracle_compare_to_spot.py @@ -2,7 +2,7 @@ from .settings import EXPLORER_URL, EXPLORER_TOKEN -def test_tricrypto_usdc(stablecoin_aggregator): +def test_tricrypto_usdc(lp_oracle_factory, stablecoin_aggregator, admin): tricrypto_usdc_pool_address = "0x7F86Bf177Dd4F3494b841a37e810A34dD56c829B" crvusd_usdc_pool_address = "0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E" @@ -11,8 +11,11 @@ def test_tricrypto_usdc(stablecoin_aggregator): 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 + with boa.env.prank(admin): + tricrypto_usdc_crvusd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleCrypto.vy').at( + lp_oracle_factory.deploy_oracle(tricrypto_usdc_pool_address, usdc_crvusd_oracle.address)) # USDC/LP * crvUSD/USDC + tricrypto_usdc_usd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleCrypto.vy').at( + lp_oracle_factory.deploy_oracle(tricrypto_usdc_pool_address, usdc_usd_oracle.address)) # USDC/LP * crvUSD/USDC * USD/crvUSD # --- Compare current oracle and spot prices --- @@ -108,7 +111,7 @@ def test_tricrypto_usdc(stablecoin_aggregator): raise Exception("Success") -def test_tricrypto_usdt(stablecoin_aggregator): +def test_tricrypto_usdt(lp_oracle_factory, stablecoin_aggregator, admin): tricrypto_usdt_pool_address = "0xf5f5B97624542D72A9E06f04804Bf81baA15e2B4" crvusd_usdt_pool_address = "0x390f3595bCa2Df7d23783dFd126427CCeb997BF4" @@ -117,8 +120,11 @@ def test_tricrypto_usdt(stablecoin_aggregator): 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 + with boa.env.prank(admin): + tricrypto_usdt_crvusd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleCrypto.vy').at( + lp_oracle_factory.deploy_oracle(tricrypto_usdt_pool_address, usdt_crvusd_oracle.address)) # USDT/LP * crvUSD/USDT + tricrypto_usdt_usd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleCrypto.vy').at( + lp_oracle_factory.deploy_oracle(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() @@ -134,13 +140,15 @@ def test_tricrypto_usdt(stablecoin_aggregator): assert abs(lp_spot_price_usd - lp_oracle_price_usd) / lp_oracle_price_usd < 0.005 -def test_tricrv(stablecoin_aggregator): +def test_tricrv(lp_oracle_factory, stablecoin_aggregator, admin): 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 + with boa.env.prank(admin): + tricrv_crvusd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleCrypto.vy').at( + lp_oracle_factory.deploy_oracle(tricrv_pool_address, "0x0000000000000000000000000000000000000000")) # USDT/LP * crvUSD/USDT + tricrv_usd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleCrypto.vy').at( + lp_oracle_factory.deploy_oracle(tricrv_pool_address, stablecoin_aggregator.address)) # USDT/LP * crvUSD/USDT * USD/crvUSD # Oracle price lp_oracle_price_crvusd = tricrv_crvusd_lp_oracle.price() @@ -155,7 +163,7 @@ def test_tricrv(stablecoin_aggregator): assert abs(lp_spot_price_usd - lp_oracle_price_usd) / lp_oracle_price_usd < 0.005 -def test_strategic_reserve(stablecoin_aggregator): +def test_strategic_reserve(lp_oracle_factory, stablecoin_aggregator, admin): strategic_reserve_pool_address = "0x4f493B7dE8aAC7d55F71853688b1F7C8F0243C85" crvusd_usdc_pool_address = "0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E" @@ -164,8 +172,11 @@ def test_strategic_reserve(stablecoin_aggregator): 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 + with boa.env.prank(admin): + strategic_reserve_crvusd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleStable.vy').at( + lp_oracle_factory.deploy_oracle(strategic_reserve_pool_address, usdc_crvusd_oracle.address)) # USDC/LP * crvUSD/USDC + strategic_reserve_usd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleStable.vy').at( + lp_oracle_factory.deploy_oracle(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() @@ -181,7 +192,7 @@ def test_strategic_reserve(stablecoin_aggregator): assert abs(lp_spot_price_usd - lp_oracle_price_usd) / lp_oracle_price_usd < 0.005 -def test_weeth_weth(stablecoin_aggregator): +def test_weeth_weth(lp_oracle_factory, stablecoin_aggregator, admin): weeth_ng_pool_address = "0xDB74dfDD3BB46bE8Ce6C33dC9D82777BCFc3dEd5" tricrypto_usdt_pool_address = "0xf5f5B97624542D72A9E06f04804Bf81baA15e2B4" crvusd_usdt_pool_address = "0x390f3595bCa2Df7d23783dFd126427CCeb997BF4" @@ -194,8 +205,11 @@ def test_weeth_weth(stablecoin_aggregator): [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 + with boa.env.prank(admin): + tricrypto_usdt_crvusd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleStable.vy').at( + lp_oracle_factory.deploy_oracle(weeth_ng_pool_address, usdt_crvusd_oracle.address)) # ETH/LP * crvUSD/ETH + tricrypto_usdt_usd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleStable.vy').at( + lp_oracle_factory.deploy_oracle(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() @@ -212,7 +226,7 @@ def test_weeth_weth(stablecoin_aggregator): assert abs(lp_spot_price_usd - lp_oracle_price_usd) / lp_oracle_price_usd < 0.006 -def test_cvxcrv(stablecoin_aggregator): +def test_cvxcrv(lp_oracle_factory, stablecoin_aggregator, admin): cvxcrv_pool_address = "0x971add32Ea87f10bD192671630be3BE8A11b8623" tricrv_pool_address = "0x4eBdF703948ddCEA3B11f675B4D1Fba9d2414A14" @@ -221,8 +235,11 @@ def test_cvxcrv(stablecoin_aggregator): 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 + with boa.env.prank(admin): + cvxcrv_pool_crvusd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleStable.vy').at( + lp_oracle_factory.deploy_oracle(cvxcrv_pool_address, crv_crvusd_oracle)) # CRV/LP * crvUSD/CRV + cvxcrv_pool_usd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleStable.vy').at( + lp_oracle_factory.deploy_oracle(cvxcrv_pool_address, crv_usd_oracle)) # CRV/LP * crvUSD/CRV * USD/crvUSD # Oracle price lp_oracle_price_crvusd = cvxcrv_pool_crvusd_lp_oracle.price() From b723f10b9a473003f8511572f04a79f5921fe676 Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 24 Jun 2025 15:29:02 +0200 Subject: [PATCH 004/413] refactor: use lib for shared code --- .../lp-oracles/LPOracleCrypto.vy | 34 ++++--------------- .../lp-oracles/LPOracleStable.vy | 32 ++++------------- .../price_oracles/lp-oracles/oracle_lib.vy | 27 +++++++++++++++ 3 files changed, 40 insertions(+), 53 deletions(-) create mode 100644 contracts/price_oracles/lp-oracles/oracle_lib.vy diff --git a/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy b/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy index 30fa4ca4..3b89c4bd 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy @@ -10,44 +10,24 @@ Then it chains with another oracle (target_coin/coin0) to get the final price. """ +import oracle_lib +initializes: oracle_lib + interface CryptoPool: def lp_price() -> uint256: view # Exists only for cryptopools -interface PriceOracle: - def price() -> uint256: view - def price_w() -> uint256: nonpayable - POOL: public(immutable(CryptoPool)) -COIN0_ORACLE: public(immutable(PriceOracle)) - @deploy -def __init__(pool: CryptoPool, coin0_oracle: PriceOracle): +def __init__(pool: CryptoPool, coin0_oracle: oracle_lib.PriceOracle): assert staticcall pool.lp_price() > 0 if coin0_oracle.address != empty(address): assert staticcall coin0_oracle.price() > 0 assert extcall coin0_oracle.price_w() > 0 POOL = pool - COIN0_ORACLE = coin0_oracle - - -@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 + oracle_lib.__init__(coin0_oracle) @internal @@ -59,9 +39,9 @@ def _price_in_coin0() -> uint256: @external @view def price() -> uint256: - return self._price_in_coin0() * self._coin0_oracle_price() // 10 ** 18 + return self._price_in_coin0() * oracle_lib._coin0_oracle_price() // 10 ** 18 @external def price_w() -> uint256: - return self._price_in_coin0() * self._coin0_oracle_price_w() // 10 ** 18 + return self._price_in_coin0() * oracle_lib._coin0_oracle_price_w() // 10 ** 18 diff --git a/contracts/price_oracles/lp-oracles/LPOracleStable.vy b/contracts/price_oracles/lp-oracles/LPOracleStable.vy index cae26565..90684551 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleStable.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleStable.vy @@ -16,21 +16,18 @@ interface StablePool: def price_oracle(i: uint256 = 0) -> uint256: view # Universal method! def get_virtual_price() -> uint256: view -interface PriceOracle: - def price() -> uint256: view - def price_w() -> uint256: nonpayable - +import oracle_lib +initializes: oracle_lib MAX_COINS: constant(uint256) = 8 POOL: public(immutable(StablePool)) -COIN0_ORACLE: public(immutable(PriceOracle)) NO_ARGUMENT: public(immutable(bool)) N_COINS: public(immutable(uint256)) @deploy -def __init__(pool: StablePool, coin0_oracle: PriceOracle): +def __init__(pool: StablePool, coin0_oracle: oracle_lib.PriceOracle): no_argument: bool = False # Init variables for raw calls @@ -63,25 +60,8 @@ def __init__(pool: StablePool, coin0_oracle: PriceOracle): assert extcall coin0_oracle.price_w() > 0 POOL = pool - COIN0_ORACLE = coin0_oracle NO_ARGUMENT = no_argument - - -@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 + oracle_lib.__init__(coin0_oracle) @internal @@ -105,9 +85,9 @@ def _price_in_coin0() -> uint256: @external @view def price() -> uint256: - return self._price_in_coin0() * self._coin0_oracle_price() // 10 ** 18 + return self._price_in_coin0() * oracle_lib._coin0_oracle_price() // 10 ** 18 @external def price_w() -> uint256: - return self._price_in_coin0() * self._coin0_oracle_price_w() // 10 ** 18 + return self._price_in_coin0() * oracle_lib._coin0_oracle_price_w() // 10 ** 18 diff --git a/contracts/price_oracles/lp-oracles/oracle_lib.vy b/contracts/price_oracles/lp-oracles/oracle_lib.vy new file mode 100644 index 00000000..6dd0c140 --- /dev/null +++ b/contracts/price_oracles/lp-oracles/oracle_lib.vy @@ -0,0 +1,27 @@ +# pragma version 0.4.1 + +interface PriceOracle: + def price() -> uint256: view + def price_w() -> uint256: nonpayable + +@deploy +def __init__(coin0_oracle: PriceOracle): + COIN0_ORACLE = coin0_oracle + +COIN0_ORACLE: public(immutable(PriceOracle)) + +@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 \ No newline at end of file From d8631e32852896144977028e2838cce78bc51b18 Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 24 Jun 2025 15:32:58 +0200 Subject: [PATCH 005/413] fix: expose getter --- contracts/price_oracles/lp-oracles/LPOracleCrypto.vy | 1 + contracts/price_oracles/lp-oracles/LPOracleStable.vy | 1 + 2 files changed, 2 insertions(+) diff --git a/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy b/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy index 3b89c4bd..1debdf77 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy @@ -12,6 +12,7 @@ import oracle_lib initializes: oracle_lib +exports: oracle_lib.COIN0_ORACLE interface CryptoPool: diff --git a/contracts/price_oracles/lp-oracles/LPOracleStable.vy b/contracts/price_oracles/lp-oracles/LPOracleStable.vy index 90684551..fd2b18bf 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleStable.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleStable.vy @@ -18,6 +18,7 @@ interface StablePool: import oracle_lib initializes: oracle_lib +exports: oracle_lib.COIN0_ORACLE MAX_COINS: constant(uint256) = 8 From c95255077cdb84862b9af2cc5fc586bc7309e194 Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 25 Jun 2025 12:08:11 +0400 Subject: [PATCH 006/413] chore: rename oracle_lib -> lp_oracle_lib --- .../price_oracles/lp-oracles/LPOracleCrypto.vy | 14 +++++++------- .../price_oracles/lp-oracles/LPOracleStable.vy | 14 +++++++------- .../lp-oracles/{oracle_lib.vy => lp_oracle_lib.vy} | 0 3 files changed, 14 insertions(+), 14 deletions(-) rename contracts/price_oracles/lp-oracles/{oracle_lib.vy => lp_oracle_lib.vy} (100%) diff --git a/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy b/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy index 1debdf77..b210e484 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy @@ -10,9 +10,9 @@ Then it chains with another oracle (target_coin/coin0) to get the final price. """ -import oracle_lib -initializes: oracle_lib -exports: oracle_lib.COIN0_ORACLE +import lp_oracle_lib +initializes: lp_oracle_lib +exports: lp_oracle_lib.COIN0_ORACLE interface CryptoPool: @@ -22,13 +22,13 @@ interface CryptoPool: POOL: public(immutable(CryptoPool)) @deploy -def __init__(pool: CryptoPool, coin0_oracle: oracle_lib.PriceOracle): +def __init__(pool: CryptoPool, coin0_oracle: lp_oracle_lib.PriceOracle): assert staticcall pool.lp_price() > 0 if coin0_oracle.address != empty(address): assert staticcall coin0_oracle.price() > 0 assert extcall coin0_oracle.price_w() > 0 POOL = pool - oracle_lib.__init__(coin0_oracle) + lp_oracle_lib.__init__(coin0_oracle) @internal @@ -40,9 +40,9 @@ def _price_in_coin0() -> uint256: @external @view def price() -> uint256: - return self._price_in_coin0() * oracle_lib._coin0_oracle_price() // 10 ** 18 + return self._price_in_coin0() * lp_oracle_lib._coin0_oracle_price() // 10 ** 18 @external def price_w() -> uint256: - return self._price_in_coin0() * oracle_lib._coin0_oracle_price_w() // 10 ** 18 + return self._price_in_coin0() * lp_oracle_lib._coin0_oracle_price_w() // 10 ** 18 diff --git a/contracts/price_oracles/lp-oracles/LPOracleStable.vy b/contracts/price_oracles/lp-oracles/LPOracleStable.vy index fd2b18bf..9d8ee5ee 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleStable.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleStable.vy @@ -16,9 +16,9 @@ interface StablePool: def price_oracle(i: uint256 = 0) -> uint256: view # Universal method! def get_virtual_price() -> uint256: view -import oracle_lib -initializes: oracle_lib -exports: oracle_lib.COIN0_ORACLE +import lp_oracle_lib +initializes: lp_oracle_lib +exports: lp_oracle_lib.COIN0_ORACLE MAX_COINS: constant(uint256) = 8 @@ -28,7 +28,7 @@ N_COINS: public(immutable(uint256)) @deploy -def __init__(pool: StablePool, coin0_oracle: oracle_lib.PriceOracle): +def __init__(pool: StablePool, coin0_oracle: lp_oracle_lib.PriceOracle): no_argument: bool = False # Init variables for raw calls @@ -62,7 +62,7 @@ def __init__(pool: StablePool, coin0_oracle: oracle_lib.PriceOracle): POOL = pool NO_ARGUMENT = no_argument - oracle_lib.__init__(coin0_oracle) + lp_oracle_lib.__init__(coin0_oracle) @internal @@ -86,9 +86,9 @@ def _price_in_coin0() -> uint256: @external @view def price() -> uint256: - return self._price_in_coin0() * oracle_lib._coin0_oracle_price() // 10 ** 18 + return self._price_in_coin0() * lp_oracle_lib._coin0_oracle_price() // 10 ** 18 @external def price_w() -> uint256: - return self._price_in_coin0() * oracle_lib._coin0_oracle_price_w() // 10 ** 18 + return self._price_in_coin0() * lp_oracle_lib._coin0_oracle_price_w() // 10 ** 18 diff --git a/contracts/price_oracles/lp-oracles/oracle_lib.vy b/contracts/price_oracles/lp-oracles/lp_oracle_lib.vy similarity index 100% rename from contracts/price_oracles/lp-oracles/oracle_lib.vy rename to contracts/price_oracles/lp-oracles/lp_oracle_lib.vy From 2ac34645c7fa9f362dec44b3931dbd655b4ab7d9 Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 25 Jun 2025 12:39:14 +0400 Subject: [PATCH 007/413] refactor: use ownable for LPOracleFactory --- .../lp-oracles/LPOracleFactory.vy | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/contracts/price_oracles/lp-oracles/LPOracleFactory.vy b/contracts/price_oracles/lp-oracles/LPOracleFactory.vy index 8d2f7df9..e2667a0a 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleFactory.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleFactory.vy @@ -3,6 +3,10 @@ #pragma evm-version shanghai +from snekmate.auth import ownable +initializes: ownable + + event DeployOracle: oracle: indexed(address) pool: indexed(address) @@ -30,7 +34,6 @@ oracles: public(address[MAX_ORACLES]) oracle_map: HashMap[address, HashMap[address, HashMap[address, address]]] # oracle_map[pool][coin0_oracle][implementation] -> oracle oracle_info: HashMap[address, OracleInfo] # oracle_info[oracle] -> OracleInfo -admin: public(address) stable_implementation: public(address) crypto_implementation: public(address) @@ -41,7 +44,9 @@ def __init__(admin: address): @notice Factory which creates StablePool and CryptoPool LP Oracles from blueprints @param admin Admin of the factory (ideally DAO) """ - self.admin = admin + ownable.__init__() + ownable._transfer_ownership(admin) + @external @@ -114,21 +119,9 @@ def set_implementations(stable_implementation: address, crypto_implementation: a @param stable_implementation Address of the StablePool LP Oracle blueprint @param crypto_implementation Address of the CryptoPool LP Oracle blueprint """ - assert msg.sender == self.admin, "Admin only" + ownable._check_owner() assert stable_implementation != empty(address) assert crypto_implementation != empty(address) self.stable_implementation = stable_implementation self.crypto_implementation = crypto_implementation log SetImplementations(stable_implementation, crypto_implementation) - - -@external -@nonreentrant -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, "Admin only" - self.admin = admin - log SetAdmin(admin) From ee6d98fb45c804d77c28dc29ae80d6316d51f81c Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 25 Jun 2025 12:42:07 +0400 Subject: [PATCH 008/413] refactor: use kwargs for events and struct --- contracts/price_oracles/lp-oracles/LPOracleFactory.vy | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/contracts/price_oracles/lp-oracles/LPOracleFactory.vy b/contracts/price_oracles/lp-oracles/LPOracleFactory.vy index e2667a0a..8739baa1 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleFactory.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleFactory.vy @@ -71,12 +71,8 @@ def deploy_oracle(pool: address, coin0_oracle: address) -> address: self.oracles[N] = oracle self.n_oracles = N + 1 self.oracle_map[pool][coin0_oracle][implementation] = oracle - self.oracle_info[oracle] = OracleInfo({ - pool: pool, - coin0_oracle: coin0_oracle, - implementation: implementation - }) - log DeployOracle(oracle, pool, coin0_oracle, implementation) + self.oracle_info[oracle] = OracleInfo(pool=pool, coin0_oracle=coin0_oracle, implementation=implementation) + log DeployOracle(oracle=oracle, pool=pool, coin0_oracle=coin0_oracle, implementation=implementation) return oracle @@ -124,4 +120,4 @@ def set_implementations(stable_implementation: address, crypto_implementation: a assert crypto_implementation != empty(address) self.stable_implementation = stable_implementation self.crypto_implementation = crypto_implementation - log SetImplementations(stable_implementation, crypto_implementation) + log SetImplementations(stable_implementation=stable_implementation, crypto_implementation=crypto_implementation) From d758de7a693c6779a8a8f534e535d97efff6bad1 Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 25 Jun 2025 12:44:11 +0400 Subject: [PATCH 009/413] chore: remove SetAdmin event --- contracts/price_oracles/lp-oracles/LPOracleFactory.vy | 3 --- 1 file changed, 3 deletions(-) diff --git a/contracts/price_oracles/lp-oracles/LPOracleFactory.vy b/contracts/price_oracles/lp-oracles/LPOracleFactory.vy index 8739baa1..57edfa8e 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleFactory.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleFactory.vy @@ -13,9 +13,6 @@ event DeployOracle: coin0_oracle: address implementation: address -event SetAdmin: - admin: address - event SetImplementations: stable_implementation: address crypto_implementation: address From 17918e54e6eab10832eeb2c90a6108655297b263 Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 25 Jun 2025 15:13:10 +0400 Subject: [PATCH 010/413] fix: export ownable interface --- contracts/price_oracles/lp-oracles/LPOracleFactory.vy | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/price_oracles/lp-oracles/LPOracleFactory.vy b/contracts/price_oracles/lp-oracles/LPOracleFactory.vy index 57edfa8e..10134306 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleFactory.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleFactory.vy @@ -5,6 +5,7 @@ from snekmate.auth import ownable initializes: ownable +exports: ownable.__interface__ event DeployOracle: From 8a0240524a41199bda8bce793fdd69959bed3df9 Mon Sep 17 00:00:00 2001 From: WormholeOracle <51072084+WormholeOracle@users.noreply.github.com> Date: Fri, 27 Jun 2025 15:52:19 +0200 Subject: [PATCH 011/413] Create OracleProxy.vy Oracle proxy for LlamaLend markets that allows DAO to set oracle implementation post-deployment. --- contracts/price_oracles/OracleProxy.vy | 124 +++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 contracts/price_oracles/OracleProxy.vy diff --git a/contracts/price_oracles/OracleProxy.vy b/contracts/price_oracles/OracleProxy.vy new file mode 100644 index 00000000..6ec40ce3 --- /dev/null +++ b/contracts/price_oracles/OracleProxy.vy @@ -0,0 +1,124 @@ +# @version 0.3.10 +""" +@title OracleProxy +@notice Oracle proxy allowing LlamaLend factory admin to set price oracle contract after deployment +@author Curve.Fi +@license MIT +""" + +interface IFactory: + def admin() -> address: view + +interface IPriceOracle: + def price() -> uint256: view + def price_w() -> uint256: nonpayable + +event PriceOracleSet: + new_implementation: address + +event MaxDeviationSet: + max_deviation: uint256 + +MAX_DEVIATION_BPS: constant(uint256) = 5000 # 50% + +factory: public(IFactory) +implementation: public(address) +max_deviation: public(uint256) + +@external +def __init__(_implementation: address, _factory: IFactory, _max_deviation: uint256): + """ + @notice Initializer for Llamalend oracle proxy + @param _implementation oracle implementation contract + @param _factory LlamaLend factory contract + @param _max_deviation max price deviation when setting new oracle, in BPS (e.g. 500 == 5%) + """ + assert _max_deviation > 0, "Invalid max deviation" + assert _max_deviation <= MAX_DEVIATION_BPS, "Invalid max deviation" + assert _factory.address != ZERO_ADDRESS + self._validate_price_oracle(_implementation) + self.implementation = _implementation + self.factory = _factory + self.max_deviation = _max_deviation + +@internal +def _validate_price_oracle(_oracle: address) -> uint256: + """ + @notice Validates the new implementation has methods implemented correctly + """ + assert _oracle != ZERO_ADDRESS, "Invalid address" + + block_price: uint256 = IPriceOracle(_oracle).price() + assert IPriceOracle(_oracle).price() > 0, "price() call failed" + assert IPriceOracle(_oracle).price_w() > 0, "price_w() call failed" + + return block_price + +@internal +def _check_price_deviation(_old_oracle: address, new_price: uint256): + """ + @notice Ensures price returned by new implementation is within acceptable bounds + """ + old_price: uint256 = IPriceOracle(_old_oracle).price() + + if old_price > 0: + 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_implementation: address): + """ + @notice Sets a new oracle implementation contract + @param _new_implementation new oracle implementation contract + """ + assert msg.sender == self.factory.admin(), "Not authorized" + + block_price: uint256 = self._validate_price_oracle(_new_implementation) + + # If current implementation price() is borked, + # skip the price deviation check + success: bool = False + response: Bytes[32] = b"" + + success, response = raw_call( + self.implementation, + method_id("price()"), + max_outsize=32, + is_static_call=True, + revert_on_failure=False + ) + + if success: + self._check_price_deviation(self.implementation, block_price) + + self.implementation = _new_implementation + log PriceOracleSet(_new_implementation) + +@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 == self.factory.admin(), "Not authorized" + assert _max_deviation > 0, "Invalid deviation" + assert _max_deviation <= MAX_DEVIATION_BPS, "Deviation too high" + + self.max_deviation = _max_deviation + log MaxDeviationSet(_max_deviation) + +@external +@view +def price() -> uint256: + """ + @notice Passes price() from implementation contract + """ + return IPriceOracle(self.implementation).price() + +@external +def price_w() -> uint256: + """ + @notice Calls price_w() on implementation contract + """ + return IPriceOracle(self.implementation).price_w() From 5440f7f7c98c3b12f75d20a9607c7b83f38412a0 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 30 Jun 2025 13:09:18 +0400 Subject: [PATCH 012/413] test: generalized tests for lp oracles --- tests_forked/price_oracles/conftest.py | 5 + .../test_lp_oracle_compare_to_spot.py | 383 ++++++++++-------- 2 files changed, 214 insertions(+), 174 deletions(-) diff --git a/tests_forked/price_oracles/conftest.py b/tests_forked/price_oracles/conftest.py index 0bc0f435..10633a9b 100644 --- a/tests_forked/price_oracles/conftest.py +++ b/tests_forked/price_oracles/conftest.py @@ -19,6 +19,11 @@ 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): 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 index e7ad18ef..28d43d7d 100644 --- a/tests_forked/price_oracles/test_lp_oracle_compare_to_spot.py +++ b/tests_forked/price_oracles/test_lp_oracle_compare_to_spot.py @@ -2,7 +2,7 @@ from .settings import EXPLORER_URL, EXPLORER_TOKEN -def test_tricrypto_usdc(lp_oracle_factory, stablecoin_aggregator, admin): +def test_tricrypto_usdc(lp_oracle_factory, stablecoin_aggregator, admin, trader): tricrypto_usdc_pool_address = "0x7F86Bf177Dd4F3494b841a37e810A34dD56c829B" crvusd_usdc_pool_address = "0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E" @@ -17,101 +17,46 @@ def test_tricrypto_usdc(lp_oracle_factory, stablecoin_aggregator, admin): tricrypto_usdc_usd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleCrypto.vy').at( lp_oracle_factory.deploy_oracle(tricrypto_usdc_pool_address, usdc_usd_oracle.address)) # USDC/LP * crvUSD/USDC * USD/crvUSD - # --- Compare current oracle and spot prices --- + # --- Compare 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 --- + 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) - print("\nTrade 1000 BTC -> POOL -> X USDC") + with boa.env.prank(trader): + usdc.approve(tricrypto_usdc_pool, 2**256-1) 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() + 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) - # 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 + 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) - 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) + # Oracle price + lp_oracle_price_crvusd = tricrypto_usdc_crvusd_lp_oracle.price() + lp_oracle_price_usd = tricrypto_usdc_usd_lp_oracle.price() - 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) + # 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("USDC pool balance:", usdc.balanceOf(tricrypto_usdc_pool) / 10**6, ", WBTC pool balance:", wbtc.balanceOf(tricrypto_usdc_pool) / 10**8) + 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 - # 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(lp_oracle_factory, stablecoin_aggregator, admin): +def test_tricrypto_usdt(lp_oracle_factory, stablecoin_aggregator, admin, trader): tricrypto_usdt_pool_address = "0xf5f5B97624542D72A9E06f04804Bf81baA15e2B4" crvusd_usdt_pool_address = "0x390f3595bCa2Df7d23783dFd126427CCeb997BF4" @@ -126,44 +71,95 @@ def test_tricrypto_usdt(lp_oracle_factory, stablecoin_aggregator, admin): tricrypto_usdt_usd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleCrypto.vy').at( lp_oracle_factory.deploy_oracle(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() + # --- Compare oracle and spot prices --- - # 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 + # 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) - 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 + 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) -def test_tricrv(lp_oracle_factory, stablecoin_aggregator, admin): + # 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 if target_p / initial_wbtc_spot_price < 0.95 else 1e-3 + 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" - tricrypto_usdt_pool = boa.from_etherscan(tricrv_pool_address, "TriCRV", uri=EXPLORER_URL, api_key=EXPLORER_TOKEN) + 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/lp-oracles/LPOracleCrypto.vy').at( lp_oracle_factory.deploy_oracle(tricrv_pool_address, "0x0000000000000000000000000000000000000000")) # USDT/LP * crvUSD/USDT tricrv_usd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleCrypto.vy').at( lp_oracle_factory.deploy_oracle(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() + # --- 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 = 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 + # 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 - 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 + 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): +def test_strategic_reserve(lp_oracle_factory, stablecoin_aggregator, admin, trader): strategic_reserve_pool_address = "0x4f493B7dE8aAC7d55F71853688b1F7C8F0243C85" crvusd_usdc_pool_address = "0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E" @@ -178,21 +174,45 @@ def test_strategic_reserve(lp_oracle_factory, stablecoin_aggregator, admin): strategic_reserve_usd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleStable.vy').at( lp_oracle_factory.deploy_oracle(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() + # --- 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) - # 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 + 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) - 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 + # 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 -def test_weeth_weth(lp_oracle_factory, stablecoin_aggregator, admin): + 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" @@ -206,27 +226,57 @@ def test_weeth_weth(lp_oracle_factory, stablecoin_aggregator, admin): 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): - tricrypto_usdt_crvusd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleStable.vy').at( + weeth_ng_pool_crvusd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleStable.vy').at( lp_oracle_factory.deploy_oracle(weeth_ng_pool_address, usdt_crvusd_oracle.address)) # ETH/LP * crvUSD/ETH - tricrypto_usdt_usd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleStable.vy').at( + weeth_ng_pool_usd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleStable.vy').at( lp_oracle_factory.deploy_oracle(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() + # --- 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) - # 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 + 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) - 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 + 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() -def test_cvxcrv(lp_oracle_factory, stablecoin_aggregator, admin): + # 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 60 * 10**16 < target_p < 160 * 10**16 else 0.011 + 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" @@ -241,53 +291,38 @@ def test_cvxcrv(lp_oracle_factory, stablecoin_aggregator, admin): cvxcrv_pool_usd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleStable.vy').at( lp_oracle_factory.deploy_oracle(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 + # --- Compare oracle and spot prices --- - # 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) + boa.deal(crv, trader, crv.balanceOf(cvxcrv_pool) * 20) + boa.deal(cvxcrv, trader, cvxcrv.balanceOf(cvxcrv_pool) * 20) 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") + 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 From 663a3ec85a89d9a7b745e7949210dc042902455c Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 30 Jun 2025 15:51:40 +0400 Subject: [PATCH 013/413] chore: update vyper to 0.4.1 for OracleProxy --- contracts/price_oracles/OracleProxy.vy | 28 +++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/contracts/price_oracles/OracleProxy.vy b/contracts/price_oracles/OracleProxy.vy index 6ec40ce3..fde6c55b 100644 --- a/contracts/price_oracles/OracleProxy.vy +++ b/contracts/price_oracles/OracleProxy.vy @@ -1,4 +1,4 @@ -# @version 0.3.10 +# @version 0.4.1 """ @title OracleProxy @notice Oracle proxy allowing LlamaLend factory admin to set price oracle contract after deployment @@ -25,7 +25,7 @@ factory: public(IFactory) implementation: public(address) max_deviation: public(uint256) -@external +@deploy def __init__(_implementation: address, _factory: IFactory, _max_deviation: uint256): """ @notice Initializer for Llamalend oracle proxy @@ -35,7 +35,7 @@ def __init__(_implementation: address, _factory: IFactory, _max_deviation: uint2 """ assert _max_deviation > 0, "Invalid max deviation" assert _max_deviation <= MAX_DEVIATION_BPS, "Invalid max deviation" - assert _factory.address != ZERO_ADDRESS + assert _factory.address != empty(address) self._validate_price_oracle(_implementation) self.implementation = _implementation self.factory = _factory @@ -46,11 +46,11 @@ def _validate_price_oracle(_oracle: address) -> uint256: """ @notice Validates the new implementation has methods implemented correctly """ - assert _oracle != ZERO_ADDRESS, "Invalid address" + assert _oracle != empty(address), "Invalid address" - block_price: uint256 = IPriceOracle(_oracle).price() - assert IPriceOracle(_oracle).price() > 0, "price() call failed" - assert IPriceOracle(_oracle).price_w() > 0, "price_w() call failed" + block_price: uint256 = staticcall IPriceOracle(_oracle).price() + assert block_price > 0, "price() call failed" + assert extcall IPriceOracle(_oracle).price_w() > 0, "price_w() call failed" return block_price @@ -59,11 +59,11 @@ def _check_price_deviation(_old_oracle: address, new_price: uint256): """ @notice Ensures price returned by new implementation is within acceptable bounds """ - old_price: uint256 = IPriceOracle(_old_oracle).price() + old_price: uint256 = staticcall IPriceOracle(_old_oracle).price() if old_price > 0: 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 + max_delta: uint256 = old_price * self.max_deviation // 10_000 assert delta <= max_delta, "Price deviation too high" @external @@ -72,7 +72,7 @@ def set_price_oracle(_new_implementation: address): @notice Sets a new oracle implementation contract @param _new_implementation new oracle implementation contract """ - assert msg.sender == self.factory.admin(), "Not authorized" + assert msg.sender == staticcall self.factory.admin(), "Not authorized" block_price: uint256 = self._validate_price_oracle(_new_implementation) @@ -101,7 +101,7 @@ 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 == self.factory.admin(), "Not authorized" + assert msg.sender == staticcall self.factory.admin(), "Not authorized" assert _max_deviation > 0, "Invalid deviation" assert _max_deviation <= MAX_DEVIATION_BPS, "Deviation too high" @@ -114,11 +114,11 @@ def price() -> uint256: """ @notice Passes price() from implementation contract """ - return IPriceOracle(self.implementation).price() - + return staticcall IPriceOracle(self.implementation).price() + @external def price_w() -> uint256: """ @notice Calls price_w() on implementation contract """ - return IPriceOracle(self.implementation).price_w() + return extcall IPriceOracle(self.implementation).price_w() From 6ce00e68c35421464f6905bde2d33e42fc2cf379 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 30 Jun 2025 15:56:22 +0400 Subject: [PATCH 014/413] refactor: make FACTORY immutable --- contracts/price_oracles/OracleProxy.vy | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/contracts/price_oracles/OracleProxy.vy b/contracts/price_oracles/OracleProxy.vy index fde6c55b..339d27f9 100644 --- a/contracts/price_oracles/OracleProxy.vy +++ b/contracts/price_oracles/OracleProxy.vy @@ -6,6 +6,7 @@ @license MIT """ + interface IFactory: def admin() -> address: view @@ -13,18 +14,21 @@ interface IPriceOracle: def price() -> uint256: view def price_w() -> uint256: nonpayable + event PriceOracleSet: new_implementation: address event MaxDeviationSet: max_deviation: uint256 + MAX_DEVIATION_BPS: constant(uint256) = 5000 # 50% -factory: public(IFactory) +FACTORY: public(immutable(IFactory)) implementation: public(address) max_deviation: public(uint256) + @deploy def __init__(_implementation: address, _factory: IFactory, _max_deviation: uint256): """ @@ -38,9 +42,10 @@ def __init__(_implementation: address, _factory: IFactory, _max_deviation: uint2 assert _factory.address != empty(address) self._validate_price_oracle(_implementation) self.implementation = _implementation - self.factory = _factory + FACTORY = _factory self.max_deviation = _max_deviation + @internal def _validate_price_oracle(_oracle: address) -> uint256: """ @@ -54,6 +59,7 @@ def _validate_price_oracle(_oracle: address) -> uint256: return block_price + @internal def _check_price_deviation(_old_oracle: address, new_price: uint256): """ @@ -66,13 +72,14 @@ def _check_price_deviation(_old_oracle: address, new_price: uint256): max_delta: uint256 = old_price * self.max_deviation // 10_000 assert delta <= max_delta, "Price deviation too high" + @external def set_price_oracle(_new_implementation: address): """ @notice Sets a new oracle implementation contract @param _new_implementation new oracle implementation contract """ - assert msg.sender == staticcall self.factory.admin(), "Not authorized" + assert msg.sender == staticcall FACTORY.admin(), "Not authorized" block_price: uint256 = self._validate_price_oracle(_new_implementation) @@ -95,19 +102,21 @@ def set_price_oracle(_new_implementation: address): self.implementation = _new_implementation log PriceOracleSet(_new_implementation) + @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.admin(), "Not authorized" + assert msg.sender == staticcall FACTORY.admin(), "Not authorized" assert _max_deviation > 0, "Invalid deviation" assert _max_deviation <= MAX_DEVIATION_BPS, "Deviation too high" self.max_deviation = _max_deviation log MaxDeviationSet(_max_deviation) + @external @view def price() -> uint256: @@ -116,6 +125,7 @@ def price() -> uint256: """ return staticcall IPriceOracle(self.implementation).price() + @external def price_w() -> uint256: """ From 060c2c80945e6f2fdac6be3a42b6429e224baf99 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 30 Jun 2025 16:07:08 +0400 Subject: [PATCH 015/413] refactor: rename implementation -> oracle, use IPriceOracle instead of address --- contracts/price_oracles/OracleProxy.vy | 59 ++++++++++++++------------ 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/contracts/price_oracles/OracleProxy.vy b/contracts/price_oracles/OracleProxy.vy index 339d27f9..c14cb74e 100644 --- a/contracts/price_oracles/OracleProxy.vy +++ b/contracts/price_oracles/OracleProxy.vy @@ -16,7 +16,7 @@ interface IPriceOracle: event PriceOracleSet: - new_implementation: address + new_oracle: address event MaxDeviationSet: max_deviation: uint256 @@ -25,47 +25,48 @@ event MaxDeviationSet: MAX_DEVIATION_BPS: constant(uint256) = 5000 # 50% FACTORY: public(immutable(IFactory)) -implementation: public(address) + +oracle: public(IPriceOracle) max_deviation: public(uint256) @deploy -def __init__(_implementation: address, _factory: IFactory, _max_deviation: uint256): +def __init__(_oracle: IPriceOracle, _factory: IFactory, _max_deviation: uint256): """ @notice Initializer for Llamalend oracle proxy - @param _implementation oracle implementation contract + @param _oracle Oracle contract @param _factory LlamaLend factory contract - @param _max_deviation max price deviation when setting new oracle, in BPS (e.g. 500 == 5%) + @param _max_deviation Max price deviation when setting new oracle, in BPS (e.g. 500 == 5%) """ assert _max_deviation > 0, "Invalid max deviation" assert _max_deviation <= MAX_DEVIATION_BPS, "Invalid max deviation" assert _factory.address != empty(address) - self._validate_price_oracle(_implementation) - self.implementation = _implementation + self._validate_price_oracle(_oracle) + self.oracle = _oracle FACTORY = _factory self.max_deviation = _max_deviation @internal -def _validate_price_oracle(_oracle: address) -> uint256: +def _validate_price_oracle(_oracle: IPriceOracle) -> uint256: """ - @notice Validates the new implementation has methods implemented correctly + @notice Validates the new oracle has methods implemented correctly """ - assert _oracle != empty(address), "Invalid address" + assert _oracle.address != empty(address), "Invalid address" - block_price: uint256 = staticcall IPriceOracle(_oracle).price() + block_price: uint256 = staticcall _oracle.price() assert block_price > 0, "price() call failed" - assert extcall IPriceOracle(_oracle).price_w() > 0, "price_w() call failed" + assert extcall _oracle.price_w() > 0, "price_w() call failed" return block_price @internal -def _check_price_deviation(_old_oracle: address, new_price: uint256): +def _check_price_deviation(_old_oracle: IPriceOracle, new_price: uint256): """ - @notice Ensures price returned by new implementation is within acceptable bounds + @notice Ensures price returned by new oracle is within acceptable bounds """ - old_price: uint256 = staticcall IPriceOracle(_old_oracle).price() + old_price: uint256 = staticcall _old_oracle.price() if old_price > 0: delta: uint256 = new_price - old_price if old_price < new_price else old_price - new_price @@ -74,22 +75,22 @@ def _check_price_deviation(_old_oracle: address, new_price: uint256): @external -def set_price_oracle(_new_implementation: address): +def set_price_oracle(_new_oracle: IPriceOracle): """ - @notice Sets a new oracle implementation contract - @param _new_implementation new oracle implementation contract + @notice Sets the new oracle contract + @param _new_oracle The new oracle contract """ assert msg.sender == staticcall FACTORY.admin(), "Not authorized" - block_price: uint256 = self._validate_price_oracle(_new_implementation) + block_price: uint256 = self._validate_price_oracle(_new_oracle) - # If current implementation price() is borked, + # If current oracle price() is borked, # skip the price deviation check success: bool = False response: Bytes[32] = b"" success, response = raw_call( - self.implementation, + self.oracle.address, method_id("price()"), max_outsize=32, is_static_call=True, @@ -97,10 +98,11 @@ def set_price_oracle(_new_implementation: address): ) if success: - self._check_price_deviation(self.implementation, block_price) + self._check_price_deviation(self.oracle, block_price) + + self.oracle = _new_oracle - self.implementation = _new_implementation - log PriceOracleSet(_new_implementation) + log PriceOracleSet(_new_oracle.address) @external @@ -114,6 +116,7 @@ def set_max_deviation(_max_deviation: uint256): assert _max_deviation <= MAX_DEVIATION_BPS, "Deviation too high" self.max_deviation = _max_deviation + log MaxDeviationSet(_max_deviation) @@ -121,14 +124,14 @@ def set_max_deviation(_max_deviation: uint256): @view def price() -> uint256: """ - @notice Passes price() from implementation contract + @notice Passes price() from oracle contract """ - return staticcall IPriceOracle(self.implementation).price() + return staticcall self.oracle.price() @external def price_w() -> uint256: """ - @notice Calls price_w() on implementation contract + @notice Calls price_w() on oracle contract """ - return extcall IPriceOracle(self.implementation).price_w() + return extcall self.oracle.price_w() From b551a38741fa33047f0f7e762c53d0cc589756c0 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 30 Jun 2025 16:13:02 +0400 Subject: [PATCH 016/413] refactor: logs --- contracts/price_oracles/OracleProxy.vy | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/contracts/price_oracles/OracleProxy.vy b/contracts/price_oracles/OracleProxy.vy index c14cb74e..292235f8 100644 --- a/contracts/price_oracles/OracleProxy.vy +++ b/contracts/price_oracles/OracleProxy.vy @@ -16,7 +16,7 @@ interface IPriceOracle: event PriceOracleSet: - new_oracle: address + oracle: address event MaxDeviationSet: max_deviation: uint256 @@ -38,14 +38,16 @@ def __init__(_oracle: IPriceOracle, _factory: IFactory, _max_deviation: uint256) @param _factory LlamaLend factory contract @param _max_deviation Max price deviation when setting new oracle, in BPS (e.g. 500 == 5%) """ - assert _max_deviation > 0, "Invalid max deviation" - assert _max_deviation <= MAX_DEVIATION_BPS, "Invalid max deviation" + assert _max_deviation > 0 and _max_deviation <= MAX_DEVIATION_BPS, "Invalid max deviation" assert _factory.address != empty(address) self._validate_price_oracle(_oracle) self.oracle = _oracle FACTORY = _factory self.max_deviation = _max_deviation + log PriceOracleSet(oracle=_oracle.address) + log MaxDeviationSet(max_deviation=_max_deviation) + @internal def _validate_price_oracle(_oracle: IPriceOracle) -> uint256: @@ -102,7 +104,7 @@ def set_price_oracle(_new_oracle: IPriceOracle): self.oracle = _new_oracle - log PriceOracleSet(_new_oracle.address) + log PriceOracleSet(oracle=_new_oracle.address) @external @@ -117,7 +119,7 @@ def set_max_deviation(_max_deviation: uint256): self.max_deviation = _max_deviation - log MaxDeviationSet(_max_deviation) + log MaxDeviationSet(max_deviation=_max_deviation) @external From d7481540bf5d19450ffec26cf9a1e46ddbf65daa Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 30 Jun 2025 16:26:01 +0400 Subject: [PATCH 017/413] refactor: set_price_oracle --- contracts/price_oracles/OracleProxy.vy | 28 ++++++++++++-------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/contracts/price_oracles/OracleProxy.vy b/contracts/price_oracles/OracleProxy.vy index 292235f8..0c9b14ca 100644 --- a/contracts/price_oracles/OracleProxy.vy +++ b/contracts/price_oracles/OracleProxy.vy @@ -56,24 +56,21 @@ def _validate_price_oracle(_oracle: IPriceOracle) -> uint256: """ assert _oracle.address != empty(address), "Invalid address" - block_price: uint256 = staticcall _oracle.price() - assert block_price > 0, "price() call failed" - assert extcall _oracle.price_w() > 0, "price_w() call failed" + 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_oracle: IPriceOracle, new_price: uint256): +def _check_price_deviation(old_price: uint256, new_price: uint256): """ @notice Ensures price returned by new oracle is within acceptable bounds """ - old_price: uint256 = staticcall _old_oracle.price() - - if old_price > 0: - 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" + 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 @@ -84,7 +81,7 @@ def set_price_oracle(_new_oracle: IPriceOracle): """ assert msg.sender == staticcall FACTORY.admin(), "Not authorized" - block_price: uint256 = self._validate_price_oracle(_new_oracle) + new_price: uint256 = self._validate_price_oracle(_new_oracle) # If current oracle price() is borked, # skip the price deviation check @@ -99,8 +96,10 @@ def set_price_oracle(_new_oracle: IPriceOracle): revert_on_failure=False ) - if success: - self._check_price_deviation(self.oracle, block_price) + if success and len(response) > 0: + old_price: uint256 = abi_decode(response, uint256) + if old_price > 0: + self._check_price_deviation(old_price, new_price) self.oracle = _new_oracle @@ -114,8 +113,7 @@ def set_max_deviation(_max_deviation: uint256): @param _max_deviation New maximum deviation, must be > 0 and <= MAX_DEVIATION_BPS """ assert msg.sender == staticcall FACTORY.admin(), "Not authorized" - assert _max_deviation > 0, "Invalid deviation" - assert _max_deviation <= MAX_DEVIATION_BPS, "Deviation too high" + assert _max_deviation > 0 and _max_deviation <= MAX_DEVIATION_BPS, "Invalid deviation" self.max_deviation = _max_deviation From ef4de229dbea11a4f98275d45d74c405c5d93298 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 30 Jun 2025 19:28:12 +0400 Subject: [PATCH 018/413] feat: use proxies for LP oracles --- contracts/price_oracles/OracleProxy.vy | 31 +++++++++++++++++-- .../lp-oracles/LPOracleFactory.vy | 24 +++++++++----- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/contracts/price_oracles/OracleProxy.vy b/contracts/price_oracles/OracleProxy.vy index 0c9b14ca..c98df9ae 100644 --- a/contracts/price_oracles/OracleProxy.vy +++ b/contracts/price_oracles/OracleProxy.vy @@ -8,6 +8,7 @@ interface IFactory: + def owner() -> address: view def admin() -> address: view interface IPriceOracle: @@ -25,6 +26,7 @@ event MaxDeviationSet: MAX_DEVIATION_BPS: constant(uint256) = 5000 # 50% FACTORY: public(immutable(IFactory)) +USE_OWNER: immutable(bool) oracle: public(IPriceOracle) max_deviation: public(uint256) @@ -45,6 +47,20 @@ def __init__(_oracle: IPriceOracle, _factory: IFactory, _max_deviation: uint256) FACTORY = _factory self.max_deviation = _max_deviation + success: bool = False + response: Bytes[32] = b"" + success, response = raw_call( + _factory.address, + method_id("owner()"), + max_outsize=32, + is_static_call=True, + revert_on_failure=False + ) + if len(response) == 0: + success = False + + USE_OWNER = success + log PriceOracleSet(oracle=_oracle.address) log MaxDeviationSet(max_deviation=_max_deviation) @@ -63,6 +79,17 @@ def _validate_price_oracle(_oracle: IPriceOracle) -> uint256: return block_price +@internal +def _check_admin(): + """ + @notice Throws if the sender is not the owner/admin. + """ + if USE_OWNER: + assert msg.sender == staticcall FACTORY.owner(), "Not authorized" + else: + assert msg.sender == staticcall FACTORY.admin(), "Not authorized" + + @internal def _check_price_deviation(old_price: uint256, new_price: uint256): """ @@ -79,7 +106,7 @@ def set_price_oracle(_new_oracle: IPriceOracle): @notice Sets the new oracle contract @param _new_oracle The new oracle contract """ - assert msg.sender == staticcall FACTORY.admin(), "Not authorized" + self._check_admin() new_price: uint256 = self._validate_price_oracle(_new_oracle) @@ -112,7 +139,7 @@ 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 FACTORY.admin(), "Not authorized" + self._check_admin() assert _max_deviation > 0 and _max_deviation <= MAX_DEVIATION_BPS, "Invalid deviation" self.max_deviation = _max_deviation diff --git a/contracts/price_oracles/lp-oracles/LPOracleFactory.vy b/contracts/price_oracles/lp-oracles/LPOracleFactory.vy index 10134306..809cb9bd 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleFactory.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleFactory.vy @@ -17,6 +17,7 @@ event DeployOracle: event SetImplementations: stable_implementation: address crypto_implementation: address + proxy_implementation: address struct OracleInfo: @@ -34,6 +35,7 @@ oracle_info: HashMap[address, OracleInfo] # oracle_info[oracle] -> OracleInfo stable_implementation: public(address) crypto_implementation: public(address) +proxy_implementation: public(address) @deploy @@ -49,7 +51,7 @@ def __init__(admin: address): @external @nonreentrant -def deploy_oracle(pool: address, coin0_oracle: address) -> address: +def deploy_oracle(pool: address, coin0_oracle: address, use_proxy: bool = True, save_to_storage: bool = True) -> address: """ @notice Deploy a new LP oracle @param pool Curve pool either stable or crypto @@ -64,12 +66,16 @@ def deploy_oracle(pool: address, coin0_oracle: address) -> address: assert self.oracle_map[pool][coin0_oracle][implementation] == empty(address), "Oracle already exists" oracle: address = create_from_blueprint(implementation, pool, coin0_oracle, code_offset=3) + if use_proxy: + oracle = create_from_blueprint(self.proxy_implementation, oracle, self, convert(100, uint256), code_offset=3) + + if save_to_storage: + N: uint256 = self.n_oracles + self.oracles[N] = oracle + self.n_oracles = N + 1 + self.oracle_map[pool][coin0_oracle][implementation] = oracle + self.oracle_info[oracle] = OracleInfo(pool=pool, coin0_oracle=coin0_oracle, implementation=implementation) - N: uint256 = self.n_oracles - self.oracles[N] = oracle - self.n_oracles = N + 1 - self.oracle_map[pool][coin0_oracle][implementation] = oracle - self.oracle_info[oracle] = OracleInfo(pool=pool, coin0_oracle=coin0_oracle, implementation=implementation) log DeployOracle(oracle=oracle, pool=pool, coin0_oracle=coin0_oracle, implementation=implementation) return oracle @@ -107,7 +113,7 @@ def _is_crypto(pool: address) -> bool: @external @nonreentrant -def set_implementations(stable_implementation: address, crypto_implementation: address): +def set_implementations(stable_implementation: address, crypto_implementation: address, proxy_implementation: address): """ @notice Set new implementations (blueprints) for stable and crypto oracles. Doesn't change existing ones @param stable_implementation Address of the StablePool LP Oracle blueprint @@ -118,4 +124,6 @@ def set_implementations(stable_implementation: address, crypto_implementation: a assert crypto_implementation != empty(address) self.stable_implementation = stable_implementation self.crypto_implementation = crypto_implementation - log SetImplementations(stable_implementation=stable_implementation, crypto_implementation=crypto_implementation) + self.proxy_implementation = proxy_implementation + + log SetImplementations(stable_implementation=stable_implementation, crypto_implementation=crypto_implementation, proxy_implementation=proxy_implementation) From cca1983776bdbb861dcaba5a21f6087a0fb7416b Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 30 Jun 2025 19:28:35 +0400 Subject: [PATCH 019/413] test: use proxies for LP oracles --- tests_forked/price_oracles/conftest.py | 10 +++++-- .../test_lp_oracle_compare_to_spot.py | 28 +++++++++---------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/tests_forked/price_oracles/conftest.py b/tests_forked/price_oracles/conftest.py index 10633a9b..d3526ba6 100644 --- a/tests_forked/price_oracles/conftest.py +++ b/tests_forked/price_oracles/conftest.py @@ -37,8 +37,14 @@ def crypto_impl(admin): @pytest.fixture(scope="module") -def lp_oracle_factory(admin, stable_impl, crypto_impl): +def proxy_impl(admin): + with boa.env.prank(admin): + return boa.load_partial('contracts/price_oracles/OracleProxy.vy').deploy_as_blueprint() + + +@pytest.fixture(scope="module") +def lp_oracle_factory(admin, stable_impl, crypto_impl, proxy_impl): with boa.env.prank(admin): factory = boa.load("contracts/price_oracles/lp-oracles/LPOracleFactory.vy", admin) - factory.set_implementations(stable_impl, crypto_impl) + factory.set_implementations(stable_impl, crypto_impl, proxy_impl) return factory 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 index 28d43d7d..b224f200 100644 --- a/tests_forked/price_oracles/test_lp_oracle_compare_to_spot.py +++ b/tests_forked/price_oracles/test_lp_oracle_compare_to_spot.py @@ -12,9 +12,10 @@ def test_tricrypto_usdc(lp_oracle_factory, stablecoin_aggregator, admin, trader) 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/lp-oracles/LPOracleCrypto.vy').at( + tricrypto_usdc_crvusd_lp_oracle = boa.load_partial('contracts/price_oracles/OracleProxy.vy').at( lp_oracle_factory.deploy_oracle(tricrypto_usdc_pool_address, usdc_crvusd_oracle.address)) # USDC/LP * crvUSD/USDC - tricrypto_usdc_usd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleCrypto.vy').at( + tricrypto_usdc_crvusd_lp_oracle.set_max_deviation(500) + tricrypto_usdc_usd_lp_oracle = boa.load_partial('contracts/price_oracles/OracleProxy.vy').at( lp_oracle_factory.deploy_oracle(tricrypto_usdc_pool_address, usdc_usd_oracle.address)) # USDC/LP * crvUSD/USDC * USD/crvUSD # --- Compare oracle and spot prices --- @@ -66,9 +67,9 @@ def test_tricrypto_usdt(lp_oracle_factory, stablecoin_aggregator, admin, trader) 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/lp-oracles/LPOracleCrypto.vy').at( + tricrypto_usdt_crvusd_lp_oracle = boa.load_partial('contracts/price_oracles/OracleProxy.vy').at( lp_oracle_factory.deploy_oracle(tricrypto_usdt_pool_address, usdt_crvusd_oracle.address)) # USDT/LP * crvUSD/USDT - tricrypto_usdt_usd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleCrypto.vy').at( + tricrypto_usdt_usd_lp_oracle = boa.load_partial('contracts/price_oracles/OracleProxy.vy').at( lp_oracle_factory.deploy_oracle(tricrypto_usdt_pool_address, usdt_usd_oracle.address)) # USDT/LP * crvUSD/USDT * USD/crvUSD # --- Compare oracle and spot prices --- @@ -104,7 +105,6 @@ def test_tricrypto_usdt(lp_oracle_factory, stablecoin_aggregator, admin, trader) lp_spot_price_crvusd = crvusd_from_lp lp_spot_price_usd = crvusd_from_lp * stablecoin_aggregator.price() // 10 ** 18 - # delta = 6e-3 if target_p / initial_wbtc_spot_price < 0.95 else 1e-3 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 @@ -116,9 +116,9 @@ def test_tricrv(lp_oracle_factory, stablecoin_aggregator, admin, trader): 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/lp-oracles/LPOracleCrypto.vy').at( + tricrv_crvusd_lp_oracle = boa.load_partial('contracts/price_oracles/OracleProxy.vy').at( lp_oracle_factory.deploy_oracle(tricrv_pool_address, "0x0000000000000000000000000000000000000000")) # USDT/LP * crvUSD/USDT - tricrv_usd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleCrypto.vy').at( + tricrv_usd_lp_oracle = boa.load_partial('contracts/price_oracles/OracleProxy.vy').at( lp_oracle_factory.deploy_oracle(tricrv_pool_address, stablecoin_aggregator.address)) # USDT/LP * crvUSD/USDT * USD/crvUSD # --- Compare oracle and spot prices --- @@ -169,9 +169,9 @@ def test_strategic_reserve(lp_oracle_factory, stablecoin_aggregator, admin, trad 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/lp-oracles/LPOracleStable.vy').at( + strategic_reserve_crvusd_lp_oracle = boa.load_partial('contracts/price_oracles/OracleProxy.vy').at( lp_oracle_factory.deploy_oracle(strategic_reserve_pool_address, usdc_crvusd_oracle.address)) # USDC/LP * crvUSD/USDC - strategic_reserve_usd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleStable.vy').at( + strategic_reserve_usd_lp_oracle = boa.load_partial('contracts/price_oracles/OracleProxy.vy').at( lp_oracle_factory.deploy_oracle(strategic_reserve_pool_address, usdc_usd_oracle.address)) # USDC/LP * crvUSD/USDC * USD/crvUSD # --- Compare oracle and spot prices --- @@ -226,9 +226,9 @@ def test_weeth_weth(lp_oracle_factory, stablecoin_aggregator, admin, trader): 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/lp-oracles/LPOracleStable.vy').at( + weeth_ng_pool_crvusd_lp_oracle = boa.load_partial('contracts/price_oracles/OracleProxy.vy').at( lp_oracle_factory.deploy_oracle(weeth_ng_pool_address, usdt_crvusd_oracle.address)) # ETH/LP * crvUSD/ETH - weeth_ng_pool_usd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleStable.vy').at( + weeth_ng_pool_usd_lp_oracle = boa.load_partial('contracts/price_oracles/OracleProxy.vy').at( lp_oracle_factory.deploy_oracle(weeth_ng_pool_address, usdt_usd_oracle.address)) # ETH/LP * crvUSD/ETH * USD/crvUSD # --- Compare oracle and spot prices --- @@ -270,7 +270,7 @@ def test_weeth_weth(lp_oracle_factory, stablecoin_aggregator, admin, trader): 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 60 * 10**16 < target_p < 160 * 10**16 else 0.011 + 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 @@ -286,9 +286,9 @@ def test_cvxcrv(lp_oracle_factory, stablecoin_aggregator, admin, trader): 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/lp-oracles/LPOracleStable.vy').at( + cvxcrv_pool_crvusd_lp_oracle = boa.load_partial('contracts/price_oracles/OracleProxy.vy').at( lp_oracle_factory.deploy_oracle(cvxcrv_pool_address, crv_crvusd_oracle)) # CRV/LP * crvUSD/CRV - cvxcrv_pool_usd_lp_oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleStable.vy').at( + cvxcrv_pool_usd_lp_oracle = boa.load_partial('contracts/price_oracles/OracleProxy.vy').at( lp_oracle_factory.deploy_oracle(cvxcrv_pool_address, crv_usd_oracle)) # CRV/LP * crvUSD/CRV * USD/crvUSD # --- Compare oracle and spot prices --- From b802b78ec9fef62d88bb150c028837ea68f74aac Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 7 Jul 2025 16:22:42 +0400 Subject: [PATCH 020/413] feat: ProxyOracleFctory --- contracts/price_oracles/proxy/ProxyOracle.vy | 131 ++++++++++++++++++ .../price_oracles/proxy/ProxyOracleFactory.vy | 69 +++++++++ 2 files changed, 200 insertions(+) create mode 100644 contracts/price_oracles/proxy/ProxyOracle.vy create mode 100644 contracts/price_oracles/proxy/ProxyOracleFactory.vy diff --git a/contracts/price_oracles/proxy/ProxyOracle.vy b/contracts/price_oracles/proxy/ProxyOracle.vy new file mode 100644 index 00000000..b24220c6 --- /dev/null +++ b/contracts/price_oracles/proxy/ProxyOracle.vy @@ -0,0 +1,131 @@ +# @version 0.4.1 +#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 +""" + + +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..85dd651a --- /dev/null +++ b/contracts/price_oracles/proxy/ProxyOracleFactory.vy @@ -0,0 +1,69 @@ +# @version 0.4.1 +#pragma optimize gas +#pragma evm-version shanghai + +""" +@title ProxyOracleFactory +@author Curve +@license MIT +@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 + + +event SetOracle: + proxy: indexed(address) + oracle: indexed(address) + + +PROXY_ORACLE_IMPLEMENTATION: public(immutable(address)) +get_proxy: public(HashMap[address, address]) + + +@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 set_new_oracle(_proxy: IProxyOracle, _new_oracle: address, _skip_price_deviation_check: bool = False): + ownable._check_owner() + extcall _proxy.set_price_oracle(_new_oracle, _skip_price_deviation_check) + + log SetOracle(proxy=_proxy.address, oracle=_new_oracle) From 115e10f50a3a6258bbdd874f66e9f37b05ff4ab8 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 7 Jul 2025 16:23:53 +0400 Subject: [PATCH 021/413] refactor: use ProxyOracleFactory in LPOracleFactory --- contracts/price_oracles/OracleProxy.vy | 164 ------------------ .../lp-oracles/LPOracleFactory.vy | 18 +- .../price_oracles/lp-oracles/lp_oracle_lib.vy | 6 +- tests_forked/price_oracles/conftest.py | 14 +- .../test_lp_oracle_compare_to_spot.py | 24 +-- 5 files changed, 37 insertions(+), 189 deletions(-) delete mode 100644 contracts/price_oracles/OracleProxy.vy diff --git a/contracts/price_oracles/OracleProxy.vy b/contracts/price_oracles/OracleProxy.vy deleted file mode 100644 index c98df9ae..00000000 --- a/contracts/price_oracles/OracleProxy.vy +++ /dev/null @@ -1,164 +0,0 @@ -# @version 0.4.1 -""" -@title OracleProxy -@notice Oracle proxy allowing LlamaLend factory admin to set price oracle contract after deployment -@author Curve.Fi -@license MIT -""" - - -interface IFactory: - def owner() -> address: view - def admin() -> 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(immutable(IFactory)) -USE_OWNER: immutable(bool) - -oracle: public(IPriceOracle) -max_deviation: public(uint256) - - -@deploy -def __init__(_oracle: IPriceOracle, _factory: IFactory, _max_deviation: uint256): - """ - @notice Initializer for Llamalend oracle proxy - @param _oracle Oracle contract - @param _factory LlamaLend factory contract - @param _max_deviation Max price deviation when setting new oracle, in BPS (e.g. 500 == 5%) - """ - assert _max_deviation > 0 and _max_deviation <= MAX_DEVIATION_BPS, "Invalid max deviation" - assert _factory.address != empty(address) - self._validate_price_oracle(_oracle) - self.oracle = _oracle - FACTORY = _factory - self.max_deviation = _max_deviation - - success: bool = False - response: Bytes[32] = b"" - success, response = raw_call( - _factory.address, - method_id("owner()"), - max_outsize=32, - is_static_call=True, - revert_on_failure=False - ) - if len(response) == 0: - success = False - - USE_OWNER = success - - 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_admin(): - """ - @notice Throws if the sender is not the owner/admin. - """ - if USE_OWNER: - assert msg.sender == staticcall FACTORY.owner(), "Not authorized" - else: - assert msg.sender == staticcall FACTORY.admin(), "Not authorized" - - -@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): - """ - @notice Sets the new oracle contract - @param _new_oracle The new oracle contract - """ - self._check_admin() - - new_price: uint256 = self._validate_price_oracle(_new_oracle) - - # If current oracle price() is borked, - # skip the price deviation check - success: bool = False - response: Bytes[32] = b"" - - success, response = raw_call( - self.oracle.address, - method_id("price()"), - max_outsize=32, - is_static_call=True, - revert_on_failure=False - ) - - if success and len(response) > 0: - old_price: uint256 = abi_decode(response, uint256) - if old_price > 0: - self._check_price_deviation(old_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 - """ - self._check_admin() - 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/lp-oracles/LPOracleFactory.vy b/contracts/price_oracles/lp-oracles/LPOracleFactory.vy index 809cb9bd..06129440 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleFactory.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleFactory.vy @@ -8,6 +8,10 @@ initializes: ownable exports: ownable.__interface__ +interface IProxyOracleFactory: + def deploy_proxy_oracle(_oracle: address) -> address: nonpayable + + event DeployOracle: oracle: indexed(address) pool: indexed(address) @@ -17,7 +21,6 @@ event DeployOracle: event SetImplementations: stable_implementation: address crypto_implementation: address - proxy_implementation: address struct OracleInfo: @@ -35,11 +38,11 @@ oracle_info: HashMap[address, OracleInfo] # oracle_info[oracle] -> OracleInfo stable_implementation: public(address) crypto_implementation: public(address) -proxy_implementation: public(address) +PROXY_ORACLE_FACTORY: public(immutable(IProxyOracleFactory)) @deploy -def __init__(admin: address): +def __init__(admin: address, _proxy_oracle_factory: IProxyOracleFactory): """ @notice Factory which creates StablePool and CryptoPool LP Oracles from blueprints @param admin Admin of the factory (ideally DAO) @@ -47,6 +50,8 @@ def __init__(admin: address): ownable.__init__() ownable._transfer_ownership(admin) + assert _proxy_oracle_factory.address != empty(address) + PROXY_ORACLE_FACTORY = _proxy_oracle_factory @external @@ -67,7 +72,7 @@ def deploy_oracle(pool: address, coin0_oracle: address, use_proxy: bool = True, oracle: address = create_from_blueprint(implementation, pool, coin0_oracle, code_offset=3) if use_proxy: - oracle = create_from_blueprint(self.proxy_implementation, oracle, self, convert(100, uint256), code_offset=3) + oracle = extcall PROXY_ORACLE_FACTORY.deploy_proxy_oracle(oracle) if save_to_storage: N: uint256 = self.n_oracles @@ -113,7 +118,7 @@ def _is_crypto(pool: address) -> bool: @external @nonreentrant -def set_implementations(stable_implementation: address, crypto_implementation: address, proxy_implementation: address): +def set_implementations(stable_implementation: address, crypto_implementation: address): """ @notice Set new implementations (blueprints) for stable and crypto oracles. Doesn't change existing ones @param stable_implementation Address of the StablePool LP Oracle blueprint @@ -124,6 +129,5 @@ def set_implementations(stable_implementation: address, crypto_implementation: a assert crypto_implementation != empty(address) self.stable_implementation = stable_implementation self.crypto_implementation = crypto_implementation - self.proxy_implementation = proxy_implementation - log SetImplementations(stable_implementation=stable_implementation, crypto_implementation=crypto_implementation, proxy_implementation=proxy_implementation) + log SetImplementations(stable_implementation=stable_implementation, crypto_implementation=crypto_implementation) diff --git a/contracts/price_oracles/lp-oracles/lp_oracle_lib.vy b/contracts/price_oracles/lp-oracles/lp_oracle_lib.vy index 6dd0c140..34c514de 100644 --- a/contracts/price_oracles/lp-oracles/lp_oracle_lib.vy +++ b/contracts/price_oracles/lp-oracles/lp_oracle_lib.vy @@ -1,4 +1,6 @@ -# pragma version 0.4.1 +# @version 0.4.1 +#pragma optimize gas +#pragma evm-version shanghai interface PriceOracle: def price() -> uint256: view @@ -24,4 +26,4 @@ def _coin0_oracle_price_w() -> uint256: if COIN0_ORACLE.address != empty(address): return extcall COIN0_ORACLE.price_w() else: - return 10**18 \ No newline at end of file + return 10**18 diff --git a/tests_forked/price_oracles/conftest.py b/tests_forked/price_oracles/conftest.py index d3526ba6..8fc3826f 100644 --- a/tests_forked/price_oracles/conftest.py +++ b/tests_forked/price_oracles/conftest.py @@ -39,12 +39,18 @@ def crypto_impl(admin): @pytest.fixture(scope="module") def proxy_impl(admin): with boa.env.prank(admin): - return boa.load_partial('contracts/price_oracles/OracleProxy.vy').deploy_as_blueprint() + return boa.load('contracts/price_oracles/proxy/ProxyOracle.vy') @pytest.fixture(scope="module") -def lp_oracle_factory(admin, stable_impl, crypto_impl, proxy_impl): +def proxy_factory(admin, proxy_impl): with boa.env.prank(admin): - factory = boa.load("contracts/price_oracles/lp-oracles/LPOracleFactory.vy", admin) - factory.set_implementations(stable_impl, crypto_impl, proxy_impl) + 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, proxy_factory) + factory.set_implementations(stable_impl, crypto_impl) return factory 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 index b224f200..c574ebcc 100644 --- a/tests_forked/price_oracles/test_lp_oracle_compare_to_spot.py +++ b/tests_forked/price_oracles/test_lp_oracle_compare_to_spot.py @@ -12,10 +12,10 @@ def test_tricrypto_usdc(lp_oracle_factory, stablecoin_aggregator, admin, trader) 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/OracleProxy.vy').at( + 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)) # USDC/LP * crvUSD/USDC tricrypto_usdc_crvusd_lp_oracle.set_max_deviation(500) - tricrypto_usdc_usd_lp_oracle = boa.load_partial('contracts/price_oracles/OracleProxy.vy').at( + 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)) # USDC/LP * crvUSD/USDC * USD/crvUSD # --- Compare oracle and spot prices --- @@ -67,9 +67,9 @@ def test_tricrypto_usdt(lp_oracle_factory, stablecoin_aggregator, admin, trader) 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/OracleProxy.vy').at( + 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)) # USDT/LP * crvUSD/USDT - tricrypto_usdt_usd_lp_oracle = boa.load_partial('contracts/price_oracles/OracleProxy.vy').at( + 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)) # USDT/LP * crvUSD/USDT * USD/crvUSD # --- Compare oracle and spot prices --- @@ -116,9 +116,9 @@ def test_tricrv(lp_oracle_factory, stablecoin_aggregator, admin, trader): 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/OracleProxy.vy').at( + tricrv_crvusd_lp_oracle = boa.load_partial('contracts/price_oracles/proxy/ProxyOracle.vy').at( lp_oracle_factory.deploy_oracle(tricrv_pool_address, "0x0000000000000000000000000000000000000000")) # USDT/LP * crvUSD/USDT - tricrv_usd_lp_oracle = boa.load_partial('contracts/price_oracles/OracleProxy.vy').at( + 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)) # USDT/LP * crvUSD/USDT * USD/crvUSD # --- Compare oracle and spot prices --- @@ -169,9 +169,9 @@ def test_strategic_reserve(lp_oracle_factory, stablecoin_aggregator, admin, trad 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/OracleProxy.vy').at( + 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)) # USDC/LP * crvUSD/USDC - strategic_reserve_usd_lp_oracle = boa.load_partial('contracts/price_oracles/OracleProxy.vy').at( + 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)) # USDC/LP * crvUSD/USDC * USD/crvUSD # --- Compare oracle and spot prices --- @@ -226,9 +226,9 @@ def test_weeth_weth(lp_oracle_factory, stablecoin_aggregator, admin, trader): 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/OracleProxy.vy').at( + 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)) # ETH/LP * crvUSD/ETH - weeth_ng_pool_usd_lp_oracle = boa.load_partial('contracts/price_oracles/OracleProxy.vy').at( + 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)) # ETH/LP * crvUSD/ETH * USD/crvUSD # --- Compare oracle and spot prices --- @@ -286,9 +286,9 @@ def test_cvxcrv(lp_oracle_factory, stablecoin_aggregator, admin, trader): 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/OracleProxy.vy').at( + 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)) # CRV/LP * crvUSD/CRV - cvxcrv_pool_usd_lp_oracle = boa.load_partial('contracts/price_oracles/OracleProxy.vy').at( + 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)) # CRV/LP * crvUSD/CRV * USD/crvUSD # --- Compare oracle and spot prices --- From 5ccb0407467b728bf5084b3793bad7fd01b46bc7 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 7 Jul 2025 18:24:55 +0400 Subject: [PATCH 022/413] refactor: immutable implementation --- .../lp-oracles/LPOracleFactory.vy | 54 ++++++------------- tests_forked/price_oracles/conftest.py | 3 +- 2 files changed, 17 insertions(+), 40 deletions(-) diff --git a/contracts/price_oracles/lp-oracles/LPOracleFactory.vy b/contracts/price_oracles/lp-oracles/LPOracleFactory.vy index 06129440..deb18e5d 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleFactory.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleFactory.vy @@ -16,7 +16,6 @@ event DeployOracle: oracle: indexed(address) pool: indexed(address) coin0_oracle: address - implementation: address event SetImplementations: stable_implementation: address @@ -26,23 +25,22 @@ event SetImplementations: struct OracleInfo: pool: address coin0_oracle: address - implementation: address MAX_ORACLES: constant(uint256) = 50000 n_oracles: public(uint256) oracles: public(address[MAX_ORACLES]) -oracle_map: HashMap[address, HashMap[address, HashMap[address, address]]] # oracle_map[pool][coin0_oracle][implementation] -> oracle +oracle_map: HashMap[address, HashMap[address, address]] # oracle_map[pool][coin0_oracle] -> oracle oracle_info: HashMap[address, OracleInfo] # oracle_info[oracle] -> OracleInfo -stable_implementation: public(address) -crypto_implementation: public(address) +STABLE_IMPLEMENTATION: public(immutable(address)) +CRYPTO_IMPLEMENTATION: public(immutable(address)) PROXY_ORACLE_FACTORY: public(immutable(IProxyOracleFactory)) @deploy -def __init__(admin: address, _proxy_oracle_factory: IProxyOracleFactory): +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) @@ -50,7 +48,11 @@ def __init__(admin: address, _proxy_oracle_factory: IProxyOracleFactory): 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 @@ -63,12 +65,11 @@ def deploy_oracle(pool: address, coin0_oracle: address, use_proxy: bool = True, @param coin0_oracle Oracle for the first coin of the pool @return Deployed oracle address """ - implementation: address = self.stable_implementation + implementation: address = STABLE_IMPLEMENTATION if self._is_crypto(pool): - implementation = self.crypto_implementation + implementation = CRYPTO_IMPLEMENTATION - assert implementation != empty(address), "Oracle implementation is not set" - assert self.oracle_map[pool][coin0_oracle][implementation] == empty(address), "Oracle already exists" + assert self.oracle_map[pool][coin0_oracle] == empty(address), "Oracle already exists" oracle: address = create_from_blueprint(implementation, pool, coin0_oracle, code_offset=3) if use_proxy: @@ -78,24 +79,18 @@ def deploy_oracle(pool: address, coin0_oracle: address, use_proxy: bool = True, N: uint256 = self.n_oracles self.oracles[N] = oracle self.n_oracles = N + 1 - self.oracle_map[pool][coin0_oracle][implementation] = oracle - self.oracle_info[oracle] = OracleInfo(pool=pool, coin0_oracle=coin0_oracle, implementation=implementation) + 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, implementation=implementation) + log DeployOracle(oracle=oracle, pool=pool, coin0_oracle=coin0_oracle) return oracle @external @view -def get_oracle(pool: address, coin0_oracle: address, implementation: address = empty(address)) -> address: - _implementation: address = implementation - if _implementation == empty(address): - _implementation = self.stable_implementation - if self._is_crypto(pool): - _implementation = self.crypto_implementation - - return self.oracle_map[pool][coin0_oracle][_implementation] +def get_oracle(pool: address, coin0_oracle: address) -> address: + return self.oracle_map[pool][coin0_oracle] @external @@ -114,20 +109,3 @@ def _is_crypto(pool: address) -> bool: return True return False - - -@external -@nonreentrant -def set_implementations(stable_implementation: address, crypto_implementation: address): - """ - @notice Set new implementations (blueprints) for stable and crypto oracles. Doesn't change existing ones - @param stable_implementation Address of the StablePool LP Oracle blueprint - @param crypto_implementation Address of the CryptoPool LP Oracle blueprint - """ - ownable._check_owner() - assert stable_implementation != empty(address) - assert crypto_implementation != empty(address) - self.stable_implementation = stable_implementation - self.crypto_implementation = crypto_implementation - - log SetImplementations(stable_implementation=stable_implementation, crypto_implementation=crypto_implementation) diff --git a/tests_forked/price_oracles/conftest.py b/tests_forked/price_oracles/conftest.py index 8fc3826f..ef99e75c 100644 --- a/tests_forked/price_oracles/conftest.py +++ b/tests_forked/price_oracles/conftest.py @@ -51,6 +51,5 @@ def proxy_factory(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, proxy_factory) - factory.set_implementations(stable_impl, crypto_impl) + factory = boa.load("contracts/price_oracles/lp-oracles/LPOracleFactory.vy", admin, stable_impl, crypto_impl, proxy_factory) return factory From f64be27a99ba2b1a25796ebe7f5ff123e3cca47d Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 7 Jul 2025 19:28:49 +0400 Subject: [PATCH 023/413] refactor: cleaning up --- .../lp-oracles/LPOracleCrypto.vy | 18 ++--- .../lp-oracles/LPOracleFactory.vy | 72 +++++++++---------- .../lp-oracles/LPOracleStable.vy | 2 +- .../price_oracles/lp-oracles/lp_oracle_lib.vy | 6 +- .../price_oracles/proxy/ProxyOracleFactory.vy | 4 +- .../test_lp_oracle_compare_to_spot.py | 25 ++++--- 6 files changed, 63 insertions(+), 64 deletions(-) diff --git a/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy b/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy index b210e484..2bbfd30d 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy @@ -5,11 +5,12 @@ """ @title LPOracleCrypto @author Curve.Fi -@license GNU Affero General Public License v3.0 only +@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. """ + import lp_oracle_lib initializes: lp_oracle_lib exports: lp_oracle_lib.COIN0_ORACLE @@ -21,14 +22,15 @@ interface CryptoPool: POOL: public(immutable(CryptoPool)) + @deploy -def __init__(pool: CryptoPool, coin0_oracle: lp_oracle_lib.PriceOracle): - assert staticcall pool.lp_price() > 0 - if coin0_oracle.address != empty(address): - assert staticcall coin0_oracle.price() > 0 - assert extcall coin0_oracle.price_w() > 0 - POOL = pool - lp_oracle_lib.__init__(coin0_oracle) +def __init__(_pool: CryptoPool, _coin0_oracle: lp_oracle_lib.PriceOracle): + assert staticcall _pool.lp_price() > 0 + if _coin0_oracle.address != empty(address): + assert staticcall _coin0_oracle.price() > 0 + assert extcall _coin0_oracle.price_w() > 0 + POOL = _pool + lp_oracle_lib.__init__(_coin0_oracle) @internal diff --git a/contracts/price_oracles/lp-oracles/LPOracleFactory.vy b/contracts/price_oracles/lp-oracles/LPOracleFactory.vy index deb18e5d..84f74d75 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleFactory.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleFactory.vy @@ -2,6 +2,13 @@ #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 @@ -17,22 +24,14 @@ event DeployOracle: pool: indexed(address) coin0_oracle: address -event SetImplementations: - stable_implementation: address - crypto_implementation: address - struct OracleInfo: pool: address coin0_oracle: address -MAX_ORACLES: constant(uint256) = 50000 -n_oracles: public(uint256) -oracles: public(address[MAX_ORACLES]) - oracle_map: HashMap[address, HashMap[address, address]] # oracle_map[pool][coin0_oracle] -> oracle -oracle_info: HashMap[address, OracleInfo] # oracle_info[oracle] -> OracleInfo +oracle_info: public(HashMap[address, OracleInfo]) # oracle_info[oracle] -> OracleInfo STABLE_IMPLEMENTATION: public(immutable(address)) CRYPTO_IMPLEMENTATION: public(immutable(address)) @@ -40,13 +39,13 @@ PROXY_ORACLE_FACTORY: public(immutable(IProxyOracleFactory)) @deploy -def __init__(admin: address, _stable_implementation: address, _crypto_implementation: address, _proxy_oracle_factory: IProxyOracleFactory): +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) + ownable._transfer_ownership(_admin) assert _stable_implementation != empty(address) assert _crypto_implementation != empty(address) @@ -58,45 +57,40 @@ def __init__(admin: address, _stable_implementation: address, _crypto_implementa @external @nonreentrant -def deploy_oracle(pool: address, coin0_oracle: address, use_proxy: bool = True, save_to_storage: bool = True) -> address: +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 - @return Deployed oracle address + @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): + 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) - assert self.oracle_map[pool][coin0_oracle] == empty(address), "Oracle already exists" - - oracle: address = create_from_blueprint(implementation, pool, coin0_oracle, code_offset=3) - if use_proxy: - oracle = extcall PROXY_ORACLE_FACTORY.deploy_proxy_oracle(oracle) - - if save_to_storage: - N: uint256 = self.n_oracles - self.oracles[N] = oracle - self.n_oracles = N + 1 - 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) + log DeployOracle(oracle=oracle, pool=_pool, coin0_oracle=_coin0_oracle) - return oracle + return [oracle, proxy] @external @view -def get_oracle(pool: address, coin0_oracle: address) -> address: - return self.oracle_map[pool][coin0_oracle] - - -@external -@view -def get_oracle_info(oracle: address) -> OracleInfo: - return self.oracle_info[oracle] +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 diff --git a/contracts/price_oracles/lp-oracles/LPOracleStable.vy b/contracts/price_oracles/lp-oracles/LPOracleStable.vy index 9d8ee5ee..7f66415a 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleStable.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleStable.vy @@ -5,7 +5,7 @@ """ @title LPOracleStable @author Curve.Fi -@license GNU Affero General Public License v3.0 only +@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. """ diff --git a/contracts/price_oracles/lp-oracles/lp_oracle_lib.vy b/contracts/price_oracles/lp-oracles/lp_oracle_lib.vy index 34c514de..4e281f57 100644 --- a/contracts/price_oracles/lp-oracles/lp_oracle_lib.vy +++ b/contracts/price_oracles/lp-oracles/lp_oracle_lib.vy @@ -2,15 +2,19 @@ #pragma optimize gas #pragma evm-version shanghai + interface PriceOracle: def price() -> uint256: view def price_w() -> uint256: nonpayable + +COIN0_ORACLE: public(immutable(PriceOracle)) + + @deploy def __init__(coin0_oracle: PriceOracle): COIN0_ORACLE = coin0_oracle -COIN0_ORACLE: public(immutable(PriceOracle)) @internal @view diff --git a/contracts/price_oracles/proxy/ProxyOracleFactory.vy b/contracts/price_oracles/proxy/ProxyOracleFactory.vy index 85dd651a..c72561c9 100644 --- a/contracts/price_oracles/proxy/ProxyOracleFactory.vy +++ b/contracts/price_oracles/proxy/ProxyOracleFactory.vy @@ -5,7 +5,7 @@ """ @title ProxyOracleFactory @author Curve -@license MIT +@license GNU Affero General Public License v3.0 only @notice Proxy oracle factory deploying proxy oracles and replacing price oracle contracts after the deployment """ @@ -26,7 +26,7 @@ event SetOracle: PROXY_ORACLE_IMPLEMENTATION: public(immutable(address)) -get_proxy: public(HashMap[address, address]) +get_proxy: public(HashMap[address, address]) # get_proxy[oracle] -> proxy @deploy 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 index c574ebcc..07476a38 100644 --- a/tests_forked/price_oracles/test_lp_oracle_compare_to_spot.py +++ b/tests_forked/price_oracles/test_lp_oracle_compare_to_spot.py @@ -13,10 +13,9 @@ def test_tricrypto_usdc(lp_oracle_factory, stablecoin_aggregator, admin, trader) 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)) # USDC/LP * crvUSD/USDC - tricrypto_usdc_crvusd_lp_oracle.set_max_deviation(500) + 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)) # USDC/LP * crvUSD/USDC * USD/crvUSD + 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 --- @@ -68,9 +67,9 @@ def test_tricrypto_usdt(lp_oracle_factory, stablecoin_aggregator, admin, trader) 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)) # USDT/LP * crvUSD/USDT + 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)) # USDT/LP * crvUSD/USDT * USD/crvUSD + 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 --- @@ -117,9 +116,9 @@ def test_tricrv(lp_oracle_factory, stablecoin_aggregator, admin, trader): 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")) # USDT/LP * crvUSD/USDT + 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)) # USDT/LP * crvUSD/USDT * USD/crvUSD + lp_oracle_factory.deploy_oracle(tricrv_pool_address, stablecoin_aggregator.address)[1]) # USDT/LP * crvUSD/USDT * USD/crvUSD # --- Compare oracle and spot prices --- @@ -170,9 +169,9 @@ def test_strategic_reserve(lp_oracle_factory, stablecoin_aggregator, admin, trad 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)) # USDC/LP * crvUSD/USDC + 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)) # USDC/LP * crvUSD/USDC * USD/crvUSD + 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 --- @@ -227,9 +226,9 @@ def test_weeth_weth(lp_oracle_factory, stablecoin_aggregator, admin, trader): [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)) # ETH/LP * crvUSD/ETH + 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)) # ETH/LP * crvUSD/ETH * USD/crvUSD + 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 --- @@ -287,9 +286,9 @@ def test_cvxcrv(lp_oracle_factory, stablecoin_aggregator, admin, trader): 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)) # CRV/LP * crvUSD/CRV + 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)) # CRV/LP * crvUSD/CRV * USD/crvUSD + lp_oracle_factory.deploy_oracle(cvxcrv_pool_address, crv_usd_oracle)[1]) # CRV/LP * crvUSD/CRV * USD/crvUSD # --- Compare oracle and spot prices --- From e146dfb100a9ff2a096172d0d8c141a48944198a Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 8 Jul 2025 11:29:27 +0400 Subject: [PATCH 024/413] chore: rename set_new_oracle -> replace_oracle --- contracts/price_oracles/proxy/ProxyOracleFactory.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/price_oracles/proxy/ProxyOracleFactory.vy b/contracts/price_oracles/proxy/ProxyOracleFactory.vy index c72561c9..fce00712 100644 --- a/contracts/price_oracles/proxy/ProxyOracleFactory.vy +++ b/contracts/price_oracles/proxy/ProxyOracleFactory.vy @@ -62,7 +62,7 @@ def deploy_proxy_oracle(_oracle: address) -> address: @external @nonreentrant -def set_new_oracle(_proxy: IProxyOracle, _new_oracle: address, _skip_price_deviation_check: bool = False): +def replace_oracle(_proxy: IProxyOracle, _new_oracle: address, _skip_price_deviation_check: bool = False): ownable._check_owner() extcall _proxy.set_price_oracle(_new_oracle, _skip_price_deviation_check) From 1627f887ac07f45810468177472eb611befa58e2 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 8 Jul 2025 11:33:31 +0400 Subject: [PATCH 025/413] fix: update get_proxy --- contracts/price_oracles/proxy/ProxyOracleFactory.vy | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/price_oracles/proxy/ProxyOracleFactory.vy b/contracts/price_oracles/proxy/ProxyOracleFactory.vy index fce00712..af7d9c9b 100644 --- a/contracts/price_oracles/proxy/ProxyOracleFactory.vy +++ b/contracts/price_oracles/proxy/ProxyOracleFactory.vy @@ -18,6 +18,7 @@ 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: @@ -64,6 +65,9 @@ def deploy_proxy_oracle(_oracle: address) -> address: @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) From 2656a64c89fa42d0e393af4686353a16c53513fc Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 8 Jul 2025 11:41:48 +0400 Subject: [PATCH 026/413] test: proxy oracle --- tests/price_oracles/__init__.py | 0 tests/price_oracles/proxy/__init__.py | 0 tests/price_oracles/proxy/conftest.py | 36 +++++++++++++ tests/price_oracles/proxy/test_proxy.py | 69 +++++++++++++++++++++++++ 4 files changed, 105 insertions(+) create mode 100644 tests/price_oracles/__init__.py create mode 100644 tests/price_oracles/proxy/__init__.py create mode 100644 tests/price_oracles/proxy/conftest.py create mode 100644 tests/price_oracles/proxy/test_proxy.py diff --git a/tests/price_oracles/__init__.py b/tests/price_oracles/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/price_oracles/proxy/__init__.py b/tests/price_oracles/proxy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/price_oracles/proxy/conftest.py b/tests/price_oracles/proxy/conftest.py new file mode 100644 index 00000000..2d22e38f --- /dev/null +++ b/tests/price_oracles/proxy/conftest.py @@ -0,0 +1,36 @@ +import boa +import pytest + + +@pytest.fixture(scope="session") +def user(accounts): + return accounts[0] + + +@pytest.fixture(scope="session") +def get_price_oracle(admin): + def f(price): + with boa.env.prank(admin): + oracle = boa.load('contracts/testing/DummyPriceOracle.vy', admin, price) + return oracle + + return f + + +@pytest.fixture(scope="session") +def broken_price_oracle(admin): + with boa.env.prank(admin): + oracle = boa.load('contracts/testing/WETH.vy') + return oracle + + +@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) diff --git a/tests/price_oracles/proxy/test_proxy.py b/tests/price_oracles/proxy/test_proxy.py new file mode 100644 index 00000000..4190cdfa --- /dev/null +++ b/tests/price_oracles/proxy/test_proxy.py @@ -0,0 +1,69 @@ +import boa + +ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" + + +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 = boa.load_partial('contracts/price_oracles/proxy/ProxyOracle.vy').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() From 8e7ff3e82504cf359364065b0c7f517d1ab39936 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 8 Jul 2025 13:07:54 +0400 Subject: [PATCH 027/413] test: add boa.coverage --- .coveragerc | 2 ++ .gitignore | 1 + 2 files changed, 3 insertions(+) create mode 100644 .coveragerc 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/.gitignore b/.gitignore index 99316a81..eb5b62d8 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ venv /.py312 /scripts/networks.py /brownie-deploy-emas/build +.coverage From ec7925e5b86526245ac64ba70d60b96ae1cda462 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 8 Jul 2025 13:08:39 +0400 Subject: [PATCH 028/413] fix: min coins check --- contracts/price_oracles/lp-oracles/LPOracleStable.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/price_oracles/lp-oracles/LPOracleStable.vy b/contracts/price_oracles/lp-oracles/LPOracleStable.vy index 7f66415a..e863368e 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleStable.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleStable.vy @@ -42,7 +42,7 @@ def __init__(pool: StablePool, coin0_oracle: lp_oracle_lib.PriceOracle): 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)" + assert i > 1, "Less than 2 coins" N_COINS = i break From 9c6914c7c50e73a582382f73da8ae7a0651c4227 Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 9 Jul 2025 11:56:50 +0400 Subject: [PATCH 029/413] fix: check pool.price_oracle(i) > 0 --- .../lp-oracles/LPOracleCrypto.vy | 7 +++--- .../lp-oracles/LPOracleStable.vy | 24 ++++++++++--------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy b/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy index 2bbfd30d..fbc60981 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy @@ -25,10 +25,11 @@ POOL: public(immutable(CryptoPool)) @deploy def __init__(_pool: CryptoPool, _coin0_oracle: lp_oracle_lib.PriceOracle): - assert staticcall _pool.lp_price() > 0 + print("init") + assert staticcall _pool.lp_price() > 0, "pool.lp_price() returns 0" if _coin0_oracle.address != empty(address): - assert staticcall _coin0_oracle.price() > 0 - assert extcall _coin0_oracle.price_w() > 0 + 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) diff --git a/contracts/price_oracles/lp-oracles/LPOracleStable.vy b/contracts/price_oracles/lp-oracles/LPOracleStable.vy index e863368e..13e1d86e 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleStable.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleStable.vy @@ -28,17 +28,17 @@ N_COINS: public(immutable(uint256)) @deploy -def __init__(pool: StablePool, coin0_oracle: lp_oracle_lib.PriceOracle): +def __init__(_pool: StablePool, _coin0_oracle: lp_oracle_lib.PriceOracle): no_argument: bool = False # Init variables for raw calls - res: Bytes[1024] = empty(Bytes[1024]) + res: Bytes[32] = empty(Bytes[32]) success: bool = False # Find N_COINS and store PRECISIONS for i: uint256 in range(MAX_COINS + 1): success, res = raw_call( - pool.address, + _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: @@ -49,20 +49,22 @@ def __init__(pool: StablePool, coin0_oracle: lp_oracle_lib.PriceOracle): # Check and record if pool requires coin id in argument or no if N_COINS == 2: success, res = raw_call( - pool.address, + _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: - assert staticcall pool.price_oracle() > 0, "No price_oracle method" + if success: + assert convert(res, uint256) > 0, "pool.price_oracle(i) returns 0" + else: + 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 - assert extcall coin0_oracle.price_w() > 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 + POOL = _pool NO_ARGUMENT = no_argument - lp_oracle_lib.__init__(coin0_oracle) + lp_oracle_lib.__init__(_coin0_oracle) @internal From 31f261a6ed6e9ba534d06a2c8de73523f0791a45 Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 9 Jul 2025 11:57:46 +0400 Subject: [PATCH 030/413] test: lp-oracles test draft --- .../lp-oracles/testing/MockCryptoSwap.vy | 21 ++++++ .../lp-oracles/testing/MockStableSwap.vy | 35 +++++++++ .../testing/MockStableSwapNoArgument.vy | 25 +++++++ tests/price_oracles/lp-oracles/__init__.py | 0 tests/price_oracles/lp-oracles/conftest.py | 73 +++++++++++++++++++ .../lp-oracles/test_lp_oracle.py | 22 ++++++ 6 files changed, 176 insertions(+) create mode 100644 contracts/price_oracles/lp-oracles/testing/MockCryptoSwap.vy create mode 100644 contracts/price_oracles/lp-oracles/testing/MockStableSwap.vy create mode 100644 contracts/price_oracles/lp-oracles/testing/MockStableSwapNoArgument.vy create mode 100644 tests/price_oracles/lp-oracles/__init__.py create mode 100644 tests/price_oracles/lp-oracles/conftest.py create mode 100644 tests/price_oracles/lp-oracles/test_lp_oracle.py 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..136c341e --- /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: 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..c290d9e3 --- /dev/null +++ b/contracts/price_oracles/lp-oracles/testing/MockStableSwap.vy @@ -0,0 +1,35 @@ +# @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])) +prices: DynArray[uint256, MAX_COINS - 1] + + +@deploy +def __init__(_admin: address, _prices: DynArray[uint256, MAX_COINS - 1]): + ADMIN = _admin + self.prices = _prices + _coins: DynArray[address, MAX_COINS] = [] + for i: uint256 in range(len(_prices) + 1, bound=MAX_COINS): + _coins.append(empty(address)) + + coins = _coins + + +@external +@view +def price_oracle(_i: uint256) -> uint256: + return self.prices[_i] + + +@external +def set_price(_i: uint256, _price: uint256): + assert msg.sender == ADMIN + self.prices[_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..b7ae8e28 --- /dev/null +++ b/contracts/price_oracles/lp-oracles/testing/MockStableSwapNoArgument.vy @@ -0,0 +1,25 @@ +# @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: uint256 + + +@deploy +def __init__(_admin: address, _price: uint256): + ADMIN = _admin + self.price_oracle = _price + coins = [empty(address), empty(address)] + + +@external +def set_price(_price: uint256): + assert msg.sender == ADMIN + self.price_oracle = _price diff --git a/tests/price_oracles/lp-oracles/__init__.py b/tests/price_oracles/lp-oracles/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/price_oracles/lp-oracles/conftest.py b/tests/price_oracles/lp-oracles/conftest.py new file mode 100644 index 00000000..64efcd9f --- /dev/null +++ b/tests/price_oracles/lp-oracles/conftest.py @@ -0,0 +1,73 @@ +import boa +import pytest + +ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" + + +@pytest.fixture(scope="session") +def user(accounts): + return accounts[0] + + +@pytest.fixture(scope="module") +def broken_contract(admin): + with boa.env.prank(admin): + return boa.load('contracts/testing/WETH.vy') + + +@pytest.fixture(scope="module") +def coin0_oracle(admin): + with boa.env.prank(admin): + return boa.load('contracts/testing/DummyPriceOracle.vy', admin, 10**18) + + +@pytest.fixture(scope="module") +def get_stable_swap(admin): + def f(N): + prices = [10**18] * (N - 1) + with boa.env.prank(admin): + return boa.load('contracts/price_oracles/lp-oracles/testing/MockStableSwap.vy', admin, prices) + + return f + + +@pytest.fixture(scope="module") +def crypto_swap(admin): + with boa.env.prank(admin): + return boa.load('contracts/price_oracles/lp-oracles/testing/MockCryptoSwap.vy', admin, 10**18) + + +@pytest.fixture(scope="module") +def stable_swap_no_argument(admin): + with boa.env.prank(admin): + return boa.load('contracts/price_oracles/lp-oracles/testing/MockStableSwapNoArgument.vy', admin, 10**18) + + +@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 stable_oracle_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_oracle_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 lp_oracle_factory(admin, stable_oracle_impl, crypto_oracle_impl, proxy_factory): + with boa.env.prank(admin): + return boa.load('contracts/price_oracles/lp-oracles/LPOracleFactory.vy', admin, stable_oracle_impl, crypto_oracle_impl, proxy_factory) 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..1e345ad7 --- /dev/null +++ b/tests/price_oracles/lp-oracles/test_lp_oracle.py @@ -0,0 +1,22 @@ +import boa + +ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" + + +def test_lp_oracle_crypto(lp_oracle_factory, crypto_swap, coin0_oracle, broken_contract, admin): + lp_oracle_factory.deploy_oracle(crypto_swap, coin0_oracle) + + 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("pool.lp_price() returns 0"): + lp_oracle_factory.deploy_oracle(crypto_swap, coin0_oracle) + + with boa.env.anchor(): + coin0_oracle.set_price(0, sender=admin) + with boa.reverts("coin0_oracle.price() returns 0"): + lp_oracle_factory.deploy_oracle(crypto_swap, coin0_oracle) From fc0f5b17251f9e370fedd8ddc06967e5a8eab575 Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 9 Jul 2025 16:37:50 +0400 Subject: [PATCH 031/413] chore: fix mock contracts --- .../lp-oracles/testing/MockCryptoSwap.vy | 2 +- .../lp-oracles/testing/MockStableSwap.vy | 14 +++++--------- .../lp-oracles/testing/MockStableSwapNoArgument.vy | 4 +++- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/contracts/price_oracles/lp-oracles/testing/MockCryptoSwap.vy b/contracts/price_oracles/lp-oracles/testing/MockCryptoSwap.vy index 136c341e..6dd76f06 100644 --- a/contracts/price_oracles/lp-oracles/testing/MockCryptoSwap.vy +++ b/contracts/price_oracles/lp-oracles/testing/MockCryptoSwap.vy @@ -5,7 +5,7 @@ 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: uint256 +lp_price: public(uint256) ADMIN: immutable(address) diff --git a/contracts/price_oracles/lp-oracles/testing/MockStableSwap.vy b/contracts/price_oracles/lp-oracles/testing/MockStableSwap.vy index c290d9e3..47948cc4 100644 --- a/contracts/price_oracles/lp-oracles/testing/MockStableSwap.vy +++ b/contracts/price_oracles/lp-oracles/testing/MockStableSwap.vy @@ -9,27 +9,23 @@ MAX_COINS: constant(uint256) = 8 ADMIN: immutable(address) coins: public(immutable(DynArray[address, MAX_COINS])) -prices: DynArray[uint256, MAX_COINS - 1] +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.prices = _prices + 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 - - -@external -@view -def price_oracle(_i: uint256) -> uint256: - return self.prices[_i] + self.get_virtual_price = 10**18 @external def set_price(_i: uint256, _price: uint256): assert msg.sender == ADMIN - self.prices[_i] = _price + 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 index b7ae8e28..2d9cf920 100644 --- a/contracts/price_oracles/lp-oracles/testing/MockStableSwapNoArgument.vy +++ b/contracts/price_oracles/lp-oracles/testing/MockStableSwapNoArgument.vy @@ -9,7 +9,8 @@ MAX_COINS: constant(uint256) = 2 ADMIN: immutable(address) coins: public(immutable(address[2])) -price_oracle: uint256 +price_oracle: public(uint256) +get_virtual_price: public(uint256) @deploy @@ -17,6 +18,7 @@ def __init__(_admin: address, _price: uint256): ADMIN = _admin self.price_oracle = _price coins = [empty(address), empty(address)] + self.get_virtual_price = 10**18 @external From 5ab29b7a714ac5ee92c03e30543672b3996eda85 Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 9 Jul 2025 16:38:59 +0400 Subject: [PATCH 032/413] fix: check price_oracle(i) for all i --- contracts/price_oracles/lp-oracles/LPOracleCrypto.vy | 1 - contracts/price_oracles/lp-oracles/LPOracleStable.vy | 11 ++++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy b/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy index fbc60981..dd285c93 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy @@ -25,7 +25,6 @@ POOL: public(immutable(CryptoPool)) @deploy def __init__(_pool: CryptoPool, _coin0_oracle: lp_oracle_lib.PriceOracle): - print("init") 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" diff --git a/contracts/price_oracles/lp-oracles/LPOracleStable.vy b/contracts/price_oracles/lp-oracles/LPOracleStable.vy index 13e1d86e..fb744e95 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleStable.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleStable.vy @@ -35,7 +35,7 @@ def __init__(_pool: StablePool, _coin0_oracle: lp_oracle_lib.PriceOracle): res: Bytes[32] = empty(Bytes[32]) success: bool = False - # Find N_COINS and store PRECISIONS + # Find N_COINS for i: uint256 in range(MAX_COINS + 1): success, res = raw_call( _pool.address, @@ -46,15 +46,16 @@ def __init__(_pool: StablePool, _coin0_oracle: lp_oracle_lib.PriceOracle): N_COINS = i break - # Check and record if pool requires coin id in argument or no - if N_COINS == 2: + # 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(empty(uint256), method_id=method_id("price_oracle(uint256)")), + 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" + 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 From b489844b10ff1f4c03a57c06e17c914fdd14a4fc Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 9 Jul 2025 16:40:09 +0400 Subject: [PATCH 033/413] test: lp-oracles --- tests/price_oracles/lp-oracles/conftest.py | 38 +++++-- .../lp-oracles/test_lp_oracle.py | 60 +++++++++-- .../lp-oracles/test_lp_oracle_factory.py | 100 ++++++++++++++++++ 3 files changed, 181 insertions(+), 17 deletions(-) create mode 100644 tests/price_oracles/lp-oracles/test_lp_oracle_factory.py diff --git a/tests/price_oracles/lp-oracles/conftest.py b/tests/price_oracles/lp-oracles/conftest.py index 64efcd9f..7e7947df 100644 --- a/tests/price_oracles/lp-oracles/conftest.py +++ b/tests/price_oracles/lp-oracles/conftest.py @@ -1,8 +1,6 @@ import boa import pytest - -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" - +import random @pytest.fixture(scope="session") def user(accounts): @@ -18,13 +16,13 @@ def broken_contract(admin): @pytest.fixture(scope="module") def coin0_oracle(admin): with boa.env.prank(admin): - return boa.load('contracts/testing/DummyPriceOracle.vy', admin, 10**18) + return boa.load('contracts/testing/DummyPriceOracle.vy', admin, random.randint(99 * 10**17, 101 * 10**17)) @pytest.fixture(scope="module") def get_stable_swap(admin): def f(N): - prices = [10**18] * (N - 1) + prices = [random.randint(10**16, 10**23) for i in range(N - 1)] with boa.env.prank(admin): return boa.load('contracts/price_oracles/lp-oracles/testing/MockStableSwap.vy', admin, prices) @@ -34,13 +32,13 @@ def f(N): @pytest.fixture(scope="module") def crypto_swap(admin): with boa.env.prank(admin): - return boa.load('contracts/price_oracles/lp-oracles/testing/MockCryptoSwap.vy', admin, 10**18) + return boa.load('contracts/price_oracles/lp-oracles/testing/MockCryptoSwap.vy', admin, random.randint(10**16, 10**23)) @pytest.fixture(scope="module") def stable_swap_no_argument(admin): with boa.env.prank(admin): - return boa.load('contracts/price_oracles/lp-oracles/testing/MockStableSwapNoArgument.vy', admin, 10**18) + return boa.load('contracts/price_oracles/lp-oracles/testing/MockStableSwapNoArgument.vy', admin, random.randint(10**16, 10**23)) @pytest.fixture(scope="module") @@ -56,18 +54,36 @@ def proxy_factory(admin, proxy_impl): @pytest.fixture(scope="module") -def stable_oracle_impl(admin): +def lp_oracle_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_oracle_impl(admin): +def lp_oracle_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 lp_oracle_factory(admin, stable_oracle_impl, crypto_oracle_impl, proxy_factory): +def lp_oracle_factory(admin, lp_oracle_stable_impl, lp_oracle_crypto_impl, proxy_factory): with boa.env.prank(admin): - return boa.load('contracts/price_oracles/lp-oracles/LPOracleFactory.vy', admin, stable_oracle_impl, crypto_oracle_impl, proxy_factory) + return boa.load('contracts/price_oracles/lp-oracles/LPOracleFactory.vy', 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 boa.load('contracts/price_oracles/lp-oracles/LPOracleStable.vy', 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 boa.load('contracts/price_oracles/lp-oracles/LPOracleCrypto.vy', 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 index 1e345ad7..af6b707e 100644 --- a/tests/price_oracles/lp-oracles/test_lp_oracle.py +++ b/tests/price_oracles/lp-oracles/test_lp_oracle.py @@ -3,20 +3,68 @@ ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" -def test_lp_oracle_crypto(lp_oracle_factory, crypto_swap, coin0_oracle, broken_contract, admin): - 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) + + +def test_lp_oracle_stable(get_lp_oracle_stable, get_stable_swap, stable_swap_no_argument, coin0_oracle, broken_contract, admin): with boa.reverts(): - lp_oracle_factory.deploy_oracle(broken_contract, coin0_oracle) + get_lp_oracle_stable(broken_contract, coin0_oracle) with boa.reverts(): - lp_oracle_factory.deploy_oracle(crypto_swap, broken_contract) + 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"): - lp_oracle_factory.deploy_oracle(crypto_swap, coin0_oracle) + 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"): - lp_oracle_factory.deploy_oracle(crypto_swap, coin0_oracle) + 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..7022241e --- /dev/null +++ b/tests/price_oracles/lp-oracles/test_lp_oracle_factory.py @@ -0,0 +1,100 @@ +import boa + +ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" + + +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 = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleStable.vy').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 = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleStable.vy').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 = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleCrypto.vy').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 = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleCrypto.vy').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) From 6209cb9fd29648e21941dd55e1b250f5a849d56c Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 23 Jul 2025 12:14:10 +0400 Subject: [PATCH 034/413] chore: separate contract for lending controller --- contracts/lending/Controller.vy | 1536 +++++++++++++++++++++++++++++++ 1 file changed, 1536 insertions(+) create mode 100644 contracts/lending/Controller.vy diff --git a/contracts/lending/Controller.vy b/contracts/lending/Controller.vy new file mode 100644 index 00000000..02aa1960 --- /dev/null +++ b/contracts/lending/Controller.vy @@ -0,0 +1,1536 @@ +# @version 0.3.10 +# pragma optimize codesize +# pragma evm-version shanghai +""" +@title crvUSD Controller +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved +""" + +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) +MAX_LOAN_DISCOUNT: constant(uint256) = 5 * 10**17 +MIN_LIQUIDATION_DISCOUNT: constant(uint256) = 10**16 # Start liquidating when threshold reached +MAX_TICKS: constant(int256) = 50 +MAX_TICKS_UINT: constant(uint256) = 50 +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 + +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 + +minted: public(uint256) +redeemed: public(uint256) + +monetary_policy: public(MonetaryPolicy) +liquidation_discount: public(uint256) +loan_discount: public(uint256) + +COLLATERAL_TOKEN: immutable(ERC20) +COLLATERAL_PRECISION: immutable(uint256) + +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) + +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 + +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) + +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 + +approval: public(HashMap[address, HashMap[address, bool]]) +extra_health: public(HashMap[address, uint256]) + + +@external +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) + + self.monetary_policy = MonetaryPolicy(monetary_policy) + + 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()) + + COLLATERAL_TOKEN = _collateral_token + BORROWED_TOKEN = _borrowed_token + COLLATERAL_PRECISION = pow_mod256(10, 18 - _collateral_token.decimals()) + BORROWED_PRECISION = pow_mod256(10, 18 - _borrowed_token.decimals()) + + SQRT_BAND_RATIO = isqrt(unsafe_div(10**36 * _A, unsafe_sub(_A, 1))) + + assert _borrowed_token.approve(msg.sender, max_value(uint256), default_return_value=True) + + +@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 + + +@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 + + +@external +@pure +def factory() -> Factory: + """ + @notice Address of the factory + """ + return FACTORY + + +@external +@pure +def amm() -> LLAMMA: + """ + @notice Address of the AMM + """ + return AMM + + +@external +@pure +def collateral_token() -> ERC20: + """ + @notice Address of the collateral token + """ + return COLLATERAL_TOKEN + + +@external +@pure +def borrowed_token() -> ERC20: + """ + @notice Address of the borrowed token + """ + return BORROWED_TOKEN + + +@internal +def _save_rate(): + """ + @notice Save current rate + """ + rate: uint256 = min(self.monetary_policy.rate_write(), MAX_RATE) + AMM.set_rate(rate) + + +@external +@nonreentrant('lock') +def save_rate(): + """ + @notice Save current rate + """ + self._save_rate() + + +@internal +@view +def _debt(user: address) -> (uint256, uint256): + """ + @notice Get the value of debt and rate_mul and update the rate_mul counter + @param user User address + @return (debt, rate_mul) + """ + rate_mul: uint256 = AMM.get_rate_mul() + loan: Loan = self.loan[user] + if loan.initial_debt == 0: + return (0, rate_mul) + else: + # Let user repay 1 smallest decimal more so that the system doesn't lose on precision + # Use ceil div + debt: uint256 = loan.initial_debt * rate_mul + 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 + return (debt, rate_mul) + + +@external +@view +@nonreentrant('lock') +def debt(user: address) -> uint256: + """ + @notice Get the value of debt without changing the state + @param user User address + @return Value of debt + """ + return self._debt(user)[0] + + +@external +@view +@nonreentrant('lock') +def loan_exists(user: address) -> bool: + """ + @notice Check whether there is a loan of `user` in existence + """ + return self.loan[user].initial_debt > 0 + + +# No decorator because used in monetary policy +@external +@view +def total_debt() -> uint256: + """ + @notice Total debt of this controller + """ + rate_mul: uint256 = AMM.get_rate_mul() + loan: Loan = self._total_debt + return loan.initial_debt * rate_mul / loan.rate_mul + + +@internal +@pure +def get_y_effective(collateral: uint256, N: uint256, discount: uint256) -> uint256: + """ + @notice Intermediary method which calculates y_effective defined as x_effective / p_base, + however discounted by loan_discount. + x_effective is an amount which can be obtained from collateral when liquidating + @param collateral Amount of collateral to get the value for + @param N Number of bands the deposit is made into + @param discount Loan discount at 1e18 base (e.g. 1e18 == 100%) + @return y_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_effective * p_oracle_up(n1) + # d_y_effective = y / N / sqrt(A / (A - 1)) + # 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) + ), + unsafe_mul(SQRT_BAND_RATIO, N)) + y_effective: uint256 = d_y_effective + for i in range(1, MAX_TICKS_UINT): + if i == N: + break + d_y_effective = unsafe_div(d_y_effective * Aminus1, 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: + """ + @notice Calculate the upper band number for the deposit to sit in to support + the given debt. 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 + @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) + + # 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]) + # p_oracle_up(n1) = base_price * ((A - 1) / A)**n1 + + # We borrow up until min band touches p_oracle, + # or it touches non-empty bands which cannot be skipped. + # We calculate required n1 for given (collateral, debt), + # and if n1 corresponds to price_oracle being too high, or unreachable band + # - 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 + + # 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) + if n1 < 0: + 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" + + # 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" + + 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: + """ + @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 + + +@external +@view +@nonreentrant('lock') +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 + @param user User to calculate the value for (only necessary for nonzero extra_health) + @return Minimal collateral required + """ + # 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) + + +@external +@view +@nonreentrant('lock') +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. + @param collateral Amount of collateral (at its native precision) + @param debt Amount of requested debt + @param N Number of bands to deposit into + @param user User to calculate n1 for (only necessary for nonzero extra_health) + @return Upper band n1 (n1 <= n2) to deposit into. Signed integer + """ + return self._calculate_debt_n1(collateral, debt, N, user) + + +@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) + + +@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: + 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) + + # Callback + response: Bytes[64] = raw_call( + callbacker, + concat(callback_sig, _abi_encode(user, stablecoins, collateral, debt, callback_args, callback_bytes)), + max_outsize=64 + ) + data.stablecoins = 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) + + 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) + + +@external +@nonreentrant('lock') +def create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address = msg.sender): + """ + @notice Create loan + @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 _for Address to create the loan for + """ + 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) + + +@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) + + # Before callback + self.transfer(BORROWED_TOKEN, callbacker, debt) + + # 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._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) + + +@internal +def _add_collateral_borrow(d_collateral: uint256, d_debt: uint256, _for: address, remove_collateral: bool, + check_rounding: bool): + """ + @notice Internal method to borrow and add or remove collateral + @param d_collateral Amount of collateral to add + @param d_debt Amount of debt increase + @param _for Address to transfer tokens to + @param remove_collateral Remove collateral instead of adding + @param check_rounding Check that amount added is no less than the rounding error on the loan + """ + debt: uint256 = 0 + rate_mul: uint256 = 0 + debt, rate_mul = self._debt(_for) + assert debt > 0, "Loan doesn't exist" + 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" + if remove_collateral: + xy[1] -= d_collateral + else: + xy[1] += d_collateral + if check_rounding: + # We need d(x + p*y) > 1 wei. For that, we do an equivalent check (but with x2 for safety) + # 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 + 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}) + + liquidation_discount: uint256 = 0 + if _for == msg.sender: + liquidation_discount = self.liquidation_discount + self.liquidation_discounts[_for] = liquidation_discount + 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) + else: + log Borrow(_for, d_collateral, d_debt) + + log UserState(_for, xy[1], debt, n1, n2, liquidation_discount) + + +@external +@nonreentrant('lock') +def add_collateral(collateral: uint256, _for: address = msg.sender): + """ + @notice Add extra collateral to avoid bad liqidations + @param collateral Amount of collateral to add + @param _for Address to add collateral for + """ + if collateral == 0: + return + self._add_collateral_borrow(collateral, 0, _for, False, _for != msg.sender) + self.transferFrom(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 + @param collateral Amount of collateral to remove + @param _for Address to remove collateral for + """ + if collateral == 0: + return + assert self._check_approval(_for) + self._add_collateral_borrow(collateral, 0, _for, True, False) + self.transferFrom(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) + @param collateral Amount of collateral to add + @param debt Amount of stablecoin 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 + """ + if debt == 0: + return + assert self._check_approval(_for) + + # Before callback + self.transfer(BORROWED_TOKEN, callbacker, debt) + + # 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) + self._save_rate() + + +@internal +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 + self.loan_ix[_for] = 0 + if loan_ix < last_loan_ix: # Need to replace + last_loan: address = self.loans[last_loan_ix] + self.loans[loan_ix] = last_loan + self.loan_ix[last_loan] = loan_ix + 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) + + 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) + + 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) + + 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 + + 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 + + self._save_rate() + + +@external +@nonreentrant('lock') +def repay_extended(callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b"", _for: address = msg.sender): + """ + @notice Repay loan but get a stablecoin for that from callback (to deleverage) + @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 + """ + 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 + 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 + + 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 + + # No need to check _health() because it's the _for + + # 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 + + self._save_rate() + + +@internal +@view +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 + @param user User address to calculate health for + @param debt The amount of debt to calculate health for + @param full Whether to take into account the price difference above the highest user's band + @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 + + 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) + 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) + + return health + + +@external +@view +@nonreentrant('lock') +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 + @param d_collateral Change in collateral amount (signed) + @param d_debt Change in debt amount (signed) + @param full Whether it's a 'full' health or not + @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 + + +@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) + + 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""): + """ + @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 + @param callbacker Address of the callback contract + @param callback_args Extra arguments for the callback (up to 5) such as min_amount etc + """ + 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" + + final_debt: uint256 = debt + debt = unsafe_div(debt * frac + (10**18 - 1), 10**18) + assert debt > 0 + final_debt = unsafe_sub(final_debt, debt) + + # Withdraw sender's stablecoin 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] + + # 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) + + 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]) + # Request what's left from user + self.transferFrom(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 + # 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) + + else: + # Withdraw collateral + self.transferFrom(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)) + + 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) + if final_debt == 0: + log UserState(user, 0, 0, 0, 0, 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: + """ + @notice Calculate the amount of stablecoins 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 + """ + 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) + + return unsafe_sub(max(debt, stablecoins), stablecoins) + + +@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]) + + +@view +@external +@nonreentrant('lock') +def users_to_liquidate(_from: uint256=0, _limit: uint256=0) -> DynArray[Position, 1000]: + """ + @notice Returns a dynamic array of users who can be "hard-liquidated". + This method is designed for convenience of liquidation bots. + @param _from Loan index to start iteration from + @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 +@view +@external +def amm_price() -> uint256: + """ + @notice Current price from the AMM + """ + return 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])] + + +@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) + """ + 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)] + + +# AMM has nonreentrant decorator +@external +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 + """ + assert msg.sender == FACTORY.admin() + assert fee <= MAX_FEE and fee >= MIN_FEE, "Fee" + AMM.set_fee(fee) + + +@nonreentrant('lock') +@external +def set_monetary_policy(monetary_policy: address): + """ + @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) + + +@nonreentrant('lock') +@external +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() + 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) + + +@external +@nonreentrant('lock') +def set_callback(cb: address): + """ + @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) + + +@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() + + # 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 + + self._save_rate() + + # 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 + + +@external +@view +@nonreentrant('lock') +def check_lock() -> bool: + return True + + +# Allowance methods + +@external +def approve(_spender: address, _allow: bool): + """ + @notice Allow another address to borrow and repay for the user + @param _spender Address to whitelist for the action + @param _allow Whether to turn the approval on or off (no amounts) + """ + self.approval[msg.sender][_spender] = _allow + log Approval(msg.sender, _spender, _allow) + + +@internal +@view +def _check_approval(_for: address) -> bool: + return msg.sender == _for or self.approval[_for][msg.sender] + + +@external +def set_extra_health(_value: uint256): + """ + @notice Add a little bit more to loan_discount to start SL with health higher than usual + @param _value 1e18-based addition to loan_discount + """ + self.extra_health[msg.sender] = _value + log SetExtraHealth(msg.sender, _value) From 4a4172250055808947dabf353d8a9c18aaa5fe96 Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 23 Jul 2025 12:15:37 +0400 Subject: [PATCH 035/413] feat: borrow caps --- contracts/lending/Controller.vy | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/contracts/lending/Controller.vy b/contracts/lending/Controller.vy index 02aa1960..7c0c30d7 100644 --- a/contracts/lending/Controller.vy +++ b/contracts/lending/Controller.vy @@ -2,7 +2,7 @@ # pragma optimize codesize # pragma evm-version shanghai """ -@title crvUSD Controller +@title LlamaLend Controller @author Curve.Fi @license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved """ @@ -106,6 +106,9 @@ event Approval: spender: indexed(address) allow: bool +event SetBorrowCap: + borrow_cap: uint256 + struct Loan: initial_debt: uint256 @@ -180,6 +183,7 @@ DEAD_SHARES: constant(uint256) = 1000 approval: public(HashMap[address, HashMap[address, bool]]) extra_health: public(HashMap[address, uint256]) +borrow_cap: public(uint256) @external @@ -701,7 +705,9 @@ def _create_loan(collateral: uint256, debt: uint256, N: uint256, transfer_coins: 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 + new_total_debt: uint256 = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul + debt + assert new_total_debt <= self.borrow_cap, "Borrow cap exceeded" + self._total_debt.initial_debt = new_total_debt self._total_debt.rate_mul = rate_mul AMM.deposit_range(_for, collateral, n1, n2) @@ -814,7 +820,9 @@ def _add_collateral_borrow(d_collateral: uint256, d_debt: uint256, _for: address 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 + new_total_debt: uint256 = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul + d_debt + assert new_total_debt <= self.borrow_cap, "Borrow cap exceeded" + self._total_debt.initial_debt = new_total_debt self._total_debt.rate_mul = rate_mul if remove_collateral: @@ -1179,6 +1187,7 @@ def _get_f_remove(frac: uint256, health_limit: uint256) -> uint256: 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""): @@ -1451,6 +1460,18 @@ def set_callback(cb: address): log SetLMCallback(cb) +@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 + """ + assert msg.sender == FACTORY.admin() + self.borrow_cap = _borrow_cap + log SetBorrowCap(_borrow_cap) + + @external @view def admin_fees() -> uint256: From 028379c688eba707253fe1ec4e69081dd096af79 Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 23 Jul 2025 12:34:33 +0400 Subject: [PATCH 036/413] refactor: rename FACTORY -> VAULT, clean init --- contracts/lending/Controller.vy | 47 +++++++++++---------------------- 1 file changed, 16 insertions(+), 31 deletions(-) diff --git a/contracts/lending/Controller.vy b/contracts/lending/Controller.vy index 7c0c30d7..f6c43241 100644 --- a/contracts/lending/Controller.vy +++ b/contracts/lending/Controller.vy @@ -44,12 +44,9 @@ interface ERC20: interface MonetaryPolicy: def rate_write() -> uint256: nonpayable -interface Factory: - def stablecoin() -> address: view +interface Vault: def admin() -> address: view def fee_receiver() -> address: view - - # Only if lending vault def borrowed_token() -> address: view def collateral_token() -> address: view @@ -127,7 +124,7 @@ struct CallbackData: collateral: uint256 -FACTORY: immutable(Factory) +VAULT: immutable(Vault) MAX_LOAN_DISCOUNT: constant(uint256) = 5 * 10**17 MIN_LIQUIDATION_DISCOUNT: constant(uint256) = 10**16 # Start liquidating when threshold reached MAX_TICKS: constant(int256) = 50 @@ -202,7 +199,7 @@ def __init__( get_x_down() for "bad liquidation" purposes @param amm AMM address (Already deployed from blueprint) """ - FACTORY = Factory(msg.sender) + VAULT = Vault(msg.sender) self.monetary_policy = MonetaryPolicy(monetary_policy) @@ -217,26 +214,14 @@ def __init__( 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()) - - COLLATERAL_TOKEN = _collateral_token - BORROWED_TOKEN = _borrowed_token - COLLATERAL_PRECISION = pow_mod256(10, 18 - _collateral_token.decimals()) - BORROWED_PRECISION = pow_mod256(10, 18 - _borrowed_token.decimals()) + COLLATERAL_TOKEN = ERC20(Vault(msg.sender).collateral_token()) + BORROWED_TOKEN = ERC20(Vault(msg.sender).borrowed_token()) + COLLATERAL_PRECISION = pow_mod256(10, 18 - COLLATERAL_TOKEN.decimals()) + BORROWED_PRECISION = pow_mod256(10, 18 - BORROWED_TOKEN.decimals()) SQRT_BAND_RATIO = isqrt(unsafe_div(10**36 * _A, unsafe_sub(_A, 1))) - assert _borrowed_token.approve(msg.sender, max_value(uint256), default_return_value=True) + assert BORROWED_TOKEN.approve(msg.sender, max_value(uint256), default_return_value=True) @internal @@ -355,11 +340,11 @@ def wad_ln(x: uint256) -> int256: @external @pure -def factory() -> Factory: +def factory() -> Vault: """ @notice Address of the factory """ - return FACTORY + return VAULT @external @@ -1414,7 +1399,7 @@ 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 """ - assert msg.sender == FACTORY.admin() + assert msg.sender == VAULT.admin() assert fee <= MAX_FEE and fee >= MIN_FEE, "Fee" AMM.set_fee(fee) @@ -1426,7 +1411,7 @@ def set_monetary_policy(monetary_policy: address): @notice Set monetary policy contract @param monetary_policy Address of the monetary policy contract """ - assert msg.sender == FACTORY.admin() + assert msg.sender == VAULT.admin() self.monetary_policy = MonetaryPolicy(monetary_policy) MonetaryPolicy(monetary_policy).rate_write() log SetMonetaryPolicy(monetary_policy) @@ -1440,7 +1425,7 @@ def set_borrowing_discounts(loan_discount: uint256, liquidation_discount: uint25 @param loan_discount Discount which defines LTV @param liquidation_discount Discount where bad liquidation starts """ - assert msg.sender == FACTORY.admin() + assert msg.sender == VAULT.admin() assert loan_discount > liquidation_discount assert liquidation_discount >= MIN_LIQUIDATION_DISCOUNT assert loan_discount <= MAX_LOAN_DISCOUNT @@ -1455,7 +1440,7 @@ def set_callback(cb: address): """ @notice Set liquidity mining callback """ - assert msg.sender == FACTORY.admin() + assert msg.sender == VAULT.admin() AMM.set_callback(cb) log SetLMCallback(cb) @@ -1467,7 +1452,7 @@ def set_borrow_cap(_borrow_cap: uint256): @dev Only callable by the factory admin @param _borrow_cap New borrow cap in units of borrowed_token """ - assert msg.sender == FACTORY.admin() + assert msg.sender == VAULT.admin() self.borrow_cap = _borrow_cap log SetBorrowCap(_borrow_cap) @@ -1494,7 +1479,7 @@ def collect_fees() -> uint256: 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() + _to: address = VAULT.fee_receiver() # Borrowing-based fees rate_mul: uint256 = AMM.get_rate_mul() From 846c058c5687870da293d3147c22c83744eac426 Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 23 Jul 2025 15:51:33 +0400 Subject: [PATCH 037/413] feat: admin fees for lending markets --- contracts/lending/Controller.vy | 73 +++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 31 deletions(-) diff --git a/contracts/lending/Controller.vy b/contracts/lending/Controller.vy index f6c43241..7d283577 100644 --- a/contracts/lending/Controller.vy +++ b/contracts/lending/Controller.vy @@ -144,8 +144,9 @@ 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 -minted: public(uint256) -redeemed: public(uint256) +repaid: public(uint256) # cumulative amount of assets ever repaid +processed: public(uint256) # cumulative amount of assets admin fees have been taken from +collected: public(uint256) # cumulative amount of assets collected by admin monetary_policy: public(MonetaryPolicy) liquidation_discount: public(uint256) @@ -163,9 +164,9 @@ Aminus1: immutable(uint256) LOGN_A_RATIO: immutable(int256) # log(A / (A - 1)) SQRT_BAND_RATIO: immutable(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 +MAX_ADMIN_FEE: constant(uint256) = 2 * 10**17 # 20% +MIN_AMM_FEE: constant(uint256) = 10**6 # 1e-12, still needs to be above 0 +MAX_AMM_FEE: immutable(uint256) # let's set to MIN_TICKS / A: for example, 4% max fee for A=100 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) @@ -181,6 +182,7 @@ DEAD_SHARES: constant(uint256) = 1000 approval: public(HashMap[address, HashMap[address, bool]]) extra_health: public(HashMap[address, uint256]) borrow_cap: public(uint256) +admin_fee: public(uint256) @external @@ -212,7 +214,7 @@ def __init__( 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) + MAX_AMM_FEE = min(unsafe_div(10**18 * MIN_TICKS, A), 10**17) COLLATERAL_TOKEN = ERC20(Vault(msg.sender).collateral_token()) BORROWED_TOKEN = ERC20(Vault(msg.sender).borrowed_token()) @@ -696,7 +698,7 @@ def _create_loan(collateral: uint256, debt: uint256, N: uint256, transfer_coins: self._total_debt.rate_mul = rate_mul AMM.deposit_range(_for, collateral, n1, n2) - self.minted += debt + self.processed += debt if transfer_coins: self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) @@ -862,7 +864,7 @@ def borrow_more(collateral: uint256, debt: uint256, _for: address = msg.sender): return assert self._check_approval(_for) self._add_collateral_borrow(collateral, debt, _for, False, False) - self.minted += debt + self.processed += debt self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) self.transfer(BORROWED_TOKEN, _for, debt) self._save_rate() @@ -897,7 +899,7 @@ def borrow_more_extended(collateral: uint256, debt: uint256, callbacker: address # After callback self._add_collateral_borrow(collateral + more_collateral, debt, _for, False, False) - self.minted += debt + self.processed += debt self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) self.transferFrom(COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral) self._save_rate() @@ -982,7 +984,7 @@ def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 # If we withdrew already - will burn less! self.transferFrom(BORROWED_TOKEN, msg.sender, self, d_debt) # fail: insufficient funds - self.redeemed += d_debt + self.repaid += 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 @@ -1067,7 +1069,7 @@ def repay_extended(callbacker: address, callback_args: DynArray[uint256,5], call # 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.repaid += 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) @@ -1243,7 +1245,7 @@ def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint2 if xy[0] > debt: self.transferFrom(BORROWED_TOKEN, AMM.address, msg.sender, unsafe_sub(xy[0], debt)) - self.redeemed += debt + self.repaid += 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) @@ -1397,10 +1399,10 @@ def user_state(user: address) -> uint256[4]: 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 + @param fee The fee which should be no higher than MAX_AMM_FEE """ assert msg.sender == VAULT.admin() - assert fee <= MAX_FEE and fee >= MIN_FEE, "Fee" + assert fee <= MAX_AMM_FEE and fee >= MIN_AMM_FEE, "Fee" AMM.set_fee(fee) @@ -1446,6 +1448,7 @@ def set_callback(cb: address): @external +@nonreentrant('lock') def set_borrow_cap(_borrow_cap: uint256): """ @notice Set the borrow cap for this market @@ -1457,6 +1460,16 @@ def set_borrow_cap(_borrow_cap: uint256): log SetBorrowCap(_borrow_cap) +@external +def set_admin_fee(admin_fee: uint256): + """ + @param admin_fee The fee which should be no higher than MAX_ADMIN_FEE + """ + assert msg.sender == VAULT.admin() + assert admin_fee <= MAX_ADMIN_FEE, "admin_fee is higher than MAX_ADMIN_FEE" + self.admin_fee = admin_fee + + @external @view def admin_fees() -> uint256: @@ -1465,20 +1478,17 @@ def admin_fees() -> uint256: """ 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) + loan.initial_debt = loan.initial_debt * rate_mul / loan.rate_mul + processed: uint256 = self.processed + return unsafe_sub(max(loan.initial_debt + self.repaid, processed), processed) @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 + @notice Collect the fees charged as a fraction of interest. """ - # Calling fee_receiver will fail for lending markets because everything gets to lenders _to: address = VAULT.fee_receiver() # Borrowing-based fees @@ -1490,17 +1500,18 @@ def collect_fees() -> uint256: self._save_rate() - # 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 + # Cumulative amount which would have been repaid if all the debt was repaid now + to_be_repaid: uint256 = loan.initial_debt + self.repaid + # Cumulative amount which was processed (admin fees have been taken from) + processed: uint256 = self.processed # 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 + if to_be_repaid > processed: + self.processed = to_be_repaid + fees: uint256 = unsafe_sub(to_be_repaid, processed) * self.admin_fee / 10**18 + self.collected += fees + self.transfer(BORROWED_TOKEN, _to, fees) + log CollectFees(fees, loan.initial_debt) + return fees else: log CollectFees(0, loan.initial_debt) return 0 From 53dece60179dd8cf028ee2b8cb83e8ea248d9ec5 Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 23 Jul 2025 17:58:45 +0400 Subject: [PATCH 038/413] feat: internal balances --- contracts/lending/Controller.vy | 36 ++++++++++++++++++++++++--------- contracts/lending/Vault.vy | 19 +++++++++++------ 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/contracts/lending/Controller.vy b/contracts/lending/Controller.vy index 7d283577..c4aef727 100644 --- a/contracts/lending/Controller.vy +++ b/contracts/lending/Controller.vy @@ -39,7 +39,6 @@ interface ERC20: 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 @@ -49,6 +48,8 @@ interface Vault: def fee_receiver() -> address: view def borrowed_token() -> address: view def collateral_token() -> address: view + def deposited() -> uint256: view + def withdrawn() -> uint256: view event UserState: @@ -144,6 +145,7 @@ 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 +lent: public(uint256) # cumulative amount of assets ever lent repaid: public(uint256) # cumulative amount of assets ever repaid processed: public(uint256) # cumulative amount of assets admin fees have been taken from collected: public(uint256) # cumulative amount of assets collected by admin @@ -560,6 +562,20 @@ def max_p_base() -> uint256: return p_base +@internal +@view +def _borrowed_balance() -> uint256: + # (VAULT.deposited() - VAULT.withdrawn()) - (self.lent - self.repaid) - self.collected + return VAULT.deposited() + self.repaid - VAULT.withdrawn() - self.lent - self.collected + + +@external +@view +@nonreentrant('lock') +def borrowed_balance() -> uint256: + return self._borrowed_balance() + + @external @view @nonreentrant('lock') @@ -593,7 +609,13 @@ def max_borrowable(collateral: uint256, N: uint256, current_debt: uint256 = 0, u 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 + + rate_mul: uint256 = AMM.get_rate_mul() + loan: Loan = self._total_debt + _total_debt: uint256 = loan.initial_debt * rate_mul / loan.rate_mul + _cap: uint256 = unsafe_sub(max(self.borrow_cap, _total_debt), _total_debt) + _cap = min(self._borrowed_balance() + current_debt, _cap) + return min(x, _cap) # Cannot borrow beyond the amount of coins Controller has or beyond borrow_cap @external @@ -698,6 +720,7 @@ def _create_loan(collateral: uint256, debt: uint256, N: uint256, transfer_coins: self._total_debt.rate_mul = rate_mul AMM.deposit_range(_for, collateral, n1, n2) + self.lent += debt self.processed += debt if transfer_coins: @@ -864,6 +887,7 @@ def borrow_more(collateral: uint256, debt: uint256, _for: address = msg.sender): return assert self._check_approval(_for) self._add_collateral_borrow(collateral, debt, _for, False, False) + self.lent += debt self.processed += debt self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) self.transfer(BORROWED_TOKEN, _for, debt) @@ -899,6 +923,7 @@ def borrow_more_extended(collateral: uint256, debt: uint256, callbacker: address # After callback self._add_collateral_borrow(collateral + more_collateral, debt, _for, False, False) + self.lent += debt self.processed += debt self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) self.transferFrom(COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral) @@ -1517,13 +1542,6 @@ def collect_fees() -> uint256: return 0 -@external -@view -@nonreentrant('lock') -def check_lock() -> bool: - return True - - # Allowance methods @external diff --git a/contracts/lending/Vault.vy b/contracts/lending/Vault.vy index a96403b5..f5928a88 100644 --- a/contracts/lending/Vault.vy +++ b/contracts/lending/Vault.vy @@ -27,6 +27,7 @@ interface AMM: interface Controller: def total_debt() -> uint256: view + def borrowed_balance() -> uint256: view def monetary_policy() -> address: view def check_lock() -> bool: view def save_rate(): nonpayable @@ -96,6 +97,9 @@ factory: public(Factory) maxSupply: public(uint256) +deposited: public(uint256) # cumulative amount of assets ever deposited +withdrawn: public(uint256) # cumulative amount of assets ever withdrawn + # ERC20 publics @@ -278,8 +282,7 @@ 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 self.controller.borrowed_balance() + self.controller.total_debt() @external @@ -405,6 +408,7 @@ def deposit(assets: uint256, receiver: address = msg.sender) -> uint256: 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) + self.deposited += assets self._mint(receiver, to_mint) controller.save_rate() log Deposit(msg.sender, receiver, assets, to_mint) @@ -449,6 +453,7 @@ def mint(shares: uint256, receiver: address = msg.sender) -> uint256: 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) + self.deposited += assets self._mint(receiver, shares) controller.save_rate() log Deposit(msg.sender, receiver, assets, shares) @@ -464,7 +469,7 @@ def maxWithdraw(owner: address) -> uint256: """ return min( self._convert_to_assets(self.balanceOf[owner]), - self.borrowed_token.balanceOf(self.controller.address)) + self.controller.borrowed_balance()) @external @@ -474,7 +479,7 @@ 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 <= self.controller.borrowed_balance() return self._convert_to_shares(assets, False) @@ -498,6 +503,7 @@ def withdraw(assets: uint256, receiver: address = msg.sender, owner: address = m controller: Controller = self.controller self._burn(owner, shares) assert self.borrowed_token.transferFrom(controller.address, receiver, assets, default_return_value=True) + self.withdrawn += assets controller.save_rate() log Withdraw(msg.sender, receiver, owner, assets, shares) return shares @@ -511,7 +517,7 @@ 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(self.controller.borrowed_balance(), False), self.balanceOf[owner]) @@ -528,7 +534,7 @@ 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) + assert assets_to_redeem <= self.controller.borrowed_balance() return assets_to_redeem @@ -557,6 +563,7 @@ def redeem(shares: uint256, receiver: address = msg.sender, owner: address = msg self._burn(owner, shares) controller: Controller = self.controller assert self.borrowed_token.transferFrom(controller.address, receiver, assets_to_redeem, default_return_value=True) + self.withdrawn += assets_to_redeem controller.save_rate() log Withdraw(msg.sender, receiver, owner, assets_to_redeem, shares) return assets_to_redeem From 68617d3c82253e51dc624aebc974964e45b38504 Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 24 Jul 2025 18:08:46 +0400 Subject: [PATCH 039/413] refactor: remove admin_fee from AMM --- contracts/AMM.vy | 82 +++++---------------------------- contracts/Controller.vy | 5 -- contracts/lending/Controller.vy | 4 -- 3 files changed, 12 insertions(+), 79 deletions(-) diff --git a/contracts/AMM.vy b/contracts/AMM.vy index a627f420..3fcbb581 100644 --- a/contracts/AMM.vy +++ b/contracts/AMM.vy @@ -74,9 +74,6 @@ event SetRate: event SetFee: fee: uint256 -event SetAdminFee: - fee: uint256 - MAX_TICKS: constant(int256) = 50 MAX_TICKS_UINT: constant(uint256) = 50 @@ -94,7 +91,6 @@ struct DetailedTrade: n2: int256 ticks_in: DynArray[uint256, MAX_TICKS_UINT] last_tick_j: uint256 - admin_fee: uint256 BORROWED_TOKEN: immutable(ERC20) # x @@ -113,7 +109,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,9 +116,6 @@ 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 @@ -165,7 +157,7 @@ def __init__( @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 admin_fee DEPRECATED, left for backward compatibility @param _price_oracle_contract External price oracle which has price() and price_w() methods which both return current price of collateral multiplied by 1e18 """ @@ -181,7 +173,6 @@ def __init__( 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() @@ -818,12 +809,10 @@ 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 - withdraw dust to the user 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) + dx += x + dy += y x = 0 y = 0 @@ -906,7 +895,6 @@ 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): @@ -950,24 +938,20 @@ def calc_swap_out(pump: bool, in_amount: uint256, p_o: uint256[2], in_precision: # 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.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 out.n2 == max_band: @@ -991,23 +975,19 @@ def calc_swap_out(pump: bool, in_amount: uint256, p_o: uint256[2], in_precision: # 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.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 out.n2 == min_band: @@ -1141,12 +1121,6 @@ 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)) @@ -1220,7 +1194,6 @@ 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): @@ -1265,9 +1238,7 @@ def calc_swap_in(pump: bool, out_amount: uint256, p_o: uint256[2], in_precision: 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,9 +1248,7 @@ 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 out.n2 == max_band: @@ -1304,9 +1273,7 @@ def calc_swap_in(pump: bool, out_amount: uint256, p_o: uint256[2], in_precision: 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,9 +1283,7 @@ 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 out.n2 == min_band: @@ -1766,29 +1731,6 @@ def set_fee(fee: uint256): 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): diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 02aa1960..77be186b 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -23,12 +23,8 @@ interface LLAMMA: 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 @@ -163,7 +159,6 @@ Aminus1: immutable(uint256) LOGN_A_RATIO: immutable(int256) # log(A / (A - 1)) SQRT_BAND_RATIO: immutable(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 diff --git a/contracts/lending/Controller.vy b/contracts/lending/Controller.vy index c4aef727..5ca5d0ba 100644 --- a/contracts/lending/Controller.vy +++ b/contracts/lending/Controller.vy @@ -23,12 +23,8 @@ interface LLAMMA: 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 From 90401acb4c17fcfb58ddd43a3593f6def8c9a34d Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 24 Jul 2025 18:22:03 +0400 Subject: [PATCH 040/413] refactor: clean init in crvUSD Controller --- contracts/Controller.vy | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 77be186b..64723ace 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -45,10 +45,6 @@ interface Factory: 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) @@ -208,22 +204,10 @@ def __init__( 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()) - - COLLATERAL_TOKEN = _collateral_token - BORROWED_TOKEN = _borrowed_token - COLLATERAL_PRECISION = pow_mod256(10, 18 - _collateral_token.decimals()) - BORROWED_PRECISION = pow_mod256(10, 18 - _borrowed_token.decimals()) + COLLATERAL_TOKEN = ERC20(collateral_token) + BORROWED_TOKEN = ERC20(Factory(msg.sender).stablecoin()) + COLLATERAL_PRECISION = pow_mod256(10, 18 - COLLATERAL_TOKEN.decimals()) + BORROWED_PRECISION = pow_mod256(10, 18 - BORROWED_TOKEN.decimals()) SQRT_BAND_RATIO = isqrt(unsafe_div(10**36 * _A, unsafe_sub(_A, 1))) From 7aa1c956ae0f2569ed9792b1b37b758b464580fc Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 24 Jul 2025 20:49:30 +0400 Subject: [PATCH 041/413] refactor: join usual and extended methods (except repay) --- contracts/lending/Controller.vy | 151 +++++++++----------------------- 1 file changed, 42 insertions(+), 109 deletions(-) diff --git a/contracts/lending/Controller.vy b/contracts/lending/Controller.vy index 5ca5d0ba..d325be91 100644 --- a/contracts/lending/Controller.vy +++ b/contracts/lending/Controller.vy @@ -166,14 +166,12 @@ MAX_ADMIN_FEE: constant(uint256) = 2 * 10**17 # 20% MIN_AMM_FEE: constant(uint256) = 10**6 # 1e-12, still needs to be above 0 MAX_AMM_FEE: immutable(uint256) # let's set to MIN_TICKS / A: for example, 4% max fee for A=100 -CALLBACK_DEPOSIT: constant(bytes4) = method_id("callback_deposit(address,uint256,uint256,uint256,uint256[])", output_type=bytes4) +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,uint256[])", output_type=bytes4) -CALLBACK_LIQUIDATE: constant(bytes4) = method_id("callback_liquidate(address,uint256,uint256,uint256,uint256[])", output_type=bytes4) +CALLBACK_LIQUIDATE: constant(bytes4) = method_id("callback_liquidate(address,uint256,uint256,uint256,bytes)", output_type=bytes4) -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 @@ -664,9 +662,8 @@ def transfer(token: ERC20, _to: address, amount: uint256): @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, stablecoins: uint256, + collateral: uint256, debt: uint256, calldata: Bytes[10**4]) -> CallbackData: assert callbacker != COLLATERAL_TOKEN.address assert callbacker != BORROWED_TOKEN.address @@ -678,7 +675,7 @@ def execute_callback(callbacker: address, callback_sig: bytes4, # Callback response: Bytes[64] = raw_call( callbacker, - concat(callback_sig, _abi_encode(user, stablecoins, collateral, debt, callback_args, callback_bytes)), + concat(callback_sig, _abi_encode(user, stablecoins, collateral, debt, calldata)), max_outsize=64 ) data.stablecoins = convert(slice(response, 0, 32), uint256) @@ -692,7 +689,7 @@ def execute_callback(callbacker: address, callback_sig: bytes4, return data @internal -def _create_loan(collateral: uint256, debt: uint256, N: uint256, transfer_coins: bool, _for: address): +def _create_loan(collateral: uint256, debt: uint256, N: uint256, _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" @@ -719,10 +716,6 @@ def _create_loan(collateral: uint256, debt: uint256, N: uint256, transfer_coins: self.lent += debt self.processed += 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) @@ -731,25 +724,7 @@ def _create_loan(collateral: uint256, debt: uint256, N: uint256, transfer_coins: @external @nonreentrant('lock') -def create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address = msg.sender): - """ - @notice Create loan - @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 _for Address to create the loan for - """ - 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) - - -@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): +def create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address = msg.sender, callbacker: address = empty(address), calldata: Bytes[10**4] = b""): """ @notice Create loan but pass stablecoin to a callback first so that it can build leverage @param collateral Amount of collateral to use @@ -757,28 +732,28 @@ def create_loan_extended(collateral: uint256, debt: uint256, N: uint256, callbac @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 calldata Any data for callbacker @param _for Address to create the loan for """ 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) - # Before callback - self.transfer(BORROWED_TOKEN, callbacker, debt) + more_collateral: uint256 = 0 + if callbacker != empty(address): + self.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 - # 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 + self._create_loan(collateral + more_collateral, debt, N, _for) - # 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) + if more_collateral > 0: + self.transferFrom(COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral) + if callbacker == empty(address): + self.transfer(BORROWED_TOKEN, _for, debt) @internal @@ -872,57 +847,34 @@ def remove_collateral(collateral: uint256, _for: address = msg.sender): @external @nonreentrant('lock') -def borrow_more(collateral: uint256, debt: uint256, _for: address = msg.sender): - """ - @notice Borrow more stablecoins while adding more collateral (not necessary) - @param collateral Amount of collateral to add - @param debt Amount of stablecoin 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.lent += debt - self.processed += 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): +def borrow_more(collateral: uint256, debt: uint256, _for: address = msg.sender, callbacker: address = empty(address), calldata: Bytes[10**4] = b""): """ @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 calldata Any data for callbacker @param _for Address to borrow for """ if debt == 0: return assert self._check_approval(_for) - # Before callback - self.transfer(BORROWED_TOKEN, callbacker, debt) - - # 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 + more_collateral: uint256 = 0 + if callbacker != empty(address): + self.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 - # After callback self._add_collateral_borrow(collateral + more_collateral, debt, _for, False, False) self.lent += debt self.processed += debt self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) - self.transferFrom(COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral) + if more_collateral > 0: + self.transferFrom(COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral) + if callbacker == empty(address): + self.transfer(BORROWED_TOKEN, _for, debt) self._save_rate() @@ -1017,7 +969,7 @@ def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 @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_extended(callbacker: address, calldata: Bytes[10**4] = b"", _for: address = msg.sender): """ @notice Repay loan but get a stablecoin for that from callback (to deleverage) @param callbacker Address of the callback contract @@ -1036,10 +988,10 @@ def repay_extended(callbacker: address, callback_args: DynArray[uint256,5], call # For compatibility callback_sig: bytes4 = CALLBACK_REPAY_WITH_BYTES - if callback_bytes == b"": + if calldata == b"": callback_sig = CALLBACK_REPAY cb: CallbackData = self.execute_callback( - callbacker, callback_sig, _for, xy[0], xy[1], debt, callback_args, callback_bytes) + callbacker, callback_sig, _for, xy[0], xy[1], debt, calldata) # After callback total_stablecoins: uint256 = cb.stablecoins + xy[0] @@ -1197,8 +1149,7 @@ def _get_f_remove(frac: uint256, health_limit: uint256) -> uint256: @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""): +def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint256, callbacker: address, calldata: Bytes[10**4]): """ @notice Perform a bad liquidation of user if the health is too bad @param user Address of the user @@ -1206,7 +1157,7 @@ def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint2 @param health_limit Minimal health to liquidate at @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 """ debt: uint256 = 0 rate_mul: uint256 = 0 @@ -1246,13 +1197,9 @@ def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint2 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 # Callback cb: CallbackData = self.execute_callback( - callbacker, callback_sig, user, xy[0], xy[1], debt, callback_args, callback_bytes) + callbacker, CALLBACK_LIQUIDATE, user, xy[0], xy[1], debt, calldata) 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)) @@ -1283,32 +1230,18 @@ def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint2 @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""): +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 (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 + @param calldata Any data for callbacker """ 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) + self._liquidate(user, min_x, discount, min(frac, 10**18), callbacker, calldata) @view From 305c3451c4ad929df6d71da69b5dbf38b1f24484 Mon Sep 17 00:00:00 2001 From: macket Date: Fri, 25 Jul 2025 15:25:23 +0400 Subject: [PATCH 042/413] refactor: join repay and repay_extended --- contracts/lending/Controller.vy | 197 ++++++++++++-------------------- 1 file changed, 74 insertions(+), 123 deletions(-) diff --git a/contracts/lending/Controller.vy b/contracts/lending/Controller.vy index d325be91..9f83e900 100644 --- a/contracts/lending/Controller.vy +++ b/contracts/lending/Controller.vy @@ -167,12 +167,9 @@ MIN_AMM_FEE: constant(uint256) = 10**6 # 1e-12, still needs to be above 0 MAX_AMM_FEE: immutable(uint256) # let's set to MIN_TICKS / A: for example, 4% max fee for A=100 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,uint256[])", 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) -# 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 - DEAD_SHARES: constant(uint256) = 1000 approval: public(HashMap[address, HashMap[address, bool]]) @@ -731,9 +728,9 @@ def create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address = @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 _for Address to create the loan for @param callbacker Address of the callback contract @param calldata Any data for callbacker - @param _for Address to create the loan for """ if _for != tx.origin: # We can create a loan for tx.origin (for example when wrapping ETH with EOA), @@ -852,9 +849,9 @@ def borrow_more(collateral: uint256, debt: uint256, _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 _for Address to borrow for @param callbacker Address of the callback contract @param calldata Any data for callbacker - @param _for Address to borrow for """ if debt == 0: return @@ -893,156 +890,110 @@ def _remove_from_list(_for: address): @external @nonreentrant('lock') -def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 = 2**255-1): +def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 = 2**255-1, + callbacker: address = empty(address), calldata: Bytes[10**4] = b""): """ @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 _d_debt The amount of debt to repay from user's wallet. 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) + @param callbacker Address of the callback contract + @param calldata Any data for callbacker """ - 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) + xy: uint256[2] = empty(uint256[2]) - if debt == 0: - # Allow to withdraw all assets even when underwater - xy: uint256[2] = AMM.withdraw(_for, 10**18) + cb: CallbackData = empty(CallbackData) + if callbacker != empty(address): + assert approval + xy = AMM.withdraw(_for, 10 ** 18) + self.transferFrom(COLLATERAL_TOKEN, AMM.address, callbacker, xy[1]) + cb = self.execute_callback( + callbacker, CALLBACK_REPAY, _for, xy[0], xy[1], debt, calldata) + + total_stablecoins: uint256 = _d_debt + xy[0] + cb.stablecoins + assert total_stablecoins > 0 # dev: no coins to repay + 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 + if callbacker == empty(address): + xy = AMM.withdraw(_for, 10 ** 18) + + # Transfer all stablecoins to self 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]) + self.transferFrom(BORROWED_TOKEN, AMM.address, self, xy[0]) + if cb.stablecoins > 0: + self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins) + if _d_debt > 0: + self.transferFrom(BORROWED_TOKEN, msg.sender, self, _d_debt) + + # Transfer stablecoins excess to _for + if total_stablecoins > d_debt: + self.transfer(BORROWED_TOKEN, _for, unsafe_sub(total_stablecoins, d_debt)) + # Transfer collateral to _for + if callbacker == empty(address): + if xy[1] > 0: + self.transferFrom(COLLATERAL_TOKEN, AMM.address, _for, xy[1]) + else: + if cb.collateral > 0: + self.transferFrom(COLLATERAL_TOKEN, callbacker, _for, cb.collateral) + + self._remove_from_list(_for) log UserState(_for, 0, 0, 0, 0, 0) log Repay(_for, xy[1], d_debt) - self._remove_from_list(_for) - + # Else - partial repayment else: active_band: int256 = AMM.active_band_with_skip() assert active_band <= max_active_band + d_debt = total_stablecoins + debt = unsafe_sub(debt, d_debt) 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) + # Not in soft-liquidation - can use callback and move bands + new_collateral: uint256 = cb.collateral + if callbacker == empty(address): + xy = AMM.withdraw(_for, 10**18) + new_collateral = xy[1] + ns[0] = self._calculate_debt_n1(new_collateral, debt, convert(unsafe_add(size, 1), uint256), _for) + ns[1] = ns[0] + size + AMM.deposit_range(_for, new_collateral, ns[0], ns[1]) + else: + # Underwater - cannot use callback or move bands but can avoid a bad liquidation + xy = AMM.get_sum_xy(_for) + assert callbacker == empty(address) + + 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: - # 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) - - 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.repaid += 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 - - self._save_rate() + if cb.stablecoins > 0: + self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins) + if _d_debt > 0: + self.transferFrom(BORROWED_TOKEN, msg.sender, self, _d_debt) + log UserState(_for, xy[1], debt, ns[0], ns[1], liquidation_discount) + log Repay(_for, 0, d_debt) -@external -@nonreentrant('lock') -def repay_extended(callbacker: address, calldata: Bytes[10**4] = b"", _for: address = msg.sender): - """ - @notice Repay loan but get a stablecoin for that from callback (to deleverage) - @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 - """ - 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 calldata == b"": - callback_sig = CALLBACK_REPAY - cb: CallbackData = self.execute_callback( - callbacker, callback_sig, _for, xy[0], xy[1], debt, calldata) - - # 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 - 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 - - 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 - - # No need to check _health() because it's the _for - - # Common calls which we will do regardless of whether it's a full repay or not - log Repay(_for, xy[1], d_debt) self.repaid += 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) From f8adb8b476db7b35f1174df59d60ed4779868a2e Mon Sep 17 00:00:00 2001 From: macket Date: Fri, 25 Jul 2025 15:25:58 +0400 Subject: [PATCH 043/413] fix: init in mint Controller --- contracts/Controller.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 64723ace..db02cd7c 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -211,7 +211,7 @@ def __init__( SQRT_BAND_RATIO = isqrt(unsafe_div(10**36 * _A, unsafe_sub(_A, 1))) - assert _borrowed_token.approve(msg.sender, max_value(uint256), default_return_value=True) + assert BORROWED_TOKEN.approve(msg.sender, max_value(uint256), default_return_value=True) @internal From 443968a2e73186edfac7d78ebf7cf320badcfec9 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 28 Jul 2025 11:17:27 +0400 Subject: [PATCH 044/413] refactor: add _get_total_debt and _update_total_debt helpers --- contracts/lending/Controller.vy | 68 +++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/contracts/lending/Controller.vy b/contracts/lending/Controller.vy index 9f83e900..ddd41510 100644 --- a/contracts/lending/Controller.vy +++ b/contracts/lending/Controller.vy @@ -432,10 +432,9 @@ def loan_exists(user: address) -> bool: return self.loan[user].initial_debt > 0 -# No decorator because used in monetary policy -@external +@internal @view -def total_debt() -> uint256: +def _get_total_debt() -> uint256: """ @notice Total debt of this controller """ @@ -444,6 +443,36 @@ def total_debt() -> uint256: return loan.initial_debt * rate_mul / loan.rate_mul +@internal +def _update_total_debt(d_debt: uint256, rate_mul: uint256, is_increase: bool) -> Loan: + """ + @param d_debt Change in debt amount (unsigned) + @param rate_mul New rate_mul + @param is_increase Whether debt increases or decreases + @notice Update total debt of this controller + """ + loan: Loan = self._total_debt + loan.initial_debt = loan.initial_debt * rate_mul / loan.rate_mul + if is_increase: + loan.initial_debt += d_debt + assert loan.initial_debt <= self.borrow_cap, "Borrow cap exceeded" + else: + loan.initial_debt = unsafe_sub(max(loan.initial_debt, d_debt), d_debt) + loan.rate_mul = rate_mul + self._total_debt = loan + + return loan + +# No decorator because used in monetary policy +@external +@view +def total_debt() -> uint256: + """ + @notice Total debt of this controller + """ + return self._get_total_debt() + + @internal @pure def get_y_effective(collateral: uint256, N: uint256, discount: uint256) -> uint256: @@ -601,9 +630,7 @@ def max_borrowable(collateral: uint256, N: uint256, current_debt: uint256 = 0, u 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 - rate_mul: uint256 = AMM.get_rate_mul() - loan: Loan = self._total_debt - _total_debt: uint256 = loan.initial_debt * rate_mul / loan.rate_mul + _total_debt: uint256 = self._get_total_debt() _cap: uint256 = unsafe_sub(max(self.borrow_cap, _total_debt), _total_debt) _cap = min(self._borrowed_balance() + current_debt, _cap) return min(x, _cap) # Cannot borrow beyond the amount of coins Controller has or beyond borrow_cap @@ -704,10 +731,7 @@ def _create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address): self.loan_ix[_for] = n_loans self.n_loans = unsafe_add(n_loans, 1) - new_total_debt: uint256 = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul + debt - assert new_total_debt <= self.borrow_cap, "Borrow cap exceeded" - self._total_debt.initial_debt = new_total_debt - self._total_debt.rate_mul = rate_mul + self._update_total_debt(debt, rate_mul, True) AMM.deposit_range(_for, collateral, n1, n2) self.lent += debt @@ -798,10 +822,7 @@ def _add_collateral_borrow(d_collateral: uint256, d_debt: uint256, _for: address liquidation_discount = self.liquidation_discounts[_for] if d_debt != 0: - new_total_debt: uint256 = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul + d_debt - assert new_total_debt <= self.borrow_cap, "Borrow cap exceeded" - self._total_debt.initial_debt = new_total_debt - self._total_debt.rate_mul = rate_mul + self._update_total_debt(d_debt, rate_mul, True) if remove_collateral: log RemoveCollateral(_for, d_collateral) @@ -995,9 +1016,7 @@ def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 self.repaid += 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 + self._update_total_debt(d_debt, rate_mul, False) self._save_rate() @@ -1172,9 +1191,7 @@ def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint2 log UserState(user, 0, 0, 0, 0, 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._update_total_debt(debt, rate_mul, False) self._save_rate() @@ -1381,11 +1398,8 @@ 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 processed: uint256 = self.processed - return unsafe_sub(max(loan.initial_debt + self.repaid, processed), processed) + return unsafe_sub(max(self._get_total_debt() + self.repaid, processed), processed) @external @@ -1398,11 +1412,7 @@ def collect_fees() -> uint256: # 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 - + loan: Loan = self._update_total_debt(0, rate_mul, False) self._save_rate() # Cumulative amount which would have been repaid if all the debt was repaid now From 128f5b5b9c5ffa0aa0e8b082b68fe8fa18683f40 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 28 Jul 2025 12:15:07 +0400 Subject: [PATCH 045/413] fix: max_p_base inconsistency --- contracts/lending/Controller.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/lending/Controller.vy b/contracts/lending/Controller.vy index ddd41510..d4354e8e 100644 --- a/contracts/lending/Controller.vy +++ b/contracts/lending/Controller.vy @@ -575,7 +575,7 @@ def max_p_base() -> uint256: if n1 <= n_min: break p_base_prev: uint256 = p_base - p_base = unsafe_div(p_base * A, Aminus1) + p_base = AMM.p_oracle_up(n1) if p_base > p_oracle: return p_base_prev From cebfc197b652795921ee98d788d9e2841bb8525c Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 28 Jul 2025 16:26:07 +0200 Subject: [PATCH 046/413] refactor: bump Controller to Vyper 0.4.1 --- contracts/Controller.vy | 467 +++++++++++++++------------------------- 1 file changed, 178 insertions(+), 289 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index db02cd7c..1b2a66cf 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -1,4 +1,4 @@ -# @version 0.3.10 +# pragma version 0.4.1 # pragma optimize codesize # pragma evm-version shanghai """ @@ -7,6 +7,11 @@ @license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved """ +from snekmate.utils import math +from ethereum.ercs import IERC20 +from ethereum.ercs import IERC20Detailed + + interface LLAMMA: def A() -> uint256: view def get_p() -> uint256: view @@ -30,13 +35,6 @@ interface LLAMMA: 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 @@ -143,10 +141,10 @@ monetary_policy: public(MonetaryPolicy) liquidation_discount: public(uint256) loan_discount: public(uint256) -COLLATERAL_TOKEN: immutable(ERC20) +COLLATERAL_TOKEN: immutable(IERC20) COLLATERAL_PRECISION: immutable(uint256) -BORROWED_TOKEN: immutable(ERC20) +BORROWED_TOKEN: immutable(IERC20) BORROWED_PRECISION: immutable(uint256) AMM: immutable(LLAMMA) @@ -173,7 +171,7 @@ approval: public(HashMap[address, HashMap[address, bool]]) extra_health: public(HashMap[address, uint256]) -@external +@deploy def __init__( collateral_token: address, monetary_policy: address, @@ -198,138 +196,27 @@ def __init__( self._total_debt.rate_mul = 10**18 AMM = LLAMMA(amm) - _A: uint256 = LLAMMA(amm).A() + _A: uint256 = staticcall 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(collateral_token) - BORROWED_TOKEN = ERC20(Factory(msg.sender).stablecoin()) - COLLATERAL_PRECISION = pow_mod256(10, 18 - COLLATERAL_TOKEN.decimals()) - BORROWED_PRECISION = pow_mod256(10, 18 - BORROWED_TOKEN.decimals()) - - SQRT_BAND_RATIO = isqrt(unsafe_div(10**36 * _A, unsafe_sub(_A, 1))) + LOGN_A_RATIO = math._wad_ln(convert(unsafe_div(_A * 10**18, unsafe_sub(_A, 1)), int256)) + MAX_FEE = min(unsafe_div(10**18 * MIN_TICKS_UINT, A), 10**17) - assert BORROWED_TOKEN.approve(msg.sender, max_value(uint256), default_return_value=True) + COLLATERAL_TOKEN = IERC20(collateral_token) + collateral_decimals: uint256 = convert(staticcall IERC20Detailed(COLLATERAL_TOKEN.address).decimals(), uint256) + COLLATERAL_PRECISION = pow_mod256(10, 18 - collateral_decimals) + BORROWED_TOKEN = IERC20(staticcall Factory(msg.sender).stablecoin()) + borrowed_decimals: uint256 = convert(staticcall IERC20Detailed(BORROWED_TOKEN.address).decimals(), uint256) + BORROWED_PRECISION = pow_mod256(10, 18 - borrowed_decimals) -@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 + SQRT_BAND_RATIO = isqrt(unsafe_div(10**36 * _A, unsafe_sub(_A, 1))) - -@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 + assert extcall BORROWED_TOKEN.approve(msg.sender, max_value(uint256), default_return_value=True) @external -@pure +@view def factory() -> Factory: """ @notice Address of the factory @@ -338,7 +225,7 @@ def factory() -> Factory: @external -@pure +@view def amm() -> LLAMMA: """ @notice Address of the AMM @@ -347,8 +234,8 @@ def amm() -> LLAMMA: @external -@pure -def collateral_token() -> ERC20: +@view +def collateral_token() -> IERC20: """ @notice Address of the collateral token """ @@ -356,8 +243,8 @@ def collateral_token() -> ERC20: @external -@pure -def borrowed_token() -> ERC20: +@view +def borrowed_token() -> IERC20: """ @notice Address of the borrowed token """ @@ -369,12 +256,12 @@ 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') +@nonreentrant def save_rate(): """ @notice Save current rate @@ -390,7 +277,7 @@ def _debt(user: address) -> (uint256, uint256): @param user User address @return (debt, rate_mul) """ - rate_mul: uint256 = AMM.get_rate_mul() + rate_mul: uint256 = staticcall AMM.get_rate_mul() loan: Loan = self.loan[user] if loan.initial_debt == 0: return (0, rate_mul) @@ -407,7 +294,7 @@ def _debt(user: address) -> (uint256, uint256): @external @view -@nonreentrant('lock') +@nonreentrant def debt(user: address) -> uint256: """ @notice Get the value of debt without changing the state @@ -419,7 +306,7 @@ def debt(user: address) -> uint256: @external @view -@nonreentrant('lock') +@nonreentrant def loan_exists(user: address) -> bool: """ @notice Check whether there is a loan of `user` in existence @@ -434,13 +321,13 @@ def total_debt() -> uint256: """ @notice Total debt of this controller """ - rate_mul: uint256 = AMM.get_rate_mul() + rate_mul: uint256 = staticcall AMM.get_rate_mul() loan: Loan = self._total_debt - return loan.initial_debt * rate_mul / loan.rate_mul + return loan.initial_debt * rate_mul // loan.rate_mul @internal -@pure +@view def get_y_effective(collateral: uint256, N: uint256, discount: uint256) -> uint256: """ @notice Intermediary method which calculates y_effective defined as x_effective / p_base, @@ -463,7 +350,7 @@ def get_y_effective(collateral: uint256, N: uint256, discount: uint256) -> uint2 ), 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) @@ -483,8 +370,8 @@ 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) @@ -504,18 +391,18 @@ def _calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256, user: add # 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_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 @@ -526,17 +413,17 @@ def max_p_base() -> uint256: """ @notice Calculate max base price including skipping bands """ - p_oracle: uint256 = AMM.price_oracle() + p_oracle: uint256 = staticcall 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) + n1: int256 = math._wad_ln(convert(staticcall AMM.get_base_price() * 10**18 // 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 = AMM.active_band_with_skip() + n_min: int256 = staticcall AMM.active_band_with_skip() n1 = max(n1, n_min + 1) - p_base: uint256 = AMM.p_oracle_up(n1) + p_base: uint256 = staticcall AMM.p_oracle_up(n1) - for i in range(MAX_SKIP_TICKS + 1): + for i: uint256 in range(MAX_SKIP_TICKS + 1): n1 -= 1 if n1 <= n_min: break @@ -550,7 +437,7 @@ def max_p_base() -> uint256: @external @view -@nonreentrant('lock') +@nonreentrant 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) @@ -581,12 +468,12 @@ def max_borrowable(collateral: uint256, N: uint256, current_debt: uint256 = 0, u 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 min(x, staticcall BORROWED_TOKEN.balanceOf(self) + current_debt) # Cannot borrow beyond the amount of coins Controller has @external @view -@nonreentrant('lock') +@nonreentrant def min_collateral(debt: uint256, N: uint256, user: address = empty(address)) -> uint256: """ @notice Minimal amount of collateral required to support debt @@ -599,7 +486,7 @@ def min_collateral(debt: uint256, N: uint256, user: address = empty(address)) -> 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)), + 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) @@ -607,7 +494,7 @@ def min_collateral(debt: uint256, N: uint256, user: address = empty(address)) -> @external @view -@nonreentrant('lock') +@nonreentrant 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 @@ -622,15 +509,15 @@ def calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256, user: addr @internal -def transferFrom(token: ERC20, _from: address, _to: address, amount: uint256): +def transferFrom(token: IERC20, _from: address, _to: address, amount: uint256): if amount > 0: - assert token.transferFrom(_from, _to, amount, default_return_value=True) + assert extcall token.transferFrom(_from, _to, amount, default_return_value=True) @internal -def transfer(token: ERC20, _to: address, amount: uint256): +def transfer(token: IERC20, _to: address, amount: uint256): if amount > 0: - assert token.transfer(_to, amount, default_return_value=True) + assert extcall token.transfer(_to, amount, default_return_value=True) @internal @@ -641,37 +528,37 @@ def execute_callback(callbacker: address, callback_sig: bytes4, 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.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)), + concat(callback_sig, abi_encode(user, stablecoins, collateral, debt, callback_args, callback_bytes)), max_outsize=64 ) data.stablecoins = 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" + assert N > MIN_TICKS_UINT - 1, "Need more ticks" + assert N < MAX_TICKS_UINT + 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}) + rate_mul: uint256 = staticcall 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 @@ -680,10 +567,10 @@ def _create_loan(collateral: uint256, debt: uint256, N: uint256, transfer_coins: 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.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) + extcall AMM.deposit_range(_for, collateral, n1, n2) self.minted += debt if transfer_coins: @@ -692,12 +579,12 @@ def _create_loan(collateral: uint256, debt: uint256, N: uint256, transfer_coins: self._save_rate() - log UserState(_for, collateral, debt, n1, n2, liquidation_discount) - log Borrow(_for, collateral, debt) + log UserState(user=_for, collateral=collateral, debt=debt, n1=n1, n2=n2, liquidation_discount=liquidation_discount) + log Borrow(user=_for, collateral_increase=collateral, loan_increase=debt) @external -@nonreentrant('lock') +@nonreentrant def create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address = msg.sender): """ @notice Create loan @@ -715,7 +602,7 @@ def create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address = @external -@nonreentrant('lock') +@nonreentrant 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 @@ -764,10 +651,8 @@ def _add_collateral_borrow(d_collateral: uint256, d_debt: uint256, _for: address debt, rate_mul = self._debt(_for) assert debt > 0, "Loan doesn't exist" 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) + xy: uint256[2] = extcall AMM.withdraw(_for, 10**18) assert xy[0] == 0, "Already in underwater mode" if remove_collateral: xy[1] -= d_collateral @@ -778,12 +663,15 @@ 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 * 10**18 * 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] = Loan(initial_debt=debt, rate_mul=rate_mul) liquidation_discount: uint256 = 0 if _for == msg.sender: @@ -793,19 +681,19 @@ def _add_collateral_borrow(d_collateral: uint256, d_debt: uint256, _for: address 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.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 RemoveCollateral(user=_for, collateral_decrease=d_collateral) else: - log Borrow(_for, d_collateral, d_debt) + log Borrow(user=_for, collateral_increase=d_collateral, loan_increase=d_debt) - log UserState(_for, xy[1], debt, n1, n2, liquidation_discount) + log UserState(user=_for, collateral=xy[1], debt=debt, n1=n1, n2=n2, liquidation_discount=liquidation_discount) @external -@nonreentrant('lock') +@nonreentrant def add_collateral(collateral: uint256, _for: address = msg.sender): """ @notice Add extra collateral to avoid bad liqidations @@ -820,7 +708,7 @@ def add_collateral(collateral: uint256, _for: address = msg.sender): @external -@nonreentrant('lock') +@nonreentrant def remove_collateral(collateral: uint256, _for: address = msg.sender): """ @notice Remove some collateral without repaying the debt @@ -836,7 +724,7 @@ def remove_collateral(collateral: uint256, _for: address = msg.sender): @external -@nonreentrant('lock') +@nonreentrant def borrow_more(collateral: uint256, debt: uint256, _for: address = msg.sender): """ @notice Borrow more stablecoins while adding more collateral (not necessary) @@ -855,7 +743,7 @@ def borrow_more(collateral: uint256, debt: uint256, _for: address = msg.sender): @external -@nonreentrant('lock') +@nonreentrant 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) @@ -903,8 +791,8 @@ def _remove_from_list(_for: address): @external -@nonreentrant('lock') -def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 = 2**255-1): +@nonreentrant +def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 = max_value(int256)): """ @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 @@ -925,41 +813,41 @@ def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 if debt == 0: # Allow to withdraw all assets even when underwater - xy: uint256[2] = AMM.withdraw(_for, 10**18) + xy: uint256[2] = extcall 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) + log UserState(user=_for, collateral=0, debt=0, n1=0, n2=0, liquidation_discount=0) + log Repay(user=_for, collateral_decrease=xy[1], loan_decrease=d_debt) self._remove_from_list(_for) else: - active_band: int256 = AMM.active_band_with_skip() + active_band: int256 = staticcall AMM.active_band_with_skip() assert active_band <= max_active_band - ns: int256[2] = AMM.read_user_tick_numbers(_for) + ns: int256[2] = staticcall 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) + xy: uint256[2] = extcall 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) + extcall 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) + log UserState(user=_for, collateral=xy[1], debt=debt, n1=n1, n2=n2, liquidation_discount=liquidation_discount) + log Repay(user=_for, collateral_decrease=0, loan_decrease=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 UserState(user=_for, collateral=max_value(uint256), debt=debt, n1=ns[0], n2=ns[1], liquidation_discount=liquidation_discount) + log Repay(user=_for, collateral_decrease=0, loan_decrease=d_debt) if not approval: # Doesn't allow non-sender to repay in a way which ends with unhealthy state @@ -970,8 +858,8 @@ def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 self.transferFrom(BORROWED_TOKEN, msg.sender, self, d_debt) # fail: insufficient funds 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.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 @@ -979,7 +867,7 @@ def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 @external -@nonreentrant('lock') +@nonreentrant def repay_extended(callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b"", _for: address = msg.sender): """ @notice Repay loan but get a stablecoin for that from callback (to deleverage) @@ -990,8 +878,8 @@ def repay_extended(callbacker: address, callback_args: DynArray[uint256,5], call assert self._check_approval(_for) # Before callback - ns: int256[2] = AMM.read_user_tick_numbers(_for) - xy: uint256[2] = AMM.withdraw(_for, 10**18) + ns: int256[2] = staticcall AMM.read_user_tick_numbers(_for) + xy: uint256[2] = extcall AMM.withdraw(_for, 10**18) debt: uint256 = 0 rate_mul: uint256 = 0 debt, rate_mul = self._debt(_for) @@ -1025,7 +913,7 @@ def repay_extended(callbacker: address, callback_args: DynArray[uint256,5], call 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) + log UserState(user=_for, collateral=0, debt=0, n1=0, n2=0, liquidation_discount=0) # Else - partial repayment -> deleverage, but only if we are not underwater else: @@ -1037,7 +925,7 @@ def repay_extended(callbacker: address, callback_args: DynArray[uint256,5], call # 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) + extcall AMM.deposit_range(_for, cb.collateral, n1, n2) liquidation_discount: uint256 = self.liquidation_discount self.liquidation_discounts[_for] = liquidation_discount @@ -1046,16 +934,16 @@ def repay_extended(callbacker: address, callback_args: DynArray[uint256,5], call 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) + log UserState(user=_for, collateral=cb.collateral, debt=debt, n1=n1, n2=n2, liquidation_discount=liquidation_discount) xy[1] -= cb.collateral # No need to check _health() because it's the _for # Common calls which we will do regardless of whether it's a full repay or not - log Repay(_for, xy[1], d_debt) + log Repay(user=_for, collateral_decrease=xy[1], loan_decrease=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.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 @@ -1076,22 +964,22 @@ def _health(user: address, debt: uint256, full: bool, liquidation_discount: uint """ 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 + health = unsafe_div(convert(staticcall AMM.get_x_down(user), int256) * health, convert(debt, int256)) - 10**18 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') +@nonreentrant 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 @@ -1102,7 +990,7 @@ 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) + ns: int256[2] = staticcall AMM.read_user_tick_numbers(user) debt: int256 = convert(self._debt(user)[0], int256) n: uint256 = N ld: int256 = 0 @@ -1119,19 +1007,19 @@ def health_calculator(user: address, d_collateral: int256, d_debt: int256, full: debt += d_debt assert debt > 0, "Non-positive debt" - active_band: int256 = AMM.active_band_with_skip() + active_band: int256 = staticcall AMM.active_band_with_skip() if ns[0] > active_band: # re-deposit - collateral = convert(AMM.get_sum_xy(user)[1], int256) + d_collateral + 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(AMM.get_x_down(user) * unsafe_mul(10**18, BORROWED_PRECISION), int256) + x_eff = convert(staticcall 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) + 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 @@ -1140,7 +1028,7 @@ def health_calculator(user: address, d_collateral: int256, d_debt: int256, full: if full: if n1 > active_band: # We are not in liquidation mode - p_diff: int256 = max(p0, convert(AMM.price_oracle(), int256)) - p0 + p_diff: int256 = max(p0, convert(staticcall AMM.price_oracle(), int256)) - p0 if p_diff > 0: health += unsafe_div(p_diff * collateral, debt) @@ -1158,6 +1046,7 @@ def _get_f_remove(frac: uint256, health_limit: uint256) -> uint256: 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""): @@ -1187,7 +1076,7 @@ def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint2 # 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 @@ -1229,14 +1118,14 @@ def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint2 self.transferFrom(BORROWED_TOKEN, AMM.address, msg.sender, unsafe_sub(xy[0], debt)) 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) + self.loan[user] = Loan(initial_debt=final_debt, rate_mul=rate_mul) + log Repay(user=user, collateral_decrease=xy[1], loan_decrease=debt) + log Liquidate(liquidator=msg.sender, user=user, collateral_received=xy[1], stablecoin_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 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 + 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 @@ -1244,7 +1133,7 @@ def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint2 @external -@nonreentrant('lock') +@nonreentrant def liquidate(user: address, min_x: uint256): """ @notice Perform a bad liquidation (or self-liquidation) of user if health is not good @@ -1257,7 +1146,7 @@ def liquidate(user: address, min_x: uint256): @external -@nonreentrant('lock') +@nonreentrant def liquidate_extended(user: address, min_x: uint256, frac: uint256, callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b""): """ @@ -1275,7 +1164,7 @@ def liquidate_extended(user: address, min_x: uint256, frac: uint256, @view @external -@nonreentrant('lock') +@nonreentrant def tokens_to_liquidate(user: address, frac: uint256 = 10 ** 18) -> uint256: """ @notice Calculate the amount of stablecoins to have in liquidator's wallet to liquidate a user @@ -1286,7 +1175,7 @@ def tokens_to_liquidate(user: address, frac: uint256 = 10 ** 18) -> uint256: 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) + stablecoins: uint256 = unsafe_div((staticcall 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) return unsafe_sub(max(debt, stablecoins), stablecoins) @@ -1294,7 +1183,7 @@ def tokens_to_liquidate(user: address, frac: uint256 = 10 ** 18) -> uint256: @view @external -@nonreentrant('lock') +@nonreentrant def health(user: address, full: bool = False) -> int256: """ @notice Returns position health normalized to 1e18 for the user. @@ -1305,7 +1194,7 @@ def health(user: address, full: bool = False) -> int256: @view @external -@nonreentrant('lock') +@nonreentrant def users_to_liquidate(_from: uint256=0, _limit: uint256=0) -> DynArray[Position, 1000]: """ @notice Returns a dynamic array of users who can be "hard-liquidated". @@ -1320,21 +1209,21 @@ def users_to_liquidate(_from: uint256=0, _limit: uint256=0) -> DynArray[Position limit = n_loans ix: uint256 = _from out: DynArray[Position, 1000] = [] - for i in range(10**6): + for i: uint256 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 - })) + xy: uint256[2] = staticcall AMM.get_sum_xy(user) + out.append(Position( + user=user, + x=xy[0], + y=xy[1], + debt=debt, + health=health + )) ix += 1 return out @@ -1346,34 +1235,34 @@ def amm_price() -> uint256: """ @notice Current price from the AMM """ - return AMM.get_p() + return staticcall AMM.get_p() @view @external -@nonreentrant('lock') +@nonreentrant 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])] + 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 -@nonreentrant('lock') +@nonreentrant 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) """ - xy: uint256[2] = AMM.get_sum_xy(user) - ns: int256[2] = AMM.read_user_tick_numbers(user) # ns[1] > ns[0] + 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)[0], convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256)] @@ -1384,25 +1273,25 @@ 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 """ - assert msg.sender == FACTORY.admin() + assert msg.sender == staticcall FACTORY.admin() assert fee <= MAX_FEE and fee >= MIN_FEE, "Fee" - AMM.set_fee(fee) + extcall AMM.set_fee(fee) -@nonreentrant('lock') +@nonreentrant @external def set_monetary_policy(monetary_policy: address): """ @notice Set monetary policy contract @param monetary_policy Address of the monetary policy contract """ - assert msg.sender == FACTORY.admin() + assert msg.sender == staticcall FACTORY.admin() self.monetary_policy = MonetaryPolicy(monetary_policy) - MonetaryPolicy(monetary_policy).rate_write() - log SetMonetaryPolicy(monetary_policy) + extcall MonetaryPolicy(monetary_policy).rate_write() + log SetMonetaryPolicy(monetary_policy=monetary_policy) -@nonreentrant('lock') +@nonreentrant @external def set_borrowing_discounts(loan_discount: uint256, liquidation_discount: uint256): """ @@ -1410,24 +1299,24 @@ def set_borrowing_discounts(loan_discount: uint256, liquidation_discount: uint25 @param loan_discount Discount which defines LTV @param liquidation_discount Discount where bad liquidation starts """ - assert msg.sender == FACTORY.admin() + assert msg.sender == staticcall FACTORY.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 SetBorrowingDiscounts(loan_discount=loan_discount, liquidation_discount=liquidation_discount) @external -@nonreentrant('lock') +@nonreentrant def set_callback(cb: address): """ @notice Set liquidity mining callback """ - assert msg.sender == FACTORY.admin() - AMM.set_callback(cb) - log SetLMCallback(cb) + assert msg.sender == staticcall FACTORY.admin() + extcall AMM.set_callback(cb) + log SetLMCallback(callback=cb) @external @@ -1436,15 +1325,15 @@ def admin_fees() -> uint256: """ @notice Calculate the amount of fees obtained from the interest """ - rate_mul: uint256 = AMM.get_rate_mul() + rate_mul: uint256 = staticcall AMM.get_rate_mul() loan: Loan = self._total_debt - loan.initial_debt = loan.initial_debt * rate_mul / loan.rate_mul + self.redeemed + 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) @external -@nonreentrant('lock') +@nonreentrant def collect_fees() -> uint256: """ @notice Collect the fees charged as interest. @@ -1452,12 +1341,12 @@ def collect_fees() -> uint256: 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() + _to: address = staticcall FACTORY.fee_receiver() # Borrowing-based fees - rate_mul: uint256 = AMM.get_rate_mul() + rate_mul: uint256 = staticcall AMM.get_rate_mul() loan: Loan = self._total_debt - loan.initial_debt = loan.initial_debt * rate_mul / loan.rate_mul + loan.initial_debt = loan.initial_debt * rate_mul // loan.rate_mul loan.rate_mul = rate_mul self._total_debt = loan @@ -1472,16 +1361,16 @@ def collect_fees() -> uint256: 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) + log CollectFees(amount=to_be_redeemed, new_supply=loan.initial_debt) return to_be_redeemed else: - log CollectFees(0, loan.initial_debt) + log CollectFees(amount=0, new_supply=loan.initial_debt) return 0 @external @view -@nonreentrant('lock') +@nonreentrant def check_lock() -> bool: return True @@ -1496,7 +1385,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 Approval(owner=msg.sender, spender=_spender, allow=_allow) @internal @@ -1512,4 +1401,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 SetExtraHealth(user=msg.sender, health=_value) From de5b3ec9927c180397f20c820b4dfdb82182c04e Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 29 Jul 2025 10:02:18 +0400 Subject: [PATCH 047/413] chore: save deprecated contracts --- contracts/deprecated/Controller.vy | 1536 +++++++++++++++++ .../deprecated/OneWayLendingFactory.vy | 435 +++++ .../deprecated/OneWayLendingFactoryL2.vy | 421 +++++ contracts/lending/deprecated/Vault.vy | 692 ++++++++ 4 files changed, 3084 insertions(+) create mode 100644 contracts/deprecated/Controller.vy create mode 100644 contracts/lending/deprecated/OneWayLendingFactory.vy create mode 100644 contracts/lending/deprecated/OneWayLendingFactoryL2.vy create mode 100644 contracts/lending/deprecated/Vault.vy diff --git a/contracts/deprecated/Controller.vy b/contracts/deprecated/Controller.vy new file mode 100644 index 00000000..02aa1960 --- /dev/null +++ b/contracts/deprecated/Controller.vy @@ -0,0 +1,1536 @@ +# @version 0.3.10 +# pragma optimize codesize +# pragma evm-version shanghai +""" +@title crvUSD Controller +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved +""" + +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) +MAX_LOAN_DISCOUNT: constant(uint256) = 5 * 10**17 +MIN_LIQUIDATION_DISCOUNT: constant(uint256) = 10**16 # Start liquidating when threshold reached +MAX_TICKS: constant(int256) = 50 +MAX_TICKS_UINT: constant(uint256) = 50 +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 + +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 + +minted: public(uint256) +redeemed: public(uint256) + +monetary_policy: public(MonetaryPolicy) +liquidation_discount: public(uint256) +loan_discount: public(uint256) + +COLLATERAL_TOKEN: immutable(ERC20) +COLLATERAL_PRECISION: immutable(uint256) + +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) + +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 + +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) + +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 + +approval: public(HashMap[address, HashMap[address, bool]]) +extra_health: public(HashMap[address, uint256]) + + +@external +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) + + self.monetary_policy = MonetaryPolicy(monetary_policy) + + 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()) + + COLLATERAL_TOKEN = _collateral_token + BORROWED_TOKEN = _borrowed_token + COLLATERAL_PRECISION = pow_mod256(10, 18 - _collateral_token.decimals()) + BORROWED_PRECISION = pow_mod256(10, 18 - _borrowed_token.decimals()) + + SQRT_BAND_RATIO = isqrt(unsafe_div(10**36 * _A, unsafe_sub(_A, 1))) + + assert _borrowed_token.approve(msg.sender, max_value(uint256), default_return_value=True) + + +@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 + + +@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 + + +@external +@pure +def factory() -> Factory: + """ + @notice Address of the factory + """ + return FACTORY + + +@external +@pure +def amm() -> LLAMMA: + """ + @notice Address of the AMM + """ + return AMM + + +@external +@pure +def collateral_token() -> ERC20: + """ + @notice Address of the collateral token + """ + return COLLATERAL_TOKEN + + +@external +@pure +def borrowed_token() -> ERC20: + """ + @notice Address of the borrowed token + """ + return BORROWED_TOKEN + + +@internal +def _save_rate(): + """ + @notice Save current rate + """ + rate: uint256 = min(self.monetary_policy.rate_write(), MAX_RATE) + AMM.set_rate(rate) + + +@external +@nonreentrant('lock') +def save_rate(): + """ + @notice Save current rate + """ + self._save_rate() + + +@internal +@view +def _debt(user: address) -> (uint256, uint256): + """ + @notice Get the value of debt and rate_mul and update the rate_mul counter + @param user User address + @return (debt, rate_mul) + """ + rate_mul: uint256 = AMM.get_rate_mul() + loan: Loan = self.loan[user] + if loan.initial_debt == 0: + return (0, rate_mul) + else: + # Let user repay 1 smallest decimal more so that the system doesn't lose on precision + # Use ceil div + debt: uint256 = loan.initial_debt * rate_mul + 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 + return (debt, rate_mul) + + +@external +@view +@nonreentrant('lock') +def debt(user: address) -> uint256: + """ + @notice Get the value of debt without changing the state + @param user User address + @return Value of debt + """ + return self._debt(user)[0] + + +@external +@view +@nonreentrant('lock') +def loan_exists(user: address) -> bool: + """ + @notice Check whether there is a loan of `user` in existence + """ + return self.loan[user].initial_debt > 0 + + +# No decorator because used in monetary policy +@external +@view +def total_debt() -> uint256: + """ + @notice Total debt of this controller + """ + rate_mul: uint256 = AMM.get_rate_mul() + loan: Loan = self._total_debt + return loan.initial_debt * rate_mul / loan.rate_mul + + +@internal +@pure +def get_y_effective(collateral: uint256, N: uint256, discount: uint256) -> uint256: + """ + @notice Intermediary method which calculates y_effective defined as x_effective / p_base, + however discounted by loan_discount. + x_effective is an amount which can be obtained from collateral when liquidating + @param collateral Amount of collateral to get the value for + @param N Number of bands the deposit is made into + @param discount Loan discount at 1e18 base (e.g. 1e18 == 100%) + @return y_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_effective * p_oracle_up(n1) + # d_y_effective = y / N / sqrt(A / (A - 1)) + # 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) + ), + unsafe_mul(SQRT_BAND_RATIO, N)) + y_effective: uint256 = d_y_effective + for i in range(1, MAX_TICKS_UINT): + if i == N: + break + d_y_effective = unsafe_div(d_y_effective * Aminus1, 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: + """ + @notice Calculate the upper band number for the deposit to sit in to support + the given debt. 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 + @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) + + # 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]) + # p_oracle_up(n1) = base_price * ((A - 1) / A)**n1 + + # We borrow up until min band touches p_oracle, + # or it touches non-empty bands which cannot be skipped. + # We calculate required n1 for given (collateral, debt), + # and if n1 corresponds to price_oracle being too high, or unreachable band + # - 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 + + # 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) + if n1 < 0: + 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" + + # 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" + + 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: + """ + @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 + + +@external +@view +@nonreentrant('lock') +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 + @param user User to calculate the value for (only necessary for nonzero extra_health) + @return Minimal collateral required + """ + # 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) + + +@external +@view +@nonreentrant('lock') +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. + @param collateral Amount of collateral (at its native precision) + @param debt Amount of requested debt + @param N Number of bands to deposit into + @param user User to calculate n1 for (only necessary for nonzero extra_health) + @return Upper band n1 (n1 <= n2) to deposit into. Signed integer + """ + return self._calculate_debt_n1(collateral, debt, N, user) + + +@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) + + +@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: + 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) + + # Callback + response: Bytes[64] = raw_call( + callbacker, + concat(callback_sig, _abi_encode(user, stablecoins, collateral, debt, callback_args, callback_bytes)), + max_outsize=64 + ) + data.stablecoins = 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) + + 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) + + +@external +@nonreentrant('lock') +def create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address = msg.sender): + """ + @notice Create loan + @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 _for Address to create the loan for + """ + 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) + + +@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) + + # Before callback + self.transfer(BORROWED_TOKEN, callbacker, debt) + + # 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._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) + + +@internal +def _add_collateral_borrow(d_collateral: uint256, d_debt: uint256, _for: address, remove_collateral: bool, + check_rounding: bool): + """ + @notice Internal method to borrow and add or remove collateral + @param d_collateral Amount of collateral to add + @param d_debt Amount of debt increase + @param _for Address to transfer tokens to + @param remove_collateral Remove collateral instead of adding + @param check_rounding Check that amount added is no less than the rounding error on the loan + """ + debt: uint256 = 0 + rate_mul: uint256 = 0 + debt, rate_mul = self._debt(_for) + assert debt > 0, "Loan doesn't exist" + 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" + if remove_collateral: + xy[1] -= d_collateral + else: + xy[1] += d_collateral + if check_rounding: + # We need d(x + p*y) > 1 wei. For that, we do an equivalent check (but with x2 for safety) + # 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 + 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}) + + liquidation_discount: uint256 = 0 + if _for == msg.sender: + liquidation_discount = self.liquidation_discount + self.liquidation_discounts[_for] = liquidation_discount + 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) + else: + log Borrow(_for, d_collateral, d_debt) + + log UserState(_for, xy[1], debt, n1, n2, liquidation_discount) + + +@external +@nonreentrant('lock') +def add_collateral(collateral: uint256, _for: address = msg.sender): + """ + @notice Add extra collateral to avoid bad liqidations + @param collateral Amount of collateral to add + @param _for Address to add collateral for + """ + if collateral == 0: + return + self._add_collateral_borrow(collateral, 0, _for, False, _for != msg.sender) + self.transferFrom(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 + @param collateral Amount of collateral to remove + @param _for Address to remove collateral for + """ + if collateral == 0: + return + assert self._check_approval(_for) + self._add_collateral_borrow(collateral, 0, _for, True, False) + self.transferFrom(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) + @param collateral Amount of collateral to add + @param debt Amount of stablecoin 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 + """ + if debt == 0: + return + assert self._check_approval(_for) + + # Before callback + self.transfer(BORROWED_TOKEN, callbacker, debt) + + # 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) + self._save_rate() + + +@internal +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 + self.loan_ix[_for] = 0 + if loan_ix < last_loan_ix: # Need to replace + last_loan: address = self.loans[last_loan_ix] + self.loans[loan_ix] = last_loan + self.loan_ix[last_loan] = loan_ix + 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) + + 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) + + 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) + + 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 + + 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 + + self._save_rate() + + +@external +@nonreentrant('lock') +def repay_extended(callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b"", _for: address = msg.sender): + """ + @notice Repay loan but get a stablecoin for that from callback (to deleverage) + @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 + """ + 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 + 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 + + 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 + + # No need to check _health() because it's the _for + + # 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 + + self._save_rate() + + +@internal +@view +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 + @param user User address to calculate health for + @param debt The amount of debt to calculate health for + @param full Whether to take into account the price difference above the highest user's band + @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 + + 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) + 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) + + return health + + +@external +@view +@nonreentrant('lock') +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 + @param d_collateral Change in collateral amount (signed) + @param d_debt Change in debt amount (signed) + @param full Whether it's a 'full' health or not + @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 + + +@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) + + 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""): + """ + @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 + @param callbacker Address of the callback contract + @param callback_args Extra arguments for the callback (up to 5) such as min_amount etc + """ + 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" + + final_debt: uint256 = debt + debt = unsafe_div(debt * frac + (10**18 - 1), 10**18) + assert debt > 0 + final_debt = unsafe_sub(final_debt, debt) + + # Withdraw sender's stablecoin 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] + + # 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) + + 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]) + # Request what's left from user + self.transferFrom(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 + # 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) + + else: + # Withdraw collateral + self.transferFrom(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)) + + 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) + if final_debt == 0: + log UserState(user, 0, 0, 0, 0, 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: + """ + @notice Calculate the amount of stablecoins 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 + """ + 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) + + return unsafe_sub(max(debt, stablecoins), stablecoins) + + +@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]) + + +@view +@external +@nonreentrant('lock') +def users_to_liquidate(_from: uint256=0, _limit: uint256=0) -> DynArray[Position, 1000]: + """ + @notice Returns a dynamic array of users who can be "hard-liquidated". + This method is designed for convenience of liquidation bots. + @param _from Loan index to start iteration from + @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 +@view +@external +def amm_price() -> uint256: + """ + @notice Current price from the AMM + """ + return 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])] + + +@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) + """ + 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)] + + +# AMM has nonreentrant decorator +@external +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 + """ + assert msg.sender == FACTORY.admin() + assert fee <= MAX_FEE and fee >= MIN_FEE, "Fee" + AMM.set_fee(fee) + + +@nonreentrant('lock') +@external +def set_monetary_policy(monetary_policy: address): + """ + @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) + + +@nonreentrant('lock') +@external +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() + 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) + + +@external +@nonreentrant('lock') +def set_callback(cb: address): + """ + @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) + + +@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() + + # 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 + + self._save_rate() + + # 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 + + +@external +@view +@nonreentrant('lock') +def check_lock() -> bool: + return True + + +# Allowance methods + +@external +def approve(_spender: address, _allow: bool): + """ + @notice Allow another address to borrow and repay for the user + @param _spender Address to whitelist for the action + @param _allow Whether to turn the approval on or off (no amounts) + """ + self.approval[msg.sender][_spender] = _allow + log Approval(msg.sender, _spender, _allow) + + +@internal +@view +def _check_approval(_for: address) -> bool: + return msg.sender == _for or self.approval[_for][msg.sender] + + +@external +def set_extra_health(_value: uint256): + """ + @notice Add a little bit more to loan_discount to start SL with health higher than usual + @param _value 1e18-based addition to loan_discount + """ + self.extra_health[msg.sender] = _value + log SetExtraHealth(msg.sender, _value) diff --git a/contracts/lending/deprecated/OneWayLendingFactory.vy b/contracts/lending/deprecated/OneWayLendingFactory.vy new file mode 100644 index 00000000..78634fcb --- /dev/null +++ b/contracts/lending/deprecated/OneWayLendingFactory.vy @@ -0,0 +1,435 @@ +# @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/deprecated/OneWayLendingFactoryL2.vy b/contracts/lending/deprecated/OneWayLendingFactoryL2.vy new file mode 100644 index 00000000..d2204626 --- /dev/null +++ b/contracts/lending/deprecated/OneWayLendingFactoryL2.vy @@ -0,0 +1,421 @@ +# @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/deprecated/Vault.vy b/contracts/lending/deprecated/Vault.vy new file mode 100644 index 00000000..a96403b5 --- /dev/null +++ b/contracts/lending/deprecated/Vault.vy @@ -0,0 +1,692 @@ +# @version 0.3.10 +""" +@title Vault +@notice ERC4626+ Vault for lending with crvUSD using LLAMMA algorithm +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved +""" +from vyper.interfaces import ERC20 as ERC20Spec +from vyper.interfaces import ERC20Detailed + + +implements: ERC20Spec +implements: ERC20Detailed + + +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 + +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 +MIN_ASSETS: constant(uint256) = 10000 + +borrowed_token: public(ERC20) +collateral_token: public(ERC20) + +price_oracle: public(PriceOracle) +amm: public(AMM) +controller: public(Controller) +factory: public(Factory) + +maxSupply: public(uint256) + + +# ERC20 publics + +decimals: public(constant(uint8)) = 18 +name: public(String[64]) +symbol: public(String[34]) + +NAME_PREFIX: constant(String[16]) = 'Curve Vault for ' +SYMBOL_PREFIX: constant(String[2]) = 'cv' + +allowance: public(HashMap[address, HashMap[address, uint256]]) +balanceOf: public(HashMap[address, uint256]) +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) + + +@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): + """ + @notice Initializer for vaults + @param amm_impl AMM implementation (blueprint) + @param controller_impl Controller implementation (blueprint) + @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 + """ + assert self.borrowed_token.address == empty(address) + + self.borrowed_token = borrowed_token + 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) + + # ERC20 set up + self.precision = borrowed_precision + borrowed_symbol: String[32] = borrowed_token.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 + self.symbol = concat(SYMBOL_PREFIX, borrowed_symbol) + + 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 + self.maxSupply = max_supply + log SetMaxSupply(max_supply) + + +@external +@view +@nonreentrant('lock') +def borrow_apr() -> uint256: + """ + @notice Borrow APR (annualized and 1e18-based) + """ + return self.amm.rate() * (365 * 86400) + + +@external +@view +@nonreentrant('lock') +def lend_apr() -> uint256: + """ + @notice Lending APR (annualized and 1e18-based) + """ + debt: uint256 = self.controller.total_debt() + if debt == 0: + return 0 + else: + return self.amm.rate() * (365 * 86400) * debt / self._total_assets() + + +@external +@view +def asset() -> ERC20: + """ + @notice Asset which is the same as borrowed_token + """ + return self.borrowed_token + + +@internal +@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() + + +@external +@view +@nonreentrant('lock') +def totalAssets() -> uint256: + """ + @notice Total assets which can be lent out or be in reserve + """ + return self._total_assets() + + +@internal +@view +def _convert_to_shares(assets: uint256, is_floor: bool = True, + _total_assets: uint256 = max_value(uint256)) -> uint256: + total_assets: uint256 = _total_assets + if total_assets == max_value(uint256): + total_assets = self._total_assets() + precision: uint256 = self.precision + numerator: uint256 = (self.totalSupply + DEAD_SHARES) * assets * precision + denominator: uint256 = (total_assets * precision + 1) + if is_floor: + return numerator / denominator + else: + return (numerator + denominator - 1) / denominator + + +@internal +@view +def _convert_to_assets(shares: uint256, is_floor: bool = True, + _total_assets: uint256 = max_value(uint256)) -> uint256: + total_assets: uint256 = _total_assets + if total_assets == max_value(uint256): + total_assets = self._total_assets() + precision: uint256 = self.precision + numerator: uint256 = shares * (total_assets * precision + 1) + denominator: uint256 = (self.totalSupply + DEAD_SHARES) * precision + if is_floor: + return numerator / denominator + else: + return (numerator + denominator - 1) / denominator + + +@external +@view +@nonreentrant('lock') +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 + 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 + else: + pps = (numerator + denominator - 1) / denominator + assert pps > 0 + return pps + + +@external +@view +@nonreentrant('lock') +def convertToShares(assets: uint256) -> uint256: + """ + @notice Returns the amount of shares which the Vault would exchange for the given amount of shares provided + """ + return self._convert_to_shares(assets) + + +@external +@view +@nonreentrant('lock') +def convertToAssets(shares: uint256) -> uint256: + """ + @notice Returns the amount of assets that the Vault would exchange for the amount of shares provided + """ + return self._convert_to_assets(shares) + + +@external +@view +def maxDeposit(receiver: address) -> uint256: + """ + @notice Maximum amount of assets which a given user can deposit (inf) + """ + max_supply: uint256 = self.maxSupply + if max_supply == max_value(uint256): + return max_supply + else: + assets: uint256 = self._total_assets() + return max(max_supply, assets) - assets + + +@external +@view +@nonreentrant('lock') +def previewDeposit(assets: uint256) -> uint256: + """ + @notice Returns the amount of shares which can be obtained upon depositing assets + """ + return self._convert_to_shares(assets) + + +@external +@nonreentrant('lock') +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 + 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) + self._mint(receiver, to_mint) + controller.save_rate() + log Deposit(msg.sender, receiver, assets, to_mint) + return to_mint + + +@external +@view +def maxMint(receiver: address) -> uint256: + """ + @notice Return maximum amount of shares which a given user can mint (inf) + """ + max_supply: uint256 = self.maxSupply + if max_supply == max_value(uint256): + return max_supply + else: + assets: uint256 = self._total_assets() + return self._convert_to_shares(max(max_supply, assets) - assets) + + +@external +@view +@nonreentrant('lock') +def previewMint(shares: uint256) -> uint256: + """ + @notice Calculate the amount of assets which is needed to exactly mint the given amount of shares + """ + return self._convert_to_assets(shares, False) + + +@external +@nonreentrant('lock') +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 + 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) + self._mint(receiver, shares) + controller.save_rate() + log Deposit(msg.sender, receiver, assets, shares) + return assets + + +@external +@view +@nonreentrant('lock') +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)) + + +@external +@view +@nonreentrant('lock') +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) + return self._convert_to_shares(assets, False) + + +@external +@nonreentrant('lock') +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 + @param assets Amount of assets to withdraw + @param receiver Receiver of the assets (optional, sender if not specified) + @param owner Owner who's shares the caller takes. Only can take those if owner gave the approval to the sender. Optional + """ + total_assets: uint256 = self._total_assets() + assert total_assets - assets >= MIN_ASSETS or total_assets == assets, "Need more assets" + shares: uint256 = self._convert_to_shares(assets, False, total_assets) + if owner != msg.sender: + allowance: uint256 = self.allowance[owner][msg.sender] + if allowance != max_value(uint256): + self._approve(owner, msg.sender, allowance - shares) + + controller: Controller = 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) + return shares + + +@external +@view +@nonreentrant('lock') +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.balanceOf[owner]) + + +@external +@view +@nonreentrant('lock') +def previewRedeem(shares: uint256) -> uint256: + """ + @notice Calculate the amount of assets which can be obtained by redeeming the given amount of shares + """ + if self.totalSupply == 0: + assert shares == 0 + return 0 + + else: + assets_to_redeem: uint256 = self._convert_to_assets(shares) + assert assets_to_redeem <= self.borrowed_token.balanceOf(self.controller.address) + return assets_to_redeem + + +@external +@nonreentrant('lock') +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 + @param shares Amount of shares to burn + @param receiver Optional receiver of the assets + @param owner Optional owner of the shares. Can only redeem if owner gave approval to the sender + """ + if owner != msg.sender: + allowance: uint256 = self.allowance[owner][msg.sender] + if allowance != max_value(uint256): + self._approve(owner, msg.sender, allowance - shares) + + total_assets: uint256 = self._total_assets() + assets_to_redeem: uint256 = self._convert_to_assets(shares, True, total_assets) + if total_assets - assets_to_redeem < MIN_ASSETS: + if shares == self.totalSupply: + # This is the last withdrawal, so we can take everything + assets_to_redeem = total_assets + 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) + return assets_to_redeem + + +# ERC20 methods + +@internal +def _approve(_owner: address, _spender: address, _value: uint256): + self.allowance[_owner][_spender] = _value + + log Approval(_owner, _spender, _value) + + +@internal +def _burn(_from: address, _value: uint256): + self.balanceOf[_from] -= _value + self.totalSupply -= _value + + log Transfer(_from, empty(address), _value) + + +@internal +def _mint(_to: address, _value: uint256): + self.balanceOf[_to] += _value + self.totalSupply += _value + + log Transfer(empty(address), _to, _value) + + +@internal +def _transfer(_from: address, _to: address, _value: uint256): + assert _to not in [self, empty(address)] + + self.balanceOf[_from] -= _value + self.balanceOf[_to] += _value + + log Transfer(_from, _to, _value) + + +@external +def transferFrom(_from: address, _to: address, _value: uint256) -> bool: + """ + @notice Transfer tokens from one account to another. + @dev The caller needs to have an allowance from account `_from` greater than or + equal to the value being transferred. An allowance equal to the uint256 type's + maximum, is considered infinite and does not decrease. + @param _from The account which tokens will be spent from. + @param _to The account which tokens will be sent to. + @param _value The amount of tokens to be transferred. + """ + allowance: uint256 = self.allowance[_from][msg.sender] + if allowance != max_value(uint256): + self._approve(_from, msg.sender, allowance - _value) + + self._transfer(_from, _to, _value) + return True + + +@external +def transfer(_to: address, _value: uint256) -> bool: + """ + @notice Transfer tokens to `_to`. + @param _to The account to transfer tokens to. + @param _value The amount of tokens to transfer. + """ + self._transfer(msg.sender, _to, _value) + return True + + +@external +def approve(_spender: address, _value: uint256) -> bool: + """ + @notice Allow `_spender` to transfer up to `_value` amount of tokens from the caller's account. + @dev Non-zero to non-zero approvals are allowed, but should be used cautiously. The methods + increaseAllowance + decreaseAllowance are available to prevent any front-running that + may occur. + @param _spender The account permitted to spend up to `_value` amount of caller's funds. + @param _value The amount of tokens `_spender` is allowed to spend. + """ + self._approve(msg.sender, _spender, _value) + return True + + +@external +def increaseAllowance(_spender: address, _add_value: uint256) -> bool: + """ + @notice Increase the allowance granted to `_spender`. + @dev This function will never overflow, and instead will bound + allowance to MAX_UINT256. This has the potential to grant an + infinite approval. + @param _spender The account to increase the allowance of. + @param _add_value The amount to increase the allowance by. + """ + cached_allowance: uint256 = self.allowance[msg.sender][_spender] + allowance: uint256 = unsafe_add(cached_allowance, _add_value) + + # check for an overflow + if allowance < cached_allowance: + allowance = max_value(uint256) + + if allowance != cached_allowance: + self._approve(msg.sender, _spender, allowance) + + return True + + +@external +def decreaseAllowance(_spender: address, _sub_value: uint256) -> bool: + """ + @notice Decrease the allowance granted to `_spender`. + @dev This function will never underflow, and instead will bound + allowance to 0. + @param _spender The account to decrease the allowance of. + @param _sub_value The amount to decrease the allowance by. + """ + cached_allowance: uint256 = self.allowance[msg.sender][_spender] + allowance: uint256 = unsafe_sub(cached_allowance, _sub_value) + + # check for an underflow + if cached_allowance < allowance: + allowance = 0 + + if allowance != cached_allowance: + self._approve(msg.sender, _spender, allowance) + + return True + + +@external +@view +def admin() -> address: + return self.factory.admin() From 33ba5ce748b2d901005ac67b92cfdf908273f0cb Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 29 Jul 2025 10:03:11 +0400 Subject: [PATCH 048/413] chore: move TwoWayLendingFactory to deprecated --- contracts/lending/{ => deprecated}/TwoWayLendingFactory.vy | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename contracts/lending/{ => deprecated}/TwoWayLendingFactory.vy (100%) diff --git a/contracts/lending/TwoWayLendingFactory.vy b/contracts/lending/deprecated/TwoWayLendingFactory.vy similarity index 100% rename from contracts/lending/TwoWayLendingFactory.vy rename to contracts/lending/deprecated/TwoWayLendingFactory.vy From 5fb87c1467f6e7df2fa9c19cc7f5bff074aba176 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 29 Jul 2025 10:16:04 +0400 Subject: [PATCH 049/413] feat: add fee_receiver to lending factories --- contracts/lending/OneWayLendingFactory.vy | 24 +++++++++++++++++++-- contracts/lending/OneWayLendingFactoryL2.vy | 24 +++++++++++++++++++-- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/contracts/lending/OneWayLendingFactory.vy b/contracts/lending/OneWayLendingFactory.vy index 78634fcb..a2cf22b2 100644 --- a/contracts/lending/OneWayLendingFactory.vy +++ b/contracts/lending/OneWayLendingFactory.vy @@ -2,7 +2,6 @@ """ @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 """ @@ -57,6 +56,9 @@ event SetDefaultRates: event SetAdmin: admin: address +event SetFeeReceiver: + fee_receiver: address + event NewVault: id: indexed(uint256) collateral_token: indexed(address) @@ -95,6 +97,7 @@ max_default_borrow_rate: public(uint256) # Admin is supposed to be the DAO admin: public(address) +fee_receiver: public(address) # Vaults can only be created but not removed vaults: public(Vault[10**18]) @@ -119,7 +122,9 @@ def __init__( pool_price_oracle: address, monetary_policy: address, gauge: address, - admin: address): + admin: address, + fee_receiver: 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 @@ -129,6 +134,7 @@ def __init__( @param monetary_policy Address for implementation of monetary policy @param gauge Address for gauge implementation @param admin Admin address (DAO) + @param fee_receiver Receiver of interest and admin fees """ STABLECOIN = stablecoin self.amm_impl = amm @@ -142,6 +148,7 @@ def __init__( self.max_default_borrow_rate = 50 * 10**16 / (365 * 86400) self.admin = admin + self.fee_receiver = fee_receiver @internal @@ -428,6 +435,19 @@ def set_admin(admin: address): log SetAdmin(admin) +@external +@nonreentrant('lock') +def set_fee_receiver(fee_receiver: address): + """ + @notice Set fee receiver who earns interest (DAO) + @param fee_receiver Address of the receiver + """ + assert msg.sender == self.admin + assert fee_receiver != empty(address) + self.fee_receiver = fee_receiver + log SetFeeReceiver(fee_receiver) + + @external @view def coins(vault_id: uint256) -> address[2]: diff --git a/contracts/lending/OneWayLendingFactoryL2.vy b/contracts/lending/OneWayLendingFactoryL2.vy index d2204626..1c3077d7 100644 --- a/contracts/lending/OneWayLendingFactoryL2.vy +++ b/contracts/lending/OneWayLendingFactoryL2.vy @@ -2,7 +2,6 @@ """ @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 @@ -61,6 +60,9 @@ event SetDefaultRates: event SetAdmin: admin: address +event SetFeeReceiver: + fee_receiver: address + event NewVault: id: indexed(uint256) collateral_token: indexed(address) @@ -94,6 +96,7 @@ max_default_borrow_rate: public(uint256) # Admin is supposed to be the DAO admin: public(address) +fee_receiver: public(address) # Vaults can only be created but not removed vaults: public(Vault[10**18]) @@ -118,7 +121,9 @@ def __init__( pool_price_oracle: address, monetary_policy: address, gauge_factory: GaugeFactory, - admin: address): + admin: address, + fee_receiver: 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 @@ -128,6 +133,7 @@ def __init__( @param monetary_policy Address for implementation of monetary policy @param gauge_factory Address for gauge factory on this L2 @param admin Admin address (DAO) + @param fee_receiver Receiver of interest and admin fees """ STABLECOIN = stablecoin self.amm_impl = amm @@ -141,6 +147,7 @@ def __init__( self.max_default_borrow_rate = 50 * 10**16 / (365 * 86400) self.admin = admin + self.fee_receiver = fee_receiver @internal @@ -414,6 +421,19 @@ def set_admin(admin: address): log SetAdmin(admin) +@external +@nonreentrant('lock') +def set_fee_receiver(fee_receiver: address): + """ + @notice Set fee receiver who earns interest (DAO) + @param fee_receiver Address of the receiver + """ + assert msg.sender == self.admin + assert fee_receiver != empty(address) + self.fee_receiver = fee_receiver + log SetFeeReceiver(fee_receiver) + + @external @view def coins(vault_id: uint256) -> address[2]: From 536842e7c70c90d82ac2130bac29ab1b9cb6e6dc Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 29 Jul 2025 15:18:40 +0400 Subject: [PATCH 050/413] refactor: deploy Controller and AMM directly from lending factory --- contracts/lending/Controller.vy | 63 +++++++---- contracts/lending/OneWayLendingFactory.vy | 128 +++++++++++++++------- contracts/lending/Vault.vy | 108 ++---------------- 3 files changed, 134 insertions(+), 165 deletions(-) diff --git a/contracts/lending/Controller.vy b/contracts/lending/Controller.vy index d4354e8e..a9918b8a 100644 --- a/contracts/lending/Controller.vy +++ b/contracts/lending/Controller.vy @@ -40,13 +40,12 @@ interface MonetaryPolicy: def rate_write() -> uint256: nonpayable interface Vault: - def admin() -> address: view - def fee_receiver() -> address: view - def borrowed_token() -> address: view - def collateral_token() -> address: view def deposited() -> uint256: view def withdrawn() -> uint256: view +interface Factory: + def admin() -> address: view + def fee_receiver() -> address: view event UserState: user: indexed(address) @@ -121,7 +120,10 @@ struct CallbackData: collateral: uint256 +FACTORY: immutable(Factory) VAULT: immutable(Vault) +AMM: immutable(LLAMMA) + MAX_LOAN_DISCOUNT: constant(uint256) = 5 * 10**17 MIN_LIQUIDATION_DISCOUNT: constant(uint256) = 10**16 # Start liquidating when threshold reached MAX_TICKS: constant(int256) = 50 @@ -156,7 +158,6 @@ COLLATERAL_PRECISION: immutable(uint256) 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)) @@ -180,21 +181,28 @@ admin_fee: public(uint256) @external def __init__( + vault: address, + amm: address, + borrowed_token: address, collateral_token: address, monetary_policy: address, loan_discount: uint256, liquidation_discount: uint256, - amm: address): +): """ @notice Controller constructor deployed by the factory from blueprint + @param vault Vault address (Already deployed as proxy) + @param amm AMM address (Already deployed from blueprint) + @param borrowed_token Token to use for borrowed @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(msg.sender) + FACTORY = Factory(msg.sender) + VAULT = Vault(vault) + AMM = LLAMMA(amm) self.monetary_policy = MonetaryPolicy(monetary_policy) @@ -202,19 +210,17 @@ def __init__( 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))) + A = amm.A() + Aminus1 = unsafe_sub(A, 1) + LOGN_A_RATIO = self.wad_ln(unsafe_div(A * 10**18, unsafe_sub(A, 1))) MAX_AMM_FEE = min(unsafe_div(10**18 * MIN_TICKS, A), 10**17) - COLLATERAL_TOKEN = ERC20(Vault(msg.sender).collateral_token()) - BORROWED_TOKEN = ERC20(Vault(msg.sender).borrowed_token()) + COLLATERAL_TOKEN = ERC20(collateral_token) + BORROWED_TOKEN = ERC20(borrowed_token) COLLATERAL_PRECISION = pow_mod256(10, 18 - COLLATERAL_TOKEN.decimals()) BORROWED_PRECISION = pow_mod256(10, 18 - BORROWED_TOKEN.decimals()) - SQRT_BAND_RATIO = isqrt(unsafe_div(10**36 * _A, unsafe_sub(_A, 1))) + SQRT_BAND_RATIO = isqrt(unsafe_div(10**36 * A, unsafe_sub(A, 1))) assert BORROWED_TOKEN.approve(msg.sender, max_value(uint256), default_return_value=True) @@ -335,10 +341,19 @@ def wad_ln(x: uint256) -> int256: @external @pure -def factory() -> Vault: +def factory() -> Factory: """ @notice Address of the factory """ + return FACTORY + + +@external +@pure +def vault() -> Vault: + """ + @notice Address of the vault + """ return VAULT @@ -1323,7 +1338,7 @@ 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_AMM_FEE """ - assert msg.sender == VAULT.admin() + assert msg.sender == FACTORY.admin() assert fee <= MAX_AMM_FEE and fee >= MIN_AMM_FEE, "Fee" AMM.set_fee(fee) @@ -1335,7 +1350,7 @@ def set_monetary_policy(monetary_policy: address): @notice Set monetary policy contract @param monetary_policy Address of the monetary policy contract """ - assert msg.sender == VAULT.admin() + assert msg.sender == FACTORY.admin() self.monetary_policy = MonetaryPolicy(monetary_policy) MonetaryPolicy(monetary_policy).rate_write() log SetMonetaryPolicy(monetary_policy) @@ -1349,7 +1364,7 @@ def set_borrowing_discounts(loan_discount: uint256, liquidation_discount: uint25 @param loan_discount Discount which defines LTV @param liquidation_discount Discount where bad liquidation starts """ - assert msg.sender == VAULT.admin() + assert msg.sender == FACTORY.admin() assert loan_discount > liquidation_discount assert liquidation_discount >= MIN_LIQUIDATION_DISCOUNT assert loan_discount <= MAX_LOAN_DISCOUNT @@ -1364,7 +1379,7 @@ def set_callback(cb: address): """ @notice Set liquidity mining callback """ - assert msg.sender == VAULT.admin() + assert msg.sender == FACTORY.admin() AMM.set_callback(cb) log SetLMCallback(cb) @@ -1377,7 +1392,7 @@ def set_borrow_cap(_borrow_cap: uint256): @dev Only callable by the factory admin @param _borrow_cap New borrow cap in units of borrowed_token """ - assert msg.sender == VAULT.admin() + assert msg.sender == FACTORY.admin() self.borrow_cap = _borrow_cap log SetBorrowCap(_borrow_cap) @@ -1387,7 +1402,7 @@ def set_admin_fee(admin_fee: uint256): """ @param admin_fee The fee which should be no higher than MAX_ADMIN_FEE """ - assert msg.sender == VAULT.admin() + assert msg.sender == FACTORY.admin() assert admin_fee <= MAX_ADMIN_FEE, "admin_fee is higher than MAX_ADMIN_FEE" self.admin_fee = admin_fee @@ -1408,7 +1423,7 @@ def collect_fees() -> uint256: """ @notice Collect the fees charged as a fraction of interest. """ - _to: address = VAULT.fee_receiver() + _to: address = FACTORY.fee_receiver() # Borrowing-based fees rate_mul: uint256 = AMM.get_rate_mul() diff --git a/contracts/lending/OneWayLendingFactory.vy b/contracts/lending/OneWayLendingFactory.vy index a2cf22b2..58e7789a 100644 --- a/contracts/lending/OneWayLendingFactory.vy +++ b/contracts/lending/OneWayLendingFactory.vy @@ -6,19 +6,11 @@ @license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved """ +from vyper.interfaces import ERC20Detailed as 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 initialize(amm: address, controller: address, borrowed_token: address): nonpayable def amm() -> address: view def controller() -> address: view def borrowed_token() -> address: view @@ -30,16 +22,16 @@ 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 + def set_admin(_admin: address): nonpayable interface Pool: def price_oracle(i: uint256 = 0) -> uint256: view # Universal method! def coins(i: uint256) -> address: view +interface PriceOracle: + def price() -> uint256: view + def price_w() -> uint256: nonpayable + event SetImplementations: amm: address @@ -80,7 +72,12 @@ STABLECOIN: public(immutable(address)) # 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% - +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_impl: public(address) @@ -151,6 +148,35 @@ def __init__( self.fee_receiver = fee_receiver +@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) + + @internal def _create( borrowed_token: address, @@ -163,13 +189,18 @@ def _create( name: String[64], min_borrow_rate: uint256, max_borrow_rate: uint256 - ) -> Vault: + ) -> address[3]: """ @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)) + 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" min_rate: uint256 = self.min_default_borrow_rate max_rate: uint256 = self.max_default_borrow_rate @@ -181,19 +212,32 @@ def _create( 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, + A_ratio: uint256 = 10**18 * A / (A - 1) + p: uint256 = PriceOracle(price_oracle).price() # This also validates price oracle ABI + assert p > 0 + assert PriceOracle(price_oracle).price_w() == p + + vault: Vault = Vault(create_minimal_proxy_to(self.vault_impl)) + amm: address = create_from_blueprint( + self.amm_impl, + borrowed_token, 10**(18 - ERC20(borrowed_token).decimals()), + collateral_token, 10**(18 - ERC20(collateral_token).decimals()), + A, isqrt(A_ratio * 10**18), self.ln_int(A_ratio), + p, fee, convert(0, uint256), price_oracle, + code_offset=3) + controller: address = create_from_blueprint( + self.controller_impl, + vault, amm, borrowed_token, collateral_token, - A, fee, - price_oracle, - monetary_policy, - loan_discount, liquidation_discount - ) + monetary_policy, loan_discount, liquidation_discount, + code_offset=3) + AMM(amm).set_admin(controller) + + vault.initialize(amm, controller, borrowed_token) market_count: uint256 = self.market_count - log NewVault(market_count, collateral_token, borrowed_token, vault.address, controller, amm, price_oracle, monetary_policy) + 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 @@ -208,7 +252,7 @@ def _create( self.token_to_vaults[token][market_count] = vault self.token_market_count[token] = market_count + 1 - return vault + return [vault.address, controller, amm] @external @@ -225,7 +269,7 @@ def create( min_borrow_rate: uint256 = 0, max_borrow_rate: uint256 = 0, supply_limit: uint256 = max_value(uint256) - ) -> Vault: + ) -> address[3]: """ @notice Creation of the vault using user-supplied price oracle contract @param borrowed_token Token which is being borrowed @@ -240,11 +284,12 @@ def create( @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, + res: address[3] = 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 + Vault(res[0]).set_max_supply(supply_limit) + + return res @external @@ -261,7 +306,7 @@ def create_from_pool( min_borrow_rate: uint256 = 0, max_borrow_rate: uint256 = 0, supply_limit: uint256 = max_value(uint256) - ) -> Vault: + ) -> address[3]: """ @notice Creation of the vault using existing oraclized Curve pool as a price oracle @param borrowed_token Token which is being borrowed @@ -301,11 +346,14 @@ def create_from_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) + res: address[3] = 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 + Vault(res[0]).set_max_supply(supply_limit) + + return res @view diff --git a/contracts/lending/Vault.vy b/contracts/lending/Vault.vy index f5928a88..6c93dea2 100644 --- a/contracts/lending/Vault.vy +++ b/contracts/lending/Vault.vy @@ -22,20 +22,14 @@ interface ERC20: 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 borrowed_balance() -> 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 @@ -70,16 +64,6 @@ event Withdraw: 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 @@ -88,9 +72,7 @@ DEAD_SHARES: constant(uint256) = 1000 MIN_ASSETS: constant(uint256) = 10000 borrowed_token: public(ERC20) -collateral_token: public(ERC20) -price_oracle: public(PriceOracle) amm: public(AMM) controller: public(Controller) factory: public(Factory) @@ -100,7 +82,6 @@ maxSupply: public(uint256) deposited: public(uint256) # cumulative amount of assets ever deposited withdrawn: public(uint256) # cumulative amount of assets ever withdrawn - # ERC20 publics decimals: public(constant(uint8)) = 18 @@ -128,97 +109,26 @@ def __init__(): 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) - - @external def initialize( - amm_impl: address, - controller_impl: address, + amm: AMM, + controller: Controller, 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): + ): """ @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 """ assert self.borrowed_token.address == empty(address) self.borrowed_token = borrowed_token - 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.amm = amm + self.controller = controller # ERC20 set up self.precision = borrowed_precision @@ -230,10 +140,6 @@ 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): From e0b606374d1a203d94c0d41d00e215883533624e Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 29 Jul 2025 15:26:15 +0400 Subject: [PATCH 051/413] refactor: remove gauges stuff from OneWayLendingFactory --- contracts/lending/OneWayLendingFactory.vy | 44 ++--------------------- 1 file changed, 2 insertions(+), 42 deletions(-) diff --git a/contracts/lending/OneWayLendingFactory.vy b/contracts/lending/OneWayLendingFactory.vy index 58e7789a..9daddcd2 100644 --- a/contracts/lending/OneWayLendingFactory.vy +++ b/contracts/lending/OneWayLendingFactory.vy @@ -39,7 +39,6 @@ event SetImplementations: vault: address price_oracle: address monetary_policy: address - gauge: address event SetDefaultRates: min_rate: uint256 @@ -61,10 +60,6 @@ event NewVault: price_oracle: address monetary_policy: address -event LiquidityGaugeDeployed: - vault: address - gauge: address - STABLECOIN: public(immutable(address)) @@ -85,7 +80,6 @@ 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 @@ -106,7 +100,6 @@ market_count: public(uint256) 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]]) @@ -118,7 +111,6 @@ def __init__( vault: address, pool_price_oracle: address, monetary_policy: address, - gauge: address, admin: address, fee_receiver: address, ): @@ -129,7 +121,6 @@ def __init__( @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) @param fee_receiver Receiver of interest and admin fees """ @@ -139,7 +130,6 @@ def __init__( 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) @@ -392,37 +382,10 @@ 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): + pool_price_oracle: address, monetary_policy: address): """ @notice Set new implementations (blueprints) for controller, amm, vault, pool price oracle and monetary polcy. Doesn't change existing ones @@ -431,7 +394,6 @@ def set_implementations(controller: address, amm: address, vault: address, @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 @@ -445,10 +407,8 @@ def set_implementations(controller: address, amm: address, vault: 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) + log SetImplementations(amm, controller, vault, pool_price_oracle, monetary_policy) @external From 30bf1dd6e2ac782f5a9f7227d379e6736d646da1 Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 29 Jul 2025 13:30:52 +0200 Subject: [PATCH 052/413] chore: bump lending controller to Vyper 0.4.1 --- .gitignore | 1 + contracts/lending/Controller.vy | 444 ++++++++++++-------------------- 2 files changed, 167 insertions(+), 278 deletions(-) diff --git a/.gitignore b/.gitignore index eb5b62d8..e5208a48 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ venv /scripts/networks.py /brownie-deploy-emas/build .coverage +.claude diff --git a/contracts/lending/Controller.vy b/contracts/lending/Controller.vy index d4354e8e..c321d22d 100644 --- a/contracts/lending/Controller.vy +++ b/contracts/lending/Controller.vy @@ -1,4 +1,4 @@ -# @version 0.3.10 +# pragma version 0.4.1 # pragma optimize codesize # pragma evm-version shanghai """ @@ -7,6 +7,11 @@ @license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved """ +from snekmate.utils import math +from ethereum.ercs import IERC20 +from ethereum.ercs import IERC20Detailed + + interface LLAMMA: def A() -> uint256: view def get_p() -> uint256: view @@ -30,12 +35,6 @@ interface LLAMMA: 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 - interface MonetaryPolicy: def rate_write() -> uint256: nonpayable @@ -150,10 +149,10 @@ monetary_policy: public(MonetaryPolicy) liquidation_discount: public(uint256) loan_discount: public(uint256) -COLLATERAL_TOKEN: immutable(ERC20) +COLLATERAL_TOKEN: immutable(IERC20) COLLATERAL_PRECISION: immutable(uint256) -BORROWED_TOKEN: immutable(ERC20) +BORROWED_TOKEN: immutable(IERC20) BORROWED_PRECISION: immutable(uint256) AMM: immutable(LLAMMA) @@ -178,7 +177,7 @@ borrow_cap: public(uint256) admin_fee: public(uint256) -@external +@deploy def __init__( collateral_token: address, monetary_policy: address, @@ -203,147 +202,35 @@ def __init__( self._total_debt.rate_mul = 10**18 AMM = LLAMMA(amm) - _A: uint256 = LLAMMA(amm).A() + _A: uint256 = staticcall 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_AMM_FEE = min(unsafe_div(10**18 * MIN_TICKS, A), 10**17) + LOGN_A_RATIO = math._wad_ln(convert(unsafe_div(_A * 10**18, unsafe_sub(_A, 1)), int256)) + MAX_AMM_FEE = min(unsafe_div(10**18 * MIN_TICKS_UINT, A), 10**17) - COLLATERAL_TOKEN = ERC20(Vault(msg.sender).collateral_token()) - BORROWED_TOKEN = ERC20(Vault(msg.sender).borrowed_token()) - COLLATERAL_PRECISION = pow_mod256(10, 18 - COLLATERAL_TOKEN.decimals()) - BORROWED_PRECISION = pow_mod256(10, 18 - BORROWED_TOKEN.decimals()) + COLLATERAL_TOKEN = IERC20(staticcall Vault(msg.sender).collateral_token()) + collateral_decimals: uint256 = convert(staticcall IERC20Detailed(COLLATERAL_TOKEN.address).decimals(), uint256) + COLLATERAL_PRECISION = pow_mod256(10, 18 - collateral_decimals) + BORROWED_TOKEN = IERC20(staticcall Vault(msg.sender).borrowed_token()) + borrowed_decimals: uint256 = convert(staticcall IERC20Detailed(BORROWED_TOKEN.address).decimals(), uint256) + BORROWED_PRECISION = pow_mod256(10, 18 - borrowed_decimals) SQRT_BAND_RATIO = isqrt(unsafe_div(10**36 * _A, unsafe_sub(_A, 1))) - assert BORROWED_TOKEN.approve(msg.sender, max_value(uint256), default_return_value=True) - - -@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 - - -@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 + assert extcall BORROWED_TOKEN.approve(msg.sender, max_value(uint256), default_return_value=True) @external -@pure -def factory() -> Vault: +@view +def factory() -> address: """ @notice Address of the factory """ - return VAULT + return VAULT.address @external -@pure +@view def amm() -> LLAMMA: """ @notice Address of the AMM @@ -352,8 +239,8 @@ def amm() -> LLAMMA: @external -@pure -def collateral_token() -> ERC20: +@view +def collateral_token() -> IERC20: """ @notice Address of the collateral token """ @@ -361,8 +248,8 @@ def collateral_token() -> ERC20: @external -@pure -def borrowed_token() -> ERC20: +@view +def borrowed_token() -> IERC20: """ @notice Address of the borrowed token """ @@ -374,12 +261,12 @@ 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') +@nonreentrant def save_rate(): """ @notice Save current rate @@ -395,7 +282,7 @@ def _debt(user: address) -> (uint256, uint256): @param user User address @return (debt, rate_mul) """ - rate_mul: uint256 = AMM.get_rate_mul() + rate_mul: uint256 = staticcall AMM.get_rate_mul() loan: Loan = self.loan[user] if loan.initial_debt == 0: return (0, rate_mul) @@ -412,7 +299,7 @@ def _debt(user: address) -> (uint256, uint256): @external @view -@nonreentrant('lock') +@nonreentrant def debt(user: address) -> uint256: """ @notice Get the value of debt without changing the state @@ -424,7 +311,7 @@ def debt(user: address) -> uint256: @external @view -@nonreentrant('lock') +@nonreentrant def loan_exists(user: address) -> bool: """ @notice Check whether there is a loan of `user` in existence @@ -438,9 +325,9 @@ def _get_total_debt() -> uint256: """ @notice Total debt of this controller """ - rate_mul: uint256 = AMM.get_rate_mul() + rate_mul: uint256 = staticcall AMM.get_rate_mul() loan: Loan = self._total_debt - return loan.initial_debt * rate_mul / loan.rate_mul + return loan.initial_debt * rate_mul // loan.rate_mul @internal @@ -452,7 +339,7 @@ def _update_total_debt(d_debt: uint256, rate_mul: uint256, is_increase: bool) -> @notice Update total debt of this controller """ loan: Loan = self._total_debt - loan.initial_debt = loan.initial_debt * rate_mul / loan.rate_mul + loan.initial_debt = loan.initial_debt * rate_mul // loan.rate_mul if is_increase: loan.initial_debt += d_debt assert loan.initial_debt <= self.borrow_cap, "Borrow cap exceeded" @@ -474,7 +361,7 @@ def total_debt() -> uint256: @internal -@pure +@view def get_y_effective(collateral: uint256, N: uint256, discount: uint256) -> uint256: """ @notice Intermediary method which calculates y_effective defined as x_effective / p_base, @@ -497,7 +384,7 @@ def get_y_effective(collateral: uint256, N: uint256, discount: uint256) -> uint2 ), 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) @@ -517,8 +404,8 @@ 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) @@ -538,18 +425,18 @@ def _calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256, user: add # 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_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 @@ -560,22 +447,22 @@ def max_p_base() -> uint256: """ @notice Calculate max base price including skipping bands """ - p_oracle: uint256 = AMM.price_oracle() + p_oracle: uint256 = staticcall 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) + n1: int256 = math._wad_ln(convert(staticcall AMM.get_base_price() * 10**18 // 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 = AMM.active_band_with_skip() + n_min: int256 = staticcall AMM.active_band_with_skip() n1 = max(n1, n_min + 1) - p_base: uint256 = AMM.p_oracle_up(n1) + p_base: uint256 = staticcall AMM.p_oracle_up(n1) - for i in range(MAX_SKIP_TICKS + 1): + for i: uint256 in range(MAX_SKIP_TICKS + 1): n1 -= 1 if n1 <= n_min: break p_base_prev: uint256 = p_base - p_base = AMM.p_oracle_up(n1) + p_base = unsafe_div(p_base * A, Aminus1) if p_base > p_oracle: return p_base_prev @@ -586,19 +473,19 @@ def max_p_base() -> uint256: @view def _borrowed_balance() -> uint256: # (VAULT.deposited() - VAULT.withdrawn()) - (self.lent - self.repaid) - self.collected - return VAULT.deposited() + self.repaid - VAULT.withdrawn() - self.lent - self.collected + return staticcall VAULT.deposited() + self.repaid - staticcall VAULT.withdrawn() - self.lent - self.collected @external @view -@nonreentrant('lock') +@nonreentrant def borrowed_balance() -> uint256: return self._borrowed_balance() @external @view -@nonreentrant('lock') +@nonreentrant 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) @@ -638,7 +525,7 @@ def max_borrowable(collateral: uint256, N: uint256, current_debt: uint256 = 0, u @external @view -@nonreentrant('lock') +@nonreentrant def min_collateral(debt: uint256, N: uint256, user: address = empty(address)) -> uint256: """ @notice Minimal amount of collateral required to support debt @@ -651,7 +538,7 @@ def min_collateral(debt: uint256, N: uint256, user: address = empty(address)) -> 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)), + 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) @@ -659,7 +546,7 @@ def min_collateral(debt: uint256, N: uint256, user: address = empty(address)) -> @external @view -@nonreentrant('lock') +@nonreentrant 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 @@ -674,15 +561,15 @@ def calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256, user: addr @internal -def transferFrom(token: ERC20, _from: address, _to: address, amount: uint256): +def transferFrom(token: IERC20, _from: address, _to: address, amount: uint256): if amount > 0: - assert token.transferFrom(_from, _to, amount, default_return_value=True) + assert extcall token.transferFrom(_from, _to, amount, default_return_value=True) @internal -def transfer(token: ERC20, _to: address, amount: uint256): +def transfer(token: IERC20, _to: address, amount: uint256): if amount > 0: - assert token.transfer(_to, amount, default_return_value=True) + assert extcall token.transfer(_to, amount, default_return_value=True) @internal @@ -692,37 +579,37 @@ def execute_callback(callbacker: address, callback_sig: bytes4, user: address, s 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.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, calldata)), + concat(callback_sig, abi_encode(user, stablecoins, collateral, debt, calldata)), max_outsize=64 ) data.stablecoins = 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, _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" + assert N > MIN_TICKS_UINT - 1, "Need more ticks" + assert N < MAX_TICKS_UINT + 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}) + rate_mul: uint256 = staticcall 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 @@ -733,18 +620,18 @@ def _create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address): self._update_total_debt(debt, rate_mul, True) - AMM.deposit_range(_for, collateral, n1, n2) + extcall AMM.deposit_range(_for, collateral, n1, n2) self.lent += debt self.processed += debt self._save_rate() - log UserState(_for, collateral, debt, n1, n2, liquidation_discount) - log Borrow(_for, collateral, debt) + log UserState(user=_for, collateral=collateral, debt=debt, n1=n1, n2=n2, liquidation_discount=liquidation_discount) + log Borrow(user=_for, collateral_increase=collateral, loan_increase=debt) @external -@nonreentrant('lock') +@nonreentrant def create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address = msg.sender, callbacker: address = empty(address), calldata: Bytes[10**4] = b""): """ @notice Create loan but pass stablecoin to a callback first so that it can build leverage @@ -793,10 +680,8 @@ def _add_collateral_borrow(d_collateral: uint256, d_debt: uint256, _for: address debt, rate_mul = self._debt(_for) assert debt > 0, "Loan doesn't exist" 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) + xy: uint256[2] = extcall AMM.withdraw(_for, 10**18) assert xy[0] == 0, "Already in underwater mode" if remove_collateral: xy[1] -= d_collateral @@ -807,12 +692,15 @@ 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 * 10**18 * 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] = Loan(initial_debt=debt, rate_mul=rate_mul) liquidation_discount: uint256 = 0 if _for == msg.sender: @@ -825,15 +713,15 @@ def _add_collateral_borrow(d_collateral: uint256, d_debt: uint256, _for: address self._update_total_debt(d_debt, rate_mul, True) if remove_collateral: - log RemoveCollateral(_for, d_collateral) + log RemoveCollateral(user=_for, collateral_decrease=d_collateral) else: - log Borrow(_for, d_collateral, d_debt) + log Borrow(user=_for, collateral_increase=d_collateral, loan_increase=d_debt) - log UserState(_for, xy[1], debt, n1, n2, liquidation_discount) + log UserState(user=_for, collateral=xy[1], debt=debt, n1=n1, n2=n2, liquidation_discount=liquidation_discount) @external -@nonreentrant('lock') +@nonreentrant def add_collateral(collateral: uint256, _for: address = msg.sender): """ @notice Add extra collateral to avoid bad liqidations @@ -848,7 +736,7 @@ def add_collateral(collateral: uint256, _for: address = msg.sender): @external -@nonreentrant('lock') +@nonreentrant def remove_collateral(collateral: uint256, _for: address = msg.sender): """ @notice Remove some collateral without repaying the debt @@ -864,7 +752,7 @@ def remove_collateral(collateral: uint256, _for: address = msg.sender): @external -@nonreentrant('lock') +@nonreentrant def borrow_more(collateral: uint256, debt: uint256, _for: address = msg.sender, callbacker: address = empty(address), calldata: Bytes[10**4] = b""): """ @notice Borrow more stablecoins while adding more collateral using a callback (to leverage more) @@ -910,8 +798,8 @@ def _remove_from_list(_for: address): @external -@nonreentrant('lock') -def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 = 2**255-1, +@nonreentrant +def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 = max_value(int256), callbacker: address = empty(address), calldata: Bytes[10**4] = b""): """ @notice Repay debt (partially or fully) @@ -931,7 +819,7 @@ def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 cb: CallbackData = empty(CallbackData) if callbacker != empty(address): assert approval - xy = AMM.withdraw(_for, 10 ** 18) + xy = extcall AMM.withdraw(_for, 10 ** 18) self.transferFrom(COLLATERAL_TOKEN, AMM.address, callbacker, xy[1]) cb = self.execute_callback( callbacker, CALLBACK_REPAY, _for, xy[0], xy[1], debt, calldata) @@ -945,7 +833,7 @@ def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 d_debt = debt debt = 0 if callbacker == empty(address): - xy = AMM.withdraw(_for, 10 ** 18) + xy = extcall AMM.withdraw(_for, 10 ** 18) # Transfer all stablecoins to self if xy[0] > 0: @@ -969,16 +857,16 @@ def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 self.transferFrom(COLLATERAL_TOKEN, callbacker, _for, cb.collateral) self._remove_from_list(_for) - log UserState(_for, 0, 0, 0, 0, 0) - log Repay(_for, xy[1], d_debt) + log UserState(user=_for, collateral=0, debt=0, n1=0, n2=0, liquidation_discount=0) + log Repay(user=_for, collateral_decrease=xy[1], loan_decrease=d_debt) # Else - partial repayment else: - active_band: int256 = AMM.active_band_with_skip() + active_band: int256 = staticcall AMM.active_band_with_skip() assert active_band <= max_active_band d_debt = total_stablecoins debt = unsafe_sub(debt, d_debt) - ns: int256[2] = AMM.read_user_tick_numbers(_for) + ns: int256[2] = staticcall AMM.read_user_tick_numbers(_for) size: int256 = unsafe_sub(ns[1], ns[0]) liquidation_discount: uint256 = self.liquidation_discounts[_for] @@ -986,14 +874,14 @@ def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 # Not in soft-liquidation - can use callback and move bands new_collateral: uint256 = cb.collateral if callbacker == empty(address): - xy = AMM.withdraw(_for, 10**18) + xy = extcall AMM.withdraw(_for, 10**18) new_collateral = xy[1] ns[0] = self._calculate_debt_n1(new_collateral, debt, convert(unsafe_add(size, 1), uint256), _for) ns[1] = ns[0] + size - AMM.deposit_range(_for, new_collateral, ns[0], ns[1]) + extcall AMM.deposit_range(_for, new_collateral, ns[0], ns[1]) else: # Underwater - cannot use callback or move bands but can avoid a bad liquidation - xy = AMM.get_sum_xy(_for) + xy = staticcall AMM.get_sum_xy(_for) assert callbacker == empty(address) if approval: @@ -1010,12 +898,12 @@ def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 if _d_debt > 0: self.transferFrom(BORROWED_TOKEN, msg.sender, self, _d_debt) - log UserState(_for, xy[1], debt, ns[0], ns[1], liquidation_discount) - log Repay(_for, 0, d_debt) + log UserState(user=_for, collateral=xy[1], debt=debt, n1=ns[0], n2=ns[1], liquidation_discount=liquidation_discount) + log Repay(user=_for, collateral_decrease=0, loan_decrease=d_debt) self.repaid += d_debt - self.loan[_for] = Loan({initial_debt: debt, rate_mul: rate_mul}) + self.loan[_for] = Loan(initial_debt=debt, rate_mul=rate_mul) self._update_total_debt(d_debt, rate_mul, False) self._save_rate() @@ -1035,22 +923,22 @@ def _health(user: address, debt: uint256, full: bool, liquidation_discount: uint """ 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 + health = unsafe_div(convert(staticcall AMM.get_x_down(user), int256) * health, convert(debt, int256)) - 10**18 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') +@nonreentrant 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 @@ -1061,7 +949,7 @@ 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) + ns: int256[2] = staticcall AMM.read_user_tick_numbers(user) debt: int256 = convert(self._debt(user)[0], int256) n: uint256 = N ld: int256 = 0 @@ -1078,19 +966,19 @@ def health_calculator(user: address, d_collateral: int256, d_debt: int256, full: debt += d_debt assert debt > 0, "Non-positive debt" - active_band: int256 = AMM.active_band_with_skip() + active_band: int256 = staticcall AMM.active_band_with_skip() if ns[0] > active_band: # re-deposit - collateral = convert(AMM.get_sum_xy(user)[1], int256) + d_collateral + 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(AMM.get_x_down(user) * unsafe_mul(10**18, BORROWED_PRECISION), int256) + x_eff = convert(staticcall 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) + 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 @@ -1099,7 +987,7 @@ def health_calculator(user: address, d_collateral: int256, d_debt: int256, full: if full: if n1 > active_band: # We are not in liquidation mode - p_diff: int256 = max(p0, convert(AMM.price_oracle(), int256)) - p0 + p_diff: int256 = max(p0, convert(staticcall AMM.price_oracle(), int256)) - p0 if p_diff > 0: health += unsafe_div(p_diff * collateral, debt) @@ -1146,7 +1034,7 @@ def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint2 # 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 @@ -1184,11 +1072,11 @@ def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint2 self.transferFrom(BORROWED_TOKEN, AMM.address, msg.sender, unsafe_sub(xy[0], debt)) self.repaid += 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) + self.loan[user] = Loan(initial_debt=final_debt, rate_mul=rate_mul) + log Repay(user=user, collateral_decrease=xy[1], loan_decrease=debt) + log Liquidate(liquidator=msg.sender, user=user, collateral_received=xy[1], stablecoin_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 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) self._update_total_debt(debt, rate_mul, False) @@ -1197,7 +1085,7 @@ def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint2 @external -@nonreentrant('lock') +@nonreentrant 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 (or self-liquidation) of user if health is not good @@ -1214,7 +1102,7 @@ def liquidate(user: address, min_x: uint256, frac: uint256 = 10**18, callbacker: @view @external -@nonreentrant('lock') +@nonreentrant def tokens_to_liquidate(user: address, frac: uint256 = 10 ** 18) -> uint256: """ @notice Calculate the amount of stablecoins to have in liquidator's wallet to liquidate a user @@ -1225,7 +1113,7 @@ def tokens_to_liquidate(user: address, frac: uint256 = 10 ** 18) -> uint256: 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) + stablecoins: uint256 = unsafe_div((staticcall 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) return unsafe_sub(max(debt, stablecoins), stablecoins) @@ -1233,7 +1121,7 @@ def tokens_to_liquidate(user: address, frac: uint256 = 10 ** 18) -> uint256: @view @external -@nonreentrant('lock') +@nonreentrant def health(user: address, full: bool = False) -> int256: """ @notice Returns position health normalized to 1e18 for the user. @@ -1244,7 +1132,7 @@ def health(user: address, full: bool = False) -> int256: @view @external -@nonreentrant('lock') +@nonreentrant def users_to_liquidate(_from: uint256=0, _limit: uint256=0) -> DynArray[Position, 1000]: """ @notice Returns a dynamic array of users who can be "hard-liquidated". @@ -1259,21 +1147,21 @@ def users_to_liquidate(_from: uint256=0, _limit: uint256=0) -> DynArray[Position limit = n_loans ix: uint256 = _from out: DynArray[Position, 1000] = [] - for i in range(10**6): + for i: uint256 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 - })) + xy: uint256[2] = staticcall AMM.get_sum_xy(user) + out.append(Position( + user=user, + x=xy[0], + y=xy[1], + debt=debt, + health=health + )) ix += 1 return out @@ -1285,34 +1173,34 @@ def amm_price() -> uint256: """ @notice Current price from the AMM """ - return AMM.get_p() + return staticcall AMM.get_p() @view @external -@nonreentrant('lock') +@nonreentrant 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])] + 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 -@nonreentrant('lock') +@nonreentrant 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) """ - xy: uint256[2] = AMM.get_sum_xy(user) - ns: int256[2] = AMM.read_user_tick_numbers(user) # ns[1] > ns[0] + 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)[0], convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256)] @@ -1323,25 +1211,25 @@ 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_AMM_FEE """ - assert msg.sender == VAULT.admin() + assert msg.sender == staticcall VAULT.admin() assert fee <= MAX_AMM_FEE and fee >= MIN_AMM_FEE, "Fee" - AMM.set_fee(fee) + extcall AMM.set_fee(fee) -@nonreentrant('lock') +@nonreentrant @external def set_monetary_policy(monetary_policy: address): """ @notice Set monetary policy contract @param monetary_policy Address of the monetary policy contract """ - assert msg.sender == VAULT.admin() + assert msg.sender == staticcall VAULT.admin() self.monetary_policy = MonetaryPolicy(monetary_policy) - MonetaryPolicy(monetary_policy).rate_write() - log SetMonetaryPolicy(monetary_policy) + extcall MonetaryPolicy(monetary_policy).rate_write() + log SetMonetaryPolicy(monetary_policy=monetary_policy) -@nonreentrant('lock') +@nonreentrant @external def set_borrowing_discounts(loan_discount: uint256, liquidation_discount: uint256): """ @@ -1349,37 +1237,37 @@ def set_borrowing_discounts(loan_discount: uint256, liquidation_discount: uint25 @param loan_discount Discount which defines LTV @param liquidation_discount Discount where bad liquidation starts """ - assert msg.sender == VAULT.admin() + assert msg.sender == staticcall VAULT.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 SetBorrowingDiscounts(loan_discount=loan_discount, liquidation_discount=liquidation_discount) @external -@nonreentrant('lock') +@nonreentrant def set_callback(cb: address): """ @notice Set liquidity mining callback """ - assert msg.sender == VAULT.admin() - AMM.set_callback(cb) - log SetLMCallback(cb) + assert msg.sender == staticcall VAULT.admin() + extcall AMM.set_callback(cb) + log SetLMCallback(callback=cb) @external -@nonreentrant('lock') +@nonreentrant 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 """ - assert msg.sender == VAULT.admin() + assert msg.sender == staticcall VAULT.admin() self.borrow_cap = _borrow_cap - log SetBorrowCap(_borrow_cap) + log SetBorrowCap(borrow_cap=_borrow_cap) @external @@ -1387,7 +1275,7 @@ def set_admin_fee(admin_fee: uint256): """ @param admin_fee The fee which should be no higher than MAX_ADMIN_FEE """ - assert msg.sender == VAULT.admin() + assert msg.sender == staticcall VAULT.admin() assert admin_fee <= MAX_ADMIN_FEE, "admin_fee is higher than MAX_ADMIN_FEE" self.admin_fee = admin_fee @@ -1403,15 +1291,15 @@ def admin_fees() -> uint256: @external -@nonreentrant('lock') +@nonreentrant def collect_fees() -> uint256: """ @notice Collect the fees charged as a fraction of interest. """ - _to: address = VAULT.fee_receiver() + _to: address = staticcall VAULT.fee_receiver() # Borrowing-based fees - rate_mul: uint256 = AMM.get_rate_mul() + rate_mul: uint256 = staticcall AMM.get_rate_mul() loan: Loan = self._update_total_debt(0, rate_mul, False) self._save_rate() @@ -1422,13 +1310,13 @@ def collect_fees() -> uint256: # Difference between to_be_redeemed and minted amount is exactly due to interest charged if to_be_repaid > processed: self.processed = to_be_repaid - fees: uint256 = unsafe_sub(to_be_repaid, processed) * self.admin_fee / 10**18 + fees: uint256 = unsafe_sub(to_be_repaid, processed) * self.admin_fee // 10**18 self.collected += fees self.transfer(BORROWED_TOKEN, _to, fees) - log CollectFees(fees, loan.initial_debt) + log CollectFees(amount=fees, new_supply=loan.initial_debt) return fees else: - log CollectFees(0, loan.initial_debt) + log CollectFees(amount=0, new_supply=loan.initial_debt) return 0 @@ -1442,7 +1330,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 Approval(owner=msg.sender, spender=_spender, allow=_allow) @internal @@ -1458,4 +1346,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 SetExtraHealth(user=msg.sender, health=_value) From 53d6344ddc7de2fe51a407dd811f50341a11ec18 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 29 Jul 2025 15:31:10 +0400 Subject: [PATCH 053/413] refactor: fully deprecate OneWayLendingFactoryL2 --- contracts/lending/OneWayLendingFactoryL2.vy | 441 -------------------- scripts/boa-arbi-console.py | 2 +- scripts/deploy-lending-arb-crv.py | 2 +- scripts/deploy-lending-arbitrum.py | 2 +- scripts/deploy-lending-fraxtal.py | 2 +- scripts/deploy-lending-fxn.py | 2 +- scripts/deploy-lending-optimism.py | 2 +- scripts/deploy-lending-sonic.py | 2 +- scripts/recreate-arbi-markets.py | 2 +- 9 files changed, 8 insertions(+), 449 deletions(-) delete mode 100644 contracts/lending/OneWayLendingFactoryL2.vy diff --git a/contracts/lending/OneWayLendingFactoryL2.vy b/contracts/lending/OneWayLendingFactoryL2.vy deleted file mode 100644 index 1c3077d7..00000000 --- a/contracts/lending/OneWayLendingFactoryL2.vy +++ /dev/null @@ -1,441 +0,0 @@ -# @version 0.3.10 -""" -@title OneWayLendingFactory -@notice Factory of non-rehypothecated lending vaults: collateral is not being lent out. - 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 SetFeeReceiver: - fee_receiver: 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) -fee_receiver: 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, - fee_receiver: 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) - @param fee_receiver Receiver of interest and admin fees - """ - 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 - self.fee_receiver = fee_receiver - - -@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 -@nonreentrant('lock') -def set_fee_receiver(fee_receiver: address): - """ - @notice Set fee receiver who earns interest (DAO) - @param fee_receiver Address of the receiver - """ - assert msg.sender == self.admin - assert fee_receiver != empty(address) - self.fee_receiver = fee_receiver - log SetFeeReceiver(fee_receiver) - - -@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/scripts/boa-arbi-console.py b/scripts/boa-arbi-console.py index caf3df8f..640d0723 100755 --- a/scripts/boa-arbi-console.py +++ b/scripts/boa-arbi-console.py @@ -30,7 +30,7 @@ def account_load(fname): 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) + factory = boa.load_partial('contracts/lending/deprecated/OneWayLendingFactoryL2.vy').at(FACTORY) import IPython IPython.embed() diff --git a/scripts/deploy-lending-arb-crv.py b/scripts/deploy-lending-arb-crv.py index f9d7cd90..44f2377b 100755 --- a/scripts/deploy-lending-arb-crv.py +++ b/scripts/deploy-lending-arb-crv.py @@ -60,7 +60,7 @@ def account_load(fname): 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" diff --git a/scripts/deploy-lending-arbitrum.py b/scripts/deploy-lending-arbitrum.py index 61d0e9cf..812e72c6 100755 --- a/scripts/deploy-lending-arbitrum.py +++ b/scripts/deploy-lending-arbitrum.py @@ -66,7 +66,7 @@ def account_load(fname): gauge_factory = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI).at(GAUGE_FACTORY) factory = boa.load( - 'contracts/lending/OneWayLendingFactoryL2.vy', + 'contracts/lending/deprecated/OneWayLendingFactoryL2.vy', CRVUSD, amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, GAUGE_FACTORY, diff --git a/scripts/deploy-lending-fraxtal.py b/scripts/deploy-lending-fraxtal.py index 052ef052..34c84fc6 100644 --- a/scripts/deploy-lending-fraxtal.py +++ b/scripts/deploy-lending-fraxtal.py @@ -113,7 +113,7 @@ def account_load(fname): gauge_factory = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI).at(GAUGE_FACTORY) factory = boa.load( - 'contracts/lending/OneWayLendingFactoryL2.vy', + 'contracts/lending/deprecated/OneWayLendingFactoryL2.vy', CRVUSD, amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, GAUGE_FACTORY, diff --git a/scripts/deploy-lending-fxn.py b/scripts/deploy-lending-fxn.py index be7bf9a8..a24e103e 100644 --- a/scripts/deploy-lending-fxn.py +++ b/scripts/deploy-lending-fxn.py @@ -63,7 +63,7 @@ def account_load(fname): 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" diff --git a/scripts/deploy-lending-optimism.py b/scripts/deploy-lending-optimism.py index 3c7d0bc0..b0e4d871 100644 --- a/scripts/deploy-lending-optimism.py +++ b/scripts/deploy-lending-optimism.py @@ -130,7 +130,7 @@ def account_load(fname): gauge_factory = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI).at(GAUGE_FACTORY) factory = boa.load( - 'contracts/lending/OneWayLendingFactoryL2.vy', + 'contracts/lending/deprecated/OneWayLendingFactoryL2.vy', CRVUSD, amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, GAUGE_FACTORY, diff --git a/scripts/deploy-lending-sonic.py b/scripts/deploy-lending-sonic.py index 851166e5..06a0bc0c 100644 --- a/scripts/deploy-lending-sonic.py +++ b/scripts/deploy-lending-sonic.py @@ -125,7 +125,7 @@ def account_load(fname): 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( + 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, diff --git a/scripts/recreate-arbi-markets.py b/scripts/recreate-arbi-markets.py index 7dde5b6f..1b3b6c73 100755 --- a/scripts/recreate-arbi-markets.py +++ b/scripts/recreate-arbi-markets.py @@ -47,7 +47,7 @@ def account_load(fname): boa.env.add_account(babe_raw) boa.env._fork_try_prefetch_state = False - factory = boa.load_partial('contracts/lending/OneWayLendingFactoryL2.vy').at(FACTORY) + 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 = {} From a94e732f802da4bb237569e9f4a5b7a76b8feb2a Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 29 Jul 2025 15:31:45 +0400 Subject: [PATCH 054/413] chore: move LiquidityGauge to deprecated --- contracts/lending/{ => deprecated}/LiquidityGauge.vy | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename contracts/lending/{ => deprecated}/LiquidityGauge.vy (100%) diff --git a/contracts/lending/LiquidityGauge.vy b/contracts/lending/deprecated/LiquidityGauge.vy similarity index 100% rename from contracts/lending/LiquidityGauge.vy rename to contracts/lending/deprecated/LiquidityGauge.vy From 7ab606401fbfa11a979b9026471896c0b0dfd188 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 29 Jul 2025 15:34:49 +0400 Subject: [PATCH 055/413] refactor: rename OneWayLendingFactory -> LendingFactory --- contracts/lending/Controller.vy | 2 +- .../{OneWayLendingFactory.vy => LendingFactory.vy} | 6 ++++-- contracts/lending/Vault.vy | 9 ++++++--- 3 files changed, 11 insertions(+), 6 deletions(-) rename contracts/lending/{OneWayLendingFactory.vy => LendingFactory.vy} (99%) diff --git a/contracts/lending/Controller.vy b/contracts/lending/Controller.vy index a9918b8a..20970e0b 100644 --- a/contracts/lending/Controller.vy +++ b/contracts/lending/Controller.vy @@ -4,7 +4,7 @@ """ @title LlamaLend Controller @author Curve.Fi -@license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved +@license Copyright (c) Curve.Fi, 2020-2025 - all rights reserved """ interface LLAMMA: diff --git a/contracts/lending/OneWayLendingFactory.vy b/contracts/lending/LendingFactory.vy similarity index 99% rename from contracts/lending/OneWayLendingFactory.vy rename to contracts/lending/LendingFactory.vy index 9daddcd2..3074974f 100644 --- a/contracts/lending/OneWayLendingFactory.vy +++ b/contracts/lending/LendingFactory.vy @@ -1,9 +1,11 @@ # @version 0.3.10 +# pragma optimize codesize +# pragma evm-version shanghai """ -@title OneWayLendingFactory +@title LlamaLend Factory @notice Factory of non-rehypothecated lending vaults: collateral is not being lent out. @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 ERC20Detailed as ERC20 diff --git a/contracts/lending/Vault.vy b/contracts/lending/Vault.vy index 6c93dea2..bf1f2a47 100644 --- a/contracts/lending/Vault.vy +++ b/contracts/lending/Vault.vy @@ -1,10 +1,13 @@ # @version 0.3.10 +# pragma optimize codesize +# pragma evm-version shanghai """ -@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 688b1d99900bb9076caeb69ed3cda6529cc2c512 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 29 Jul 2025 15:38:05 +0400 Subject: [PATCH 056/413] refactor: remove amms from Controller storage --- contracts/lending/LendingFactory.vy | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/contracts/lending/LendingFactory.vy b/contracts/lending/LendingFactory.vy index 3074974f..cba1c9f4 100644 --- a/contracts/lending/LendingFactory.vy +++ b/contracts/lending/LendingFactory.vy @@ -94,7 +94,6 @@ fee_receiver: 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) @@ -231,7 +230,6 @@ def _create( 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 @@ -354,6 +352,12 @@ def controllers(n: uint256) -> address: return self.vaults[n].controller() +@view +@external +def amms(n: uint256) -> address: + return self.vaults[n].amm() + + @view @external def borrowed_tokens(n: uint256) -> address: From fdbf4ad849743bcb3c91f69eea57121095287cee Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 29 Jul 2025 15:43:47 +0400 Subject: [PATCH 057/413] fix: add collateral_token back to Vault --- contracts/lending/LendingFactory.vy | 4 ++-- contracts/lending/Vault.vy | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/contracts/lending/LendingFactory.vy b/contracts/lending/LendingFactory.vy index cba1c9f4..d031b1f0 100644 --- a/contracts/lending/LendingFactory.vy +++ b/contracts/lending/LendingFactory.vy @@ -12,7 +12,7 @@ from vyper.interfaces import ERC20Detailed as ERC20 interface Vault: - def initialize(amm: address, controller: address, borrowed_token: address): nonpayable + def initialize(amm: address, controller: address, borrowed_token: address, collateral_token: address): nonpayable def amm() -> address: view def controller() -> address: view def borrowed_token() -> address: view @@ -224,7 +224,7 @@ def _create( code_offset=3) AMM(amm).set_admin(controller) - vault.initialize(amm, controller, borrowed_token) + vault.initialize(amm, controller, borrowed_token, collateral_token) market_count: uint256 = self.market_count log NewVault(market_count, collateral_token, borrowed_token, diff --git a/contracts/lending/Vault.vy b/contracts/lending/Vault.vy index bf1f2a47..b2afdf74 100644 --- a/contracts/lending/Vault.vy +++ b/contracts/lending/Vault.vy @@ -75,6 +75,7 @@ DEAD_SHARES: constant(uint256) = 1000 MIN_ASSETS: constant(uint256) = 10000 borrowed_token: public(ERC20) +collateral_token: public(ERC20) amm: public(AMM) controller: public(Controller) @@ -117,17 +118,20 @@ def initialize( amm: AMM, controller: Controller, borrowed_token: ERC20, + collateral_token: ERC20, ): """ @notice Initializer for vaults @param amm Address of the AMM @param controller Address of the Controller @param borrowed_token Token which is being borrowed + @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 - borrowed_token.decimals()) + self.collateral_token = collateral_token self.factory = Factory(msg.sender) self.amm = amm From add1f9268c56682547408430bd2ab53745308949 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 29 Jul 2025 15:44:45 +0400 Subject: [PATCH 058/413] fix: small fix --- contracts/lending/Controller.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/lending/Controller.vy b/contracts/lending/Controller.vy index 20970e0b..1c00902a 100644 --- a/contracts/lending/Controller.vy +++ b/contracts/lending/Controller.vy @@ -210,7 +210,7 @@ def __init__( self.loan_discount = loan_discount self._total_debt.rate_mul = 10**18 - A = amm.A() + A = AMM.A() Aminus1 = unsafe_sub(A, 1) LOGN_A_RATIO = self.wad_ln(unsafe_div(A * 10**18, unsafe_sub(A, 1))) MAX_AMM_FEE = min(unsafe_div(10**18 * MIN_TICKS, A), 10**17) From b64a97a7826d52f0d99cadbe1c5db82060a8085b Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 29 Jul 2025 13:45:22 +0200 Subject: [PATCH 059/413] feat: backport lending improvements to mint controller --- contracts/Controller.vy | 387 +++++++++++++++------------------------- 1 file changed, 144 insertions(+), 243 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 1b2a66cf..76745f80 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -153,17 +153,12 @@ Aminus1: immutable(uint256) LOGN_A_RATIO: immutable(int256) # log(A / (A - 1)) SQRT_BAND_RATIO: immutable(uint256) -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 +MIN_AMM_FEE: constant(uint256) = 10**6 # 1e-12, still needs to be above 0 +MAX_AMM_FEE: immutable(uint256) # let's set to MIN_TICKS / A: for example, 4% max fee for A=100 -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) - -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) +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) DEAD_SHARES: constant(uint256) = 1000 @@ -200,7 +195,7 @@ def __init__( A = _A Aminus1 = unsafe_sub(_A, 1) LOGN_A_RATIO = math._wad_ln(convert(unsafe_div(_A * 10**18, unsafe_sub(_A, 1)), int256)) - MAX_FEE = min(unsafe_div(10**18 * MIN_TICKS_UINT, A), 10**17) + MAX_AMM_FEE = min(unsafe_div(10**18 * MIN_TICKS_UINT, A), 10**17) COLLATERAL_TOKEN = IERC20(collateral_token) collateral_decimals: uint256 = convert(staticcall IERC20Detailed(COLLATERAL_TOKEN.address).decimals(), uint256) @@ -217,11 +212,11 @@ def __init__( @external @view -def factory() -> Factory: +def factory() -> address: """ @notice Address of the factory """ - return FACTORY + return FACTORY.address @external @@ -314,10 +309,9 @@ def loan_exists(user: address) -> bool: return self.loan[user].initial_debt > 0 -# No decorator because used in monetary policy -@external +@internal @view -def total_debt() -> uint256: +def _get_total_debt() -> uint256: """ @notice Total debt of this controller """ @@ -326,6 +320,36 @@ def total_debt() -> uint256: return loan.initial_debt * rate_mul // loan.rate_mul +@internal +def _update_total_debt(d_debt: uint256, rate_mul: uint256, is_increase: bool) -> Loan: + """ + @param d_debt Change in debt amount (unsigned) + @param rate_mul New rate_mul + @param is_increase Whether debt increases or decreases + @notice Update total debt of this controller + """ + loan: Loan = self._total_debt + loan.initial_debt = loan.initial_debt * rate_mul // loan.rate_mul + if is_increase: + loan.initial_debt += d_debt + assert loan.initial_debt <= self.borrow_cap, "Borrow cap exceeded" + else: + loan.initial_debt = unsafe_sub(max(loan.initial_debt, d_debt), d_debt) + loan.rate_mul = rate_mul + self._total_debt = loan + + return loan + +# No decorator because used in monetary policy +@external +@view +def total_debt() -> uint256: + """ + @notice Total debt of this controller + """ + return self._get_total_debt() + + @internal @view def get_y_effective(collateral: uint256, N: uint256, discount: uint256) -> uint256: @@ -521,9 +545,8 @@ def transfer(token: IERC20, _to: address, amount: uint256): @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, stablecoins: uint256, + collateral: uint256, debt: uint256, calldata: Bytes[10**4]) -> CallbackData: assert callbacker != COLLATERAL_TOKEN.address assert callbacker != BORROWED_TOKEN.address @@ -535,7 +558,7 @@ def execute_callback(callbacker: address, callback_sig: bytes4, # Callback response: Bytes[64] = raw_call( callbacker, - concat(callback_sig, abi_encode(user, stablecoins, collateral, debt, callback_args, callback_bytes)), + concat(callback_sig, abi_encode(user, stablecoins, collateral, debt, calldata)), max_outsize=64 ) data.stablecoins = convert(slice(response, 0, 32), uint256) @@ -549,7 +572,7 @@ def execute_callback(callbacker: address, callback_sig: bytes4, return data @internal -def _create_loan(collateral: uint256, debt: uint256, N: uint256, transfer_coins: bool, _for: address): +def _create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address): 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" @@ -573,10 +596,6 @@ def _create_loan(collateral: uint256, debt: uint256, N: uint256, transfer_coins: extcall 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(user=_for, collateral=collateral, debt=debt, n1=n1, n2=n2, liquidation_discount=liquidation_discount) @@ -585,54 +604,36 @@ def _create_loan(collateral: uint256, debt: uint256, N: uint256, transfer_coins: @external @nonreentrant -def create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address = msg.sender): +def create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address = msg.sender, callbacker: address = empty(address), calldata: Bytes[10**4] = b""): """ - @notice Create loan + @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 _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) - - -@external -@nonreentrant -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) - # Before callback - self.transfer(BORROWED_TOKEN, callbacker, debt) + more_collateral: uint256 = 0 + if callbacker != empty(address): + self.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 - # 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 + self._create_loan(collateral + more_collateral, debt, N, _for) - # 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) + if more_collateral > 0: + self.transferFrom(COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral) + if callbacker == empty(address): + self.transfer(BORROWED_TOKEN, _for, debt) @internal @@ -725,55 +726,33 @@ def remove_collateral(collateral: uint256, _for: address = msg.sender): @external @nonreentrant -def borrow_more(collateral: uint256, debt: uint256, _for: address = msg.sender): - """ - @notice Borrow more stablecoins while adding more collateral (not necessary) - @param collateral Amount of collateral to add - @param debt Amount of stablecoin 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 -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): +def borrow_more(collateral: uint256, debt: uint256, _for: address = msg.sender, callbacker: address = empty(address), calldata: Bytes[10**4] = b""): """ @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 callbacker Address of the callback contract + @param calldata Any data for callbacker """ if debt == 0: return assert self._check_approval(_for) - # Before callback - self.transfer(BORROWED_TOKEN, callbacker, debt) - - # 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 + more_collateral: uint256 = 0 + if callbacker != empty(address): + self.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 - # 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) + if more_collateral > 0: + self.transferFrom(COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral) + if callbacker == empty(address): + self.transfer(BORROWED_TOKEN, _for, debt) self._save_rate() @@ -792,156 +771,110 @@ def _remove_from_list(_for: address): @external @nonreentrant -def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 = max_value(int256)): +def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 = max_value(int256), + callbacker: address = empty(address), calldata: Bytes[10**4] = b""): """ @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 _d_debt The amount of debt to repay from user's wallet. 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) + @param callbacker Address of the callback contract + @param calldata Any data for callbacker """ - 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) + xy: uint256[2] = empty(uint256[2]) - if debt == 0: - # Allow to withdraw all assets even when underwater - xy: uint256[2] = extcall AMM.withdraw(_for, 10**18) + cb: CallbackData = empty(CallbackData) + if callbacker != empty(address): + assert approval + xy = extcall AMM.withdraw(_for, 10 ** 18) + self.transferFrom(COLLATERAL_TOKEN, AMM.address, callbacker, xy[1]) + cb = self.execute_callback( + callbacker, CALLBACK_REPAY, _for, xy[0], xy[1], debt, calldata) + + total_stablecoins: uint256 = _d_debt + xy[0] + cb.stablecoins + assert total_stablecoins > 0 # dev: no coins to repay + 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 + if callbacker == empty(address): + xy = extcall AMM.withdraw(_for, 10 ** 18) + + # Transfer all stablecoins to self 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]) + self.transferFrom(BORROWED_TOKEN, AMM.address, self, xy[0]) + if cb.stablecoins > 0: + self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins) + if _d_debt > 0: + self.transferFrom(BORROWED_TOKEN, msg.sender, self, _d_debt) + + # Transfer stablecoins excess to _for + if total_stablecoins > d_debt: + self.transfer(BORROWED_TOKEN, _for, unsafe_sub(total_stablecoins, d_debt)) + # Transfer collateral to _for + if callbacker == empty(address): if xy[1] > 0: self.transferFrom(COLLATERAL_TOKEN, AMM.address, _for, xy[1]) + else: + if cb.collateral > 0: + self.transferFrom(COLLATERAL_TOKEN, callbacker, _for, cb.collateral) + log UserState(user=_for, collateral=0, debt=0, n1=0, n2=0, liquidation_discount=0) log Repay(user=_for, collateral_decrease=xy[1], loan_decrease=d_debt) self._remove_from_list(_for) - + # Else - partial repayment else: active_band: int256 = staticcall AMM.active_band_with_skip() assert active_band <= max_active_band + d_debt = total_stablecoins + debt = unsafe_sub(debt, d_debt) ns: int256[2] = staticcall 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] = extcall 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 - extcall AMM.deposit_range(_for, xy[1], n1, n2) + # Not in soft-liquidation - can use callback and move bands + new_collateral: uint256 = cb.collateral + if callbacker == empty(address): + xy = extcall AMM.withdraw(_for, 10**18) + new_collateral = xy[1] + ns[0] = self._calculate_debt_n1(new_collateral, 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 - cannot use callback or move bands but can avoid a bad liquidation + xy = staticcall AMM.get_sum_xy(_for) + assert callbacker == empty(address) + 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(user=_for, collateral=xy[1], debt=debt, n1=n1, n2=n2, liquidation_discount=liquidation_discount) - log Repay(user=_for, collateral_decrease=0, loan_decrease=d_debt) else: - # Underwater - cannot move band but can avoid a bad liquidation - log UserState(user=_for, collateral=max_value(uint256), debt=debt, n1=ns[0], n2=ns[1], liquidation_discount=liquidation_discount) - log Repay(user=_for, collateral_decrease=0, loan_decrease=d_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 - - 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 - - self._save_rate() - - -@external -@nonreentrant -def repay_extended(callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b"", _for: address = msg.sender): - """ - @notice Repay loan but get a stablecoin for that from callback (to deleverage) - @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 - """ - assert self._check_approval(_for) + if cb.stablecoins > 0: + self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins) + if _d_debt > 0: + self.transferFrom(BORROWED_TOKEN, msg.sender, self, _d_debt) - # Before callback - ns: int256[2] = staticcall AMM.read_user_tick_numbers(_for) - xy: uint256[2] = extcall 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 + log UserState(user=_for, collateral=xy[1], debt=debt, n1=ns[0], n2=ns[1], liquidation_discount=liquidation_discount) + log Repay(user=_for, collateral_decrease=0, loan_decrease=d_debt) - # 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(user=_for, collateral=0, debt=0, n1=0, n2=0, liquidation_discount=0) - - # Else - partial repayment -> deleverage, but only if we are not underwater - 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 - extcall AMM.deposit_range(_for, cb.collateral, n1, n2) - liquidation_discount: uint256 = self.liquidation_discount - self.liquidation_discounts[_for] = liquidation_discount - - 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(user=_for, collateral=cb.collateral, debt=debt, n1=n1, n2=n2, liquidation_discount=liquidation_discount) - xy[1] -= cb.collateral - - # No need to check _health() because it's the _for - - # Common calls which we will do regardless of whether it's a full repay or not - log Repay(user=_for, collateral_decrease=xy[1], loan_decrease=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) @@ -1048,8 +981,7 @@ def _get_f_remove(frac: uint256, health_limit: uint256) -> uint256: @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""): +def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint256, callbacker: address, calldata: Bytes[10**4]): """ @notice Perform a bad liquidation of user if the health is too bad @param user Address of the user @@ -1057,7 +989,7 @@ def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint2 @param health_limit Minimal health to liquidate at @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 """ debt: uint256 = 0 rate_mul: uint256 = 0 @@ -1097,13 +1029,9 @@ def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint2 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 # Callback cb: CallbackData = self.execute_callback( - callbacker, callback_sig, user, xy[0], xy[1], debt, callback_args, callback_bytes) + callbacker, CALLBACK_LIQUIDATE, user, xy[0], xy[1], debt, calldata) 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)) @@ -1134,32 +1062,18 @@ def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint2 @external @nonreentrant -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 -def liquidate_extended(user: address, min_x: uint256, frac: uint256, - callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b""): +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 (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 + @param calldata Any data for callbacker """ 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) + self._liquidate(user, min_x, discount, min(frac, 10**18), callbacker, calldata) @view @@ -1274,7 +1188,7 @@ def set_amm_fee(fee: uint256): @param fee The fee which should be no higher than MAX_FEE """ assert msg.sender == staticcall FACTORY.admin() - assert fee <= MAX_FEE and fee >= MIN_FEE, "Fee" + assert fee <= MAX_AMM_FEE and fee >= MIN_AMM_FEE, "Fee" extcall AMM.set_fee(fee) @@ -1325,11 +1239,8 @@ def admin_fees() -> uint256: """ @notice Calculate the amount of fees obtained from the interest """ - rate_mul: uint256 = staticcall 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) + return unsafe_sub(max(self._get_total_debt() + self.redeemed, minted), minted) @external @@ -1337,10 +1248,7 @@ def admin_fees() -> uint256: 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 = staticcall FACTORY.fee_receiver() # Borrowing-based fees @@ -1368,13 +1276,6 @@ def collect_fees() -> uint256: return 0 -@external -@view -@nonreentrant -def check_lock() -> bool: - return True - - # Allowance methods @external From bcd7103fdc21769d32ddd0b5788c16a242cb8688 Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 29 Jul 2025 14:34:03 +0200 Subject: [PATCH 060/413] chore: bump AMM to Vyper 0.4.1 --- contracts/AMM.vy | 192 ++++++++++++++++++++++------------------------- 1 file changed, 89 insertions(+), 103 deletions(-) diff --git a/contracts/AMM.vy b/contracts/AMM.vy index 3fcbb581..cc9f2f16 100644 --- a/contracts/AMM.vy +++ b/contracts/AMM.vy @@ -1,4 +1,4 @@ -# @version 0.3.10 +# pragma version 0.4.1 """ @title LLAMMA - crvUSD AMM @author Curve.Fi @@ -78,11 +78,12 @@ event SetFee: MAX_TICKS: constant(int256) = 50 MAX_TICKS_UINT: constant(uint256) = 50 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 + ticks: uint256[MAX_TICKS_UINT // 2] # Share fractions packed 2 per slot struct DetailedTrade: in_amount: uint256 @@ -133,7 +134,7 @@ DEAD_SHARES: constant(uint256) = 1000 liquidity_mining_callback: public(LMGauge) -@external +@deploy def __init__( _borrowed_token: address, _borrowed_precision: uint256, @@ -175,7 +176,7 @@ def __init__( self.fee = fee price_oracle_contract = PriceOracle(_price_oracle_contract) self.prev_p_o_time = block.timestamp - self.old_p_o = price_oracle_contract.price() + self.old_p_o = staticcall price_oracle_contract.price() self.rate_mul = 10**18 @@ -187,7 +188,7 @@ 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 @@ -198,7 +199,7 @@ 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) + assert extcall token.approve(_admin, max_value(uint256), default_return_value=True) @external @@ -224,7 +225,7 @@ def sqrt_int(_x: uint256) -> uint256: @external -@pure +@view def coins(i: uint256) -> address: return [BORROWED_TOKEN.address, COLLATERAL_TOKEN.address][i] @@ -257,14 +258,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 @@ -280,7 +281,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 @@ -288,9 +289,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 @@ -298,12 +299,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 price_oracle_contract.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 price_oracle_contract.price_w()) self.prev_p_o_time = block.timestamp self.old_p_o = p[0] self.old_dfee = p[1] @@ -431,7 +432,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 @@ -479,7 +480,7 @@ 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. @@ -499,7 +500,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)) @@ -530,7 +531,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 @@ -538,12 +539,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 @@ -572,7 +573,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 @@ -593,7 +594,7 @@ 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] @@ -606,13 +607,13 @@ def _read_user_ticks(user: address, ns: int256[2]) -> DynArray[uint256, MAX_TICK @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 @@ -633,12 +634,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 @@ -650,7 +651,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 @@ -661,7 +662,7 @@ 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] @@ -673,7 +674,7 @@ def save_user_shares(user: address, user_shares: DynArray[uint256, MAX_TICKS_UIN @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 @@ -705,15 +706,15 @@ def deposit_range(user: address, amount: uint256, n1: int256, n2: int256): lm: LMGauge = 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 @@ -746,29 +747,23 @@ 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 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( - n1, collateral_shares, n_bands, - method_id=method_id("callback_collateral_shares(int256,uint256[],uint256)") - ), + abi_encode(method_id("callback_collateral_shares(int256,uint256[],uint256)"), n1, collateral_shares, n_bands), max_outsize=32, revert_on_failure=False) success, res = raw_call( lm.address, - _abi_encode( - user, n1, empty(DynArray[uint256, MAX_TICKS_UINT]), n_bands, - method_id=method_id("callback_user_shares(address,int256,uint256[],uint256)") - ), + abi_encode(method_id("callback_user_shares(address,int256,uint256[],uint256)"), user, n1, empty(DynArray[uint256, MAX_TICKS_UINT]), n_bands), max_outsize=32, revert_on_failure=False) @external -@nonreentrant('lock') +@nonreentrant def withdraw(user: address, frac: uint256) -> uint256[2]: """ @notice Withdraw liquidity for the user. Only admin contract can do it @@ -794,7 +789,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) @@ -845,24 +840,18 @@ 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 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 - method_id=method_id("callback_collateral_shares(int256,uint256[],uint256)") - ), + abi_encode(method_id("callback_collateral_shares(int256,uint256[],uint256)"), ns[0], empty(DynArray[uint256, MAX_TICKS_UINT]), len(old_user_shares)), max_outsize=32, revert_on_failure=False) success, res = raw_call( lm.address, - _abi_encode( - user, ns[0], old_user_shares, len(old_user_shares), - method_id=method_id("callback_user_shares(address,int256,uint256[],uint256)") - ), + abi_encode(method_id("callback_user_shares(address,int256,uint256[],uint256)"), user, ns[0], old_user_shares, len(old_user_shares)), max_outsize=32, revert_on_failure=False) return [total_x, total_y] @@ -897,7 +886,7 @@ def calc_swap_out(pump: bool, in_amount: uint256, p_o: uint256[2], in_precision: fee: uint256 = max(self.fee, p_o[1]) 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 @@ -909,7 +898,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) @@ -937,7 +926,7 @@ 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 + 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 @@ -953,7 +942,7 @@ def calc_swap_out(pump: bool, in_amount: uint256, p_o: uint256[2], in_precision: out.in_amount += dx out.out_amount += y - 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: @@ -974,7 +963,7 @@ 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) + 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 @@ -989,7 +978,7 @@ def calc_swap_out(pump: bool, in_amount: uint256, p_o: uint256[2], in_precision: out.in_amount += dy out.out_amount += x - 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: @@ -1047,7 +1036,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 @@ -1061,7 +1050,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 @@ -1125,7 +1114,7 @@ def _exchange(i: uint256, j: uint256, amount: uint256, minmax_amount: uint256, _ 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: @@ -1149,21 +1138,18 @@ 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 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( - n_start, collateral_shares, len(collateral_shares), - method_id=method_id("callback_collateral_shares(int256,uint256[],uint256)") - ), + abi_encode(method_id("callback_collateral_shares(int256,uint256[],uint256)"), n_start, collateral_shares, len(collateral_shares)), 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) + assert extcall in_coin.transferFrom(msg.sender, self, in_amount_done, default_return_value=True) + assert extcall out_coin.transfer(_for, out_amount_done, default_return_value=True) return [in_amount_done, out_amount_done] @@ -1196,7 +1182,7 @@ def calc_swap_in(pump: bool, out_amount: uint256, p_o: uint256[2], in_precision: fee: uint256 = max(self.fee, p_o[1]) 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 @@ -1208,7 +1194,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) @@ -1234,7 +1220,7 @@ 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 @@ -1250,7 +1236,7 @@ def calc_swap_in(pump: bool, out_amount: uint256, p_o: uint256[2], in_precision: out.out_amount += y 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: @@ -1269,11 +1255,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 - out.ticks_in[j] = y + dy + out.ticks_in[j] = y + dy break else: @@ -1285,7 +1271,7 @@ def calc_swap_in(pump: bool, out_amount: uint256, p_o: uint256[2], in_precision: out.out_amount += x 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: @@ -1311,7 +1297,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 @@ -1329,7 +1315,7 @@ def get_dx(i: uint256, j: uint256, out_amount: uint256) -> uint256: @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 @@ -1345,7 +1331,7 @@ def get_dydx(i: uint256, j: uint256, out_amount: uint256) -> (uint256, uint256): @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 @@ -1360,7 +1346,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 @@ -1396,7 +1382,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 @@ -1426,7 +1412,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 @@ -1440,7 +1426,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: @@ -1474,7 +1460,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: @@ -1482,7 +1468,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: @@ -1494,11 +1480,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) @@ -1511,7 +1497,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 @@ -1523,7 +1509,7 @@ 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 @@ -1549,7 +1535,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) @@ -1572,7 +1558,7 @@ 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 @@ -1584,7 +1570,7 @@ def get_sum_xy(user: address) -> uint256[2]: @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 @@ -1596,7 +1582,7 @@ def get_xy(user: address) -> DynArray[uint256, MAX_TICKS_UINT][2]: @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` @@ -1619,7 +1605,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] @@ -1646,8 +1632,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: @@ -1659,7 +1645,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: @@ -1674,7 +1660,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: @@ -1703,7 +1689,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 @@ -1715,12 +1701,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 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 @@ -1728,7 +1714,7 @@ def set_fee(fee: uint256): """ assert msg.sender == self.admin self.fee = fee - log SetFee(fee) + log SetFee(fee=fee) # nonreentrant decorator is in Controller which is admin From 04e54bcdbf9549e057c259b3ddf570dc557b7479 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 30 Jul 2025 11:25:31 +0200 Subject: [PATCH 061/413] fix: porting mistakes --- contracts/Controller.vy | 32 ++++++-------------------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 76745f80..71e63965 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -320,26 +320,6 @@ def _get_total_debt() -> uint256: return loan.initial_debt * rate_mul // loan.rate_mul -@internal -def _update_total_debt(d_debt: uint256, rate_mul: uint256, is_increase: bool) -> Loan: - """ - @param d_debt Change in debt amount (unsigned) - @param rate_mul New rate_mul - @param is_increase Whether debt increases or decreases - @notice Update total debt of this controller - """ - loan: Loan = self._total_debt - loan.initial_debt = loan.initial_debt * rate_mul // loan.rate_mul - if is_increase: - loan.initial_debt += d_debt - assert loan.initial_debt <= self.borrow_cap, "Borrow cap exceeded" - else: - loan.initial_debt = unsafe_sub(max(loan.initial_debt, d_debt), d_debt) - loan.rate_mul = rate_mul - self._total_debt = loan - - return loan - # No decorator because used in monetary policy @external @view @@ -822,8 +802,8 @@ def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 self.transfer(BORROWED_TOKEN, _for, unsafe_sub(total_stablecoins, d_debt)) # Transfer collateral to _for if callbacker == empty(address): - if xy[1] > 0: - self.transferFrom(COLLATERAL_TOKEN, AMM.address, _for, xy[1]) + if xy[1] > 0: + self.transferFrom(COLLATERAL_TOKEN, AMM.address, _for, xy[1]) else: if cb.collateral > 0: self.transferFrom(COLLATERAL_TOKEN, callbacker, _for, cb.collateral) @@ -856,10 +836,10 @@ def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 xy = staticcall AMM.get_sum_xy(_for) assert callbacker == empty(address) - 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 + 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) From 3b42eb824b216814ce2578cead3adac28bf86909 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 30 Jul 2025 11:29:37 +0200 Subject: [PATCH 062/413] refactor(AMM): use vyi interfaces --- contracts/AMM.vy | 82 ++----- contracts/interfaces/IAMM.vyi | 304 ++++++++++++++++++++++++++ contracts/interfaces/ILMGauge.vyi | 8 + contracts/interfaces/IPriceOracle.vyi | 6 + 4 files changed, 342 insertions(+), 58 deletions(-) create mode 100644 contracts/interfaces/IAMM.vyi create mode 100644 contracts/interfaces/ILMGauge.vyi create mode 100644 contracts/interfaces/IPriceOracle.vyi diff --git a/contracts/AMM.vy b/contracts/AMM.vy index cc9f2f16..10c269f8 100644 --- a/contracts/AMM.vy +++ b/contracts/AMM.vy @@ -34,53 +34,19 @@ # (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], size: uint256): nonpayable - def callback_user_shares(user: address, n: int256, user_shares: DynArray[uint256, MAX_TICKS_UINT], size: uint256): 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 +from contracts.interfaces import IAMM +implements: IAMM -event SetRate: - rate: uint256 - rate_mul: uint256 - time: uint256 - -event SetFee: - fee: uint256 +from contracts.interfaces import IPriceOracle +from contracts.interfaces import ILMGauge +from ethereum.ercs import IERC20 MAX_TICKS: constant(int256) = 50 MAX_TICKS_UINT: constant(uint256) = 50 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_UINT // 2] # Share fractions packed 2 per slot @@ -94,9 +60,9 @@ struct DetailedTrade: last_tick_j: 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) @@ -117,7 +83,7 @@ active_band: public(int256) min_band: public(int256) max_band: public(int256) -price_oracle_contract: public(immutable(PriceOracle)) +price_oracle_contract: public(immutable(IPriceOracle)) old_p_o: uint256 old_dfee: uint256 prev_p_o_time: uint256 @@ -131,7 +97,7 @@ total_shares: HashMap[int256, uint256] user_shares: public(HashMap[address, UserTicks]) DEAD_SHARES: constant(uint256) = 1000 -liquidity_mining_callback: public(LMGauge) +liquidity_mining_callback: public(ILMGauge) @deploy @@ -162,9 +128,9 @@ def __init__( @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_TOKEN = IERC20(_borrowed_token) BORROWED_PRECISION = _borrowed_precision - COLLATERAL_TOKEN = ERC20(_collateral_token) + COLLATERAL_TOKEN = IERC20(_collateral_token) COLLATERAL_PRECISION = _collateral_precision A = _A BASE_PRICE = _base_price @@ -174,7 +140,7 @@ def __init__( Aminus12 = pow_mod256(unsafe_sub(A, 1), 2) self.fee = fee - price_oracle_contract = PriceOracle(_price_oracle_contract) + price_oracle_contract = IPriceOracle(_price_oracle_contract) self.prev_p_o_time = block.timestamp self.old_p_o = staticcall price_oracle_contract.price() @@ -194,7 +160,7 @@ def __init__( @internal -def approve_max(token: ERC20, _admin: address): +def approve_max(token: IERC20, _admin: address): """ Approve max in a separate function because it uses less bytespace than calling directly, and gas doesn't matter in set_admin @@ -703,7 +669,7 @@ 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: uint256 in range(MAX_SKIP_TICKS_UINT + 1): @@ -747,7 +713,7 @@ def deposit_range(user: address, amount: uint256, n1: int256, n2: int256): self.save_user_shares(user, user_shares) - log Deposit(provider=user, amount=amount, n1=n1, n2=n2) + log IAMM.Deposit(provider=user, amount=amount, n1=n1, n2=n2) if lm.address != empty(address): success: bool = False @@ -774,7 +740,7 @@ def withdraw(user: address, frac: uint256) -> uint256[2]: 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] @@ -840,7 +806,7 @@ 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(provider=user, amount_borrowed=total_x, amount_collateral=total_y) + log IAMM.Withdraw(provider=user, amount_borrowed=total_x, amount_collateral=total_y) if lm.address != empty(address): success: bool = False @@ -1080,11 +1046,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: @@ -1138,7 +1104,7 @@ def _exchange(i: uint256, j: uint256, amount: uint256, minmax_amount: uint256, _ self.active_band = out.n2 - log TokenExchange(buyer=_for, sold_id=i, tokens_sold=in_amount_done, bought_id=j, tokens_bought=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 @@ -1701,7 +1667,7 @@ def set_rate(rate: uint256) -> uint256: self.rate_mul = rate_mul self.rate_time = block.timestamp self.rate = rate - log SetRate(rate=rate, rate_mul=rate_mul, time=block.timestamp) + log IAMM.SetRate(rate=rate, rate_mul=rate_mul, time=block.timestamp) return rate_mul @@ -1714,12 +1680,12 @@ def set_fee(fee: uint256): """ assert msg.sender == self.admin self.fee = fee - log SetFee(fee=fee) + log IAMM.SetFee(fee=fee) # nonreentrant decorator is in Controller which is admin @external -def set_callback(liquidity_mining_callback: LMGauge): +def set_callback(liquidity_mining_callback: ILMGauge): """ @notice Set a gauge address with callbacks for liquidity mining for collateral @param liquidity_mining_callback Gauge address diff --git a/contracts/interfaces/IAMM.vyi b/contracts/interfaces/IAMM.vyi new file mode 100644 index 00000000..2c51b205 --- /dev/null +++ b/contracts/interfaces/IAMM.vyi @@ -0,0 +1,304 @@ +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 + + +# 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): + ... + + +@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: + ... + + +# TODO check with vyper team +# @view +# @external +# def price_oracle_contract() -> IPriceOracle: + # ... + + +@view +@external +def bands_x(arg0: int256) -> uint256: + ... + + +@view +@external +def bands_y(arg0: int256) -> uint256: + ... + + +# TODO check with vyper team +# @view +# @external +# def user_shares(arg0: address) -> UserTicks: + # ... + +# TODO check with vyper team +# @view +# @external +# def liquidity_mining_callback() -> ILMGauge: + # ... + diff --git a/contracts/interfaces/ILMGauge.vyi b/contracts/interfaces/ILMGauge.vyi new file mode 100644 index 00000000..b529d526 --- /dev/null +++ b/contracts/interfaces/ILMGauge.vyi @@ -0,0 +1,8 @@ +# TODO import this one? +MAX_TICKS_UINT: constant(uint256) = 50 + +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): + ... \ No newline at end of file diff --git a/contracts/interfaces/IPriceOracle.vyi b/contracts/interfaces/IPriceOracle.vyi new file mode 100644 index 00000000..0d52c578 --- /dev/null +++ b/contracts/interfaces/IPriceOracle.vyi @@ -0,0 +1,6 @@ +@view +def price() -> uint256: + ... + +def price_w() -> uint256: + ... \ No newline at end of file From 1de1402b4b4c902b9eabce80b7219d36819a8501 Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 31 Jul 2025 11:19:08 +0400 Subject: [PATCH 063/413] chore: remove twoway tests for lending --- tests/lending/_test_bigfuzz_two.py | 422 ----------------------------- tests/lending/conftest.py | 66 ----- tests/lending/test_twoway_vault.py | 90 ------ 3 files changed, 578 deletions(-) delete mode 100644 tests/lending/_test_bigfuzz_two.py delete mode 100644 tests/lending/test_twoway_vault.py 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..42a5eb14 100644 --- a/tests/lending/conftest.py +++ b/tests/lending/conftest.py @@ -151,69 +151,3 @@ def fake_leverage(collateral_token, borrowed_token, market_controller, admin): 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_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 From f27f3942066cdf88b438f1ccfc65b6d97ab3df54 Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 31 Jul 2025 12:47:15 +0400 Subject: [PATCH 064/413] fix: decimals type convertion --- contracts/lending/LendingFactory.vy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/lending/LendingFactory.vy b/contracts/lending/LendingFactory.vy index d031b1f0..99048b73 100644 --- a/contracts/lending/LendingFactory.vy +++ b/contracts/lending/LendingFactory.vy @@ -211,8 +211,8 @@ def _create( vault: Vault = Vault(create_minimal_proxy_to(self.vault_impl)) amm: address = create_from_blueprint( self.amm_impl, - borrowed_token, 10**(18 - ERC20(borrowed_token).decimals()), - collateral_token, 10**(18 - ERC20(collateral_token).decimals()), + borrowed_token, 10**convert(18 - ERC20(borrowed_token).decimals(), uint256), + collateral_token, 10**convert(18 - ERC20(collateral_token).decimals(), uint256), A, isqrt(A_ratio * 10**18), self.ln_int(A_ratio), p, fee, convert(0, uint256), price_oracle, code_offset=3) From 685cc41c221044c3350b1835c8721b12542a5caf Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 31 Jul 2025 14:21:57 +0200 Subject: [PATCH 065/413] refactor(Controller): use common module --- .gitignore | 1 + contracts/AMM.vy | 81 +- contracts/Controller.vy | 1316 +++----- contracts/constants.vy | 6 + contracts/controller_core.vy | 811 +++++ contracts/interfaces/IAMM.vyi | 28 +- contracts/interfaces/IController.vyi | 327 ++ contracts/interfaces/IFactory.vyi | 15 + contracts/interfaces/ILMGauge.vyi | 4 +- contracts/interfaces/ILlamalendController.vyi | 2 + contracts/interfaces/IMintController.vyi | 10 + contracts/interfaces/IMonetaryPolicy.vyi | 2 + contracts/interfaces/IVault.vyi | 26 + contracts/lending/Controller.vy | 1322 +++----- poetry.lock | 2806 +++++++++-------- pyproject.toml | 2 +- 16 files changed, 3647 insertions(+), 3112 deletions(-) create mode 100644 contracts/constants.vy create mode 100644 contracts/controller_core.vy create mode 100644 contracts/interfaces/IController.vyi create mode 100644 contracts/interfaces/IFactory.vyi create mode 100644 contracts/interfaces/ILlamalendController.vyi create mode 100644 contracts/interfaces/IMintController.vyi create mode 100644 contracts/interfaces/IMonetaryPolicy.vyi create mode 100644 contracts/interfaces/IVault.vyi diff --git a/.gitignore b/.gitignore index e5208a48..44ca38f9 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ venv /brownie-deploy-emas/build .coverage .claude +.vscode diff --git a/contracts/AMM.vy b/contracts/AMM.vy index 10c269f8..eaf3adae 100644 --- a/contracts/AMM.vy +++ b/contracts/AMM.vy @@ -42,22 +42,16 @@ from contracts.interfaces import ILMGauge from ethereum.ercs import IERC20 +from contracts import constants as c + + +# TODO common constants MAX_TICKS: constant(int256) = 50 -MAX_TICKS_UINT: constant(uint256) = 50 +MAX_TICKS_UINT: constant(uint256) = c.MAX_TICKS_UINT 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_UINT // 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 +# TODO create vyper issue +DEAD_SHARES: constant(uint256) = c.DEAD_SHARES BORROWED_TOKEN: immutable(IERC20) # x @@ -83,7 +77,15 @@ active_band: public(int256) min_band: public(int256) max_band: public(int256) -price_oracle_contract: public(immutable(IPriceOracle)) +_price_oracle_contract: immutable(IPriceOracle) + +# TODO This is a workaround for a compiler bug +@view +@external +def price_oracle_contract() -> IPriceOracle: + return _price_oracle_contract + + old_p_o: uint256 old_dfee: uint256 prev_p_o_time: uint256 @@ -94,10 +96,17 @@ 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(ILMGauge) +_liquidity_mining_callback: ILMGauge + +# TODO compiler bug workaround +# TODO report issue +@view +@external +def liquidity_mining_callback() -> ILMGauge: + return self._liquidity_mining_callback @deploy @@ -112,7 +121,7 @@ def __init__( _base_price: uint256, fee: uint256, admin_fee: uint256, - _price_oracle_contract: address, + price_oracle_contract: address, ): """ @notice LLAMMA constructor @@ -140,9 +149,9 @@ def __init__( Aminus12 = pow_mod256(unsafe_sub(A, 1), 2) self.fee = fee - price_oracle_contract = IPriceOracle(_price_oracle_contract) + _price_oracle_contract = IPriceOracle(price_oracle_contract) self.prev_p_o_time = block.timestamp - self.old_p_o = staticcall price_oracle_contract.price() + self.old_p_o = staticcall _price_oracle_contract.price() self.rate_mul = 10**18 @@ -265,12 +274,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(staticcall price_oracle_contract.price()) + return self.limit_p_o(staticcall _price_oracle_contract.price()) @internal def _price_oracle_w() -> uint256[2]: - p: uint256[2] = self.limit_p_o(extcall price_oracle_contract.price_w()) + p: uint256[2] = self.limit_p_o(extcall _price_oracle_contract.price_w()) self.prev_p_o_time = block.timestamp self.old_p_o = p[0] self.old_dfee = p[1] @@ -669,7 +678,7 @@ 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: ILMGauge = self.liquidity_mining_callback + lm: ILMGauge = self._liquidity_mining_callback # Autoskip bands if we can for i: uint256 in range(MAX_SKIP_TICKS_UINT + 1): @@ -740,7 +749,7 @@ def withdraw(user: address, frac: uint256) -> uint256[2]: assert msg.sender == self.admin assert frac <= 10**18 - lm: ILMGauge = self.liquidity_mining_callback + lm: ILMGauge = self._liquidity_mining_callback ns: int256[2] = self._read_user_tick_numbers(user) n: int256 = ns[0] @@ -825,7 +834,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. @@ -842,7 +851,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] @@ -970,7 +979,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 @@ -982,7 +991,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 @@ -1025,7 +1034,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) @@ -1046,7 +1055,7 @@ def _exchange(i: uint256, j: uint256, amount: uint256, minmax_amount: uint256, _ if amount == 0: return [0, 0] - lm: ILMGauge = self.liquidity_mining_callback + lm: ILMGauge = self._liquidity_mining_callback collateral_shares: DynArray[uint256, MAX_TICKS_UINT] = [] in_coin: IERC20 = BORROWED_TOKEN @@ -1059,7 +1068,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: @@ -1122,7 +1131,7 @@ def _exchange(i: uint256, j: uint256, amount: uint256, minmax_amount: uint256, _ @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. @@ -1138,7 +1147,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] @@ -1274,7 +1283,7 @@ 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 @@ -1292,7 +1301,7 @@ 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) @@ -1691,4 +1700,4 @@ def set_callback(liquidity_mining_callback: ILMGauge): @param liquidity_mining_callback Gauge address """ assert msg.sender == self.admin - self.liquidity_mining_callback = liquidity_mining_callback + self._liquidity_mining_callback = liquidity_mining_callback diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 71e63965..2258ce65 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -1,178 +1,73 @@ -# pragma version 0.4.1 +# pragma version 0.4.3 +# pragma nonreentrancy on # pragma optimize codesize -# pragma evm-version shanghai """ @title crvUSD Controller @author Curve.Fi @license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved """ -from snekmate.utils import math from ethereum.ercs import IERC20 from ethereum.ercs import IERC20Detailed +from contracts.interfaces import IAMM +from contracts.interfaces import ILMGauge +from contracts.interfaces import IMonetaryPolicy +from contracts.interfaces import IFactory + +from contracts.interfaces import IController +implements: IController +from contracts.interfaces import IMintController -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 price_oracle() -> uint256: view - def can_skip_bands(n_end: int256) -> bool: view - 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 MonetaryPolicy: - def rate_write() -> uint256: nonpayable - -interface Factory: - def stablecoin() -> address: view - def admin() -> address: view - def fee_receiver() -> 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) -MAX_LOAN_DISCOUNT: constant(uint256) = 5 * 10**17 -MIN_LIQUIDATION_DISCOUNT: constant(uint256) = 10**16 # Start liquidating when threshold reached -MAX_TICKS: constant(int256) = 50 -MAX_TICKS_UINT: constant(uint256) = 50 -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 - -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 +from snekmate.utils import math +from contracts import controller_core as ctrl + +initializes: ctrl + +exports: ( + ctrl.amm, + ctrl.amm_price, + ctrl.approval, + ctrl.approve, + ctrl.borrowed_token, + ctrl.calculate_debt_n1, + ctrl.collateral_token, + ctrl.debt, + ctrl.extra_health, + ctrl.health, + ctrl.health_calculator, + ctrl.liquidation_discount, + ctrl.liquidation_discounts, + ctrl.loan_discount, + ctrl.loan_exists, + ctrl.loan_ix, + ctrl.loans, + ctrl.min_collateral, + ctrl.monetary_policy, + ctrl.n_loans, + ctrl.save_rate, + ctrl.set_extra_health, + ctrl.tokens_to_liquidate, + ctrl.total_debt, + ctrl.user_prices, + ctrl.user_state, + ctrl.users_to_liquidate, +) + +FACTORY: immutable(IFactory) minted: public(uint256) redeemed: public(uint256) -monetary_policy: public(MonetaryPolicy) -liquidation_discount: public(uint256) -loan_discount: public(uint256) - -COLLATERAL_TOKEN: immutable(IERC20) -COLLATERAL_PRECISION: immutable(uint256) - -BORROWED_TOKEN: immutable(IERC20) -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) - -MIN_AMM_FEE: constant(uint256) = 10**6 # 1e-12, still needs to be above 0 -MAX_AMM_FEE: immutable(uint256) # let's set to MIN_TICKS / A: for example, 4% max fee for A=100 - -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) - -DEAD_SHARES: constant(uint256) = 1000 - -approval: public(HashMap[address, HashMap[address, bool]]) -extra_health: public(HashMap[address, uint256]) - @deploy def __init__( - collateral_token: address, - monetary_policy: address, - loan_discount: uint256, - liquidation_discount: uint256, - amm: address): + collateral_token: IERC20, + monetary_policy: IMonetaryPolicy, + loan_discount: uint256, + liquidation_discount: uint256, + amm: IAMM, +): """ @notice Controller constructor deployed by the factory from blueprint @param collateral_token Token to use for collateral @@ -182,32 +77,18 @@ def __init__( get_x_down() for "bad liquidation" purposes @param amm AMM address (Already deployed from blueprint) """ - FACTORY = Factory(msg.sender) - - self.monetary_policy = MonetaryPolicy(monetary_policy) - - self.liquidation_discount = liquidation_discount - self.loan_discount = loan_discount - self._total_debt.rate_mul = 10**18 - - AMM = LLAMMA(amm) - _A: uint256 = staticcall LLAMMA(amm).A() - A = _A - Aminus1 = unsafe_sub(_A, 1) - LOGN_A_RATIO = math._wad_ln(convert(unsafe_div(_A * 10**18, unsafe_sub(_A, 1)), int256)) - MAX_AMM_FEE = min(unsafe_div(10**18 * MIN_TICKS_UINT, A), 10**17) - - COLLATERAL_TOKEN = IERC20(collateral_token) - collateral_decimals: uint256 = convert(staticcall IERC20Detailed(COLLATERAL_TOKEN.address).decimals(), uint256) - COLLATERAL_PRECISION = pow_mod256(10, 18 - collateral_decimals) + FACTORY = IFactory(msg.sender) - BORROWED_TOKEN = IERC20(staticcall Factory(msg.sender).stablecoin()) - borrowed_decimals: uint256 = convert(staticcall IERC20Detailed(BORROWED_TOKEN.address).decimals(), uint256) - BORROWED_PRECISION = pow_mod256(10, 18 - borrowed_decimals) + borrowed_token: IERC20 = staticcall FACTORY.stablecoin() - SQRT_BAND_RATIO = isqrt(unsafe_div(10**36 * _A, unsafe_sub(_A, 1))) - - assert extcall BORROWED_TOKEN.approve(msg.sender, max_value(uint256), default_return_value=True) + ctrl.__init__( + amm, + collateral_token, + borrowed_token, + monetary_policy, + loan_discount, + liquidation_discount, + ) @external @@ -221,228 +102,12 @@ def factory() -> address: @external @view -def amm() -> LLAMMA: - """ - @notice Address of the AMM - """ - return AMM - - -@external -@view -def collateral_token() -> IERC20: - """ - @notice Address of the collateral token - """ - return COLLATERAL_TOKEN - - -@external -@view -def borrowed_token() -> IERC20: - """ - @notice Address of the borrowed token - """ - return BORROWED_TOKEN - - -@internal -def _save_rate(): - """ - @notice Save current rate - """ - rate: uint256 = min(extcall self.monetary_policy.rate_write(), MAX_RATE) - extcall AMM.set_rate(rate) - - -@external -@nonreentrant -def save_rate(): - """ - @notice Save current rate - """ - self._save_rate() - - -@internal -@view -def _debt(user: address) -> (uint256, uint256): - """ - @notice Get the value of debt and rate_mul and update the rate_mul counter - @param user User address - @return (debt, rate_mul) - """ - rate_mul: uint256 = staticcall AMM.get_rate_mul() - loan: Loan = self.loan[user] - if loan.initial_debt == 0: - return (0, rate_mul) - else: - # Let user repay 1 smallest decimal more so that the system doesn't lose on precision - # Use ceil div - debt: uint256 = loan.initial_debt * rate_mul - 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 - return (debt, rate_mul) - - -@external -@view -@nonreentrant -def debt(user: address) -> uint256: - """ - @notice Get the value of debt without changing the state - @param user User address - @return Value of debt - """ - return self._debt(user)[0] - - -@external -@view -@nonreentrant -def loan_exists(user: address) -> bool: - """ - @notice Check whether there is a loan of `user` in existence - """ - return self.loan[user].initial_debt > 0 - - -@internal -@view -def _get_total_debt() -> uint256: - """ - @notice Total debt of this controller - """ - rate_mul: uint256 = staticcall AMM.get_rate_mul() - loan: Loan = self._total_debt - return loan.initial_debt * rate_mul // loan.rate_mul - - -# No decorator because used in monetary policy -@external -@view -def total_debt() -> uint256: - """ - @notice Total debt of this controller - """ - return self._get_total_debt() - - -@internal -@view -def get_y_effective(collateral: uint256, N: uint256, discount: uint256) -> uint256: - """ - @notice Intermediary method which calculates y_effective defined as x_effective / p_base, - however discounted by loan_discount. - x_effective is an amount which can be obtained from collateral when liquidating - @param collateral Amount of collateral to get the value for - @param N Number of bands the deposit is made into - @param discount Loan discount at 1e18 base (e.g. 1e18 == 100%) - @return y_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_effective * p_oracle_up(n1) - # d_y_effective = y / N / sqrt(A / (A - 1)) - # 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) - ), - unsafe_mul(SQRT_BAND_RATIO, N)) - y_effective: uint256 = d_y_effective - for i: uint256 in range(1, MAX_TICKS_UINT): - if i == N: - break - d_y_effective = unsafe_div(d_y_effective * Aminus1, 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: - """ - @notice Calculate the upper band number for the deposit to sit in to support - the given debt. 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 - @return Upper band n1 (n1 <= n2) to deposit into. Signed integer - """ - assert debt > 0, "No loan" - 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]) - # p_oracle_up(n1) = base_price * ((A - 1) / A)**n1 - - # We borrow up until min band touches p_oracle, - # or it touches non-empty bands which cannot be skipped. - # We calculate required n1 for given (collateral, debt), - # and if n1 corresponds to price_oracle being too high, or unreachable band - # - 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 - - # 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 = 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_div(n1, LOGN_A_RATIO) - - n1 = min(n1, 1024 - convert(N, int256)) + n0 - if n1 <= n0: - 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 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 = 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() * 10**18 // 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 = unsafe_div(p_base * A, Aminus1) - if p_base > p_oracle: - return p_base_prev - - return p_base - - -@external -@view -@nonreentrant -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 @@ -465,126 +130,78 @@ def max_borrowable(collateral: uint256, N: uint256, current_debt: uint256 = 0, u # 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, staticcall BORROWED_TOKEN.balanceOf(self) + current_debt) # Cannot borrow beyond the amount of coins Controller has - + assert N >= ctrl.MIN_TICKS_UINT and N <= ctrl.MAX_TICKS_UINT -@external -@view -@nonreentrant -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 - @param user User to calculate the value for (only necessary for nonzero extra_health) - @return Minimal collateral required - """ - # 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) - - -@external -@view -@nonreentrant -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. - @param collateral Amount of collateral (at its native precision) - @param debt Amount of requested debt - @param N Number of bands to deposit into - @param user User to calculate n1 for (only necessary for nonzero extra_health) - @return Upper band n1 (n1 <= n2) to deposit into. Signed integer - """ - return self._calculate_debt_n1(collateral, debt, N, user) - - -@internal -def transferFrom(token: IERC20, _from: address, _to: address, amount: uint256): - if amount > 0: - assert extcall token.transferFrom(_from, _to, amount, default_return_value=True) - - -@internal -def transfer(token: IERC20, _to: address, amount: uint256): - if amount > 0: - assert extcall token.transfer(_to, amount, default_return_value=True) - - -@internal -def execute_callback(callbacker: address, callback_sig: bytes4, user: address, stablecoins: uint256, - collateral: uint256, debt: uint256, calldata: Bytes[10**4]) -> CallbackData: - assert callbacker != COLLATERAL_TOKEN.address - assert callbacker != BORROWED_TOKEN.address - - data: CallbackData = empty(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, calldata)), - max_outsize=64 + y_effective: uint256 = ctrl.get_y_effective( + collateral * ctrl.COLLATERAL_PRECISION, + N, + ctrl.loan_discount + ctrl.extra_health[user], ) - data.stablecoins = convert(slice(response, 0, 32), uint256) - data.collateral = convert(slice(response, 32, 32), uint256) - # Checks after callback - 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) + x: uint256 = unsafe_sub( + max(unsafe_div(y_effective * ctrl.max_p_base(), 10**18), 1), 1 + ) + x = unsafe_div( + x * (10**18 - 10**14), unsafe_mul(10**18, ctrl.BORROWED_PRECISION) + ) # Make it a bit smaller + # TODO need to port cap? + return min( + x, staticcall ctrl.BORROWED_TOKEN.balanceOf(self) + current_debt + ) # Cannot borrow beyond the amount of coins Controller has - return data @internal def _create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address): - 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" + assert ctrl.loan[_for].initial_debt == 0, "Loan already created" + assert N > ctrl.MIN_TICKS_UINT - 1, "Need more ticks" + assert N < ctrl.MAX_TICKS_UINT + 1, "Need less ticks" - n1: int256 = self._calculate_debt_n1(collateral, debt, N, _for) + n1: int256 = ctrl._calculate_debt_n1(collateral, debt, N, _for) n2: int256 = n1 + convert(unsafe_sub(N, 1), int256) - rate_mul: uint256 = staticcall 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 + rate_mul: uint256 = staticcall ctrl.AMM.get_rate_mul() + ctrl.loan[_for] = IController.Loan(initial_debt=debt, rate_mul=rate_mul) + liquidation_discount: uint256 = ctrl.liquidation_discount + ctrl.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) + n_loans: uint256 = ctrl.n_loans + ctrl.loans[n_loans] = _for + ctrl.loan_ix[_for] = n_loans + ctrl.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 + ctrl._total_debt.initial_debt = ( + ctrl._total_debt.initial_debt * rate_mul // ctrl._total_debt.rate_mul + + debt + ) + ctrl._total_debt.rate_mul = rate_mul - extcall AMM.deposit_range(_for, collateral, n1, n2) + extcall ctrl.AMM.deposit_range(_for, collateral, n1, n2) self.minted += debt - self._save_rate() + ctrl._save_rate() - log UserState(user=_for, collateral=collateral, debt=debt, n1=n1, n2=n2, liquidation_discount=liquidation_discount) - log Borrow(user=_for, collateral_increase=collateral, loan_increase=debt) + log IController.UserState( + user=_for, + collateral=collateral, + debt=debt, + n1=n1, + n2=n2, + liquidation_discount=liquidation_discount, + ) + log IController.Borrow( + user=_for, collateral_increase=collateral, loan_increase=debt + ) @external -@nonreentrant -def create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address = msg.sender, callbacker: address = empty(address), calldata: Bytes[10**4] = b""): +def create_loan( + collateral: uint256, + debt: uint256, + N: uint256, + _for: address = msg.sender, + callbacker: address = empty(address), + calldata: Bytes[10**4] = b"", +): """ @notice Create loan but pass stablecoin to a callback first so that it can build leverage @param collateral Amount of collateral to use @@ -598,27 +215,43 @@ def create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address = 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) + assert ctrl._check_approval(_for) more_collateral: uint256 = 0 if callbacker != empty(address): - self.transfer(BORROWED_TOKEN, callbacker, debt) + ctrl.transfer(ctrl.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 + more_collateral = ctrl.execute_callback( + callbacker, + ctrl.CALLBACK_DEPOSIT, + _for, + 0, + collateral, + debt, + calldata, + ).collateral self._create_loan(collateral + more_collateral, debt, N, _for) - self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) + ctrl.transferFrom( + ctrl.COLLATERAL_TOKEN, msg.sender, ctrl.AMM.address, collateral + ) if more_collateral > 0: - self.transferFrom(COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral) + ctrl.transferFrom( + ctrl.COLLATERAL_TOKEN, callbacker, ctrl.AMM.address, more_collateral + ) if callbacker == empty(address): - self.transfer(BORROWED_TOKEN, _for, debt) + ctrl.transfer(ctrl.BORROWED_TOKEN, _for, 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, +): """ @notice Internal method to borrow and add or remove collateral @param d_collateral Amount of collateral to add @@ -629,11 +262,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) + debt, rate_mul = ctrl._debt(_for) assert debt > 0, "Loan doesn't exist" debt += d_debt - xy: uint256[2] = extcall AMM.withdraw(_for, 10**18) + xy: uint256[2] = extcall ctrl.AMM.withdraw(_for, 10**18) assert xy[0] == 0, "Already in underwater mode" if remove_collateral: xy[1] -= d_collateral @@ -644,37 +277,55 @@ 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 * staticcall AMM.price_oracle() > 2 * 10**18 * BORROWED_PRECISION // COLLATERAL_PRECISION - - ns: int256[2] = staticcall AMM.read_user_tick_numbers(_for) + assert ( + d_collateral * staticcall ctrl.AMM.price_oracle() + > 2 + * 10**18 + * ctrl.BORROWED_PRECISION // ctrl.COLLATERAL_PRECISION + ) + ns: int256[2] = staticcall ctrl.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) + n1: int256 = ctrl._calculate_debt_n1(xy[1], debt, size, _for) n2: int256 = n1 + unsafe_sub(ns[1], ns[0]) - extcall AMM.deposit_range(_for, xy[1], n1, n2) - self.loan[_for] = Loan(initial_debt=debt, rate_mul=rate_mul) + extcall ctrl.AMM.deposit_range(_for, xy[1], n1, n2) + ctrl.loan[_for] = IController.Loan(initial_debt=debt, rate_mul=rate_mul) liquidation_discount: uint256 = 0 if _for == msg.sender: - liquidation_discount = self.liquidation_discount - self.liquidation_discounts[_for] = liquidation_discount + liquidation_discount = ctrl.liquidation_discount + ctrl.liquidation_discounts[_for] = liquidation_discount else: - liquidation_discount = self.liquidation_discounts[_for] + liquidation_discount = ctrl.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 + ctrl._total_debt.initial_debt = ( + ctrl._total_debt.initial_debt + * rate_mul // ctrl._total_debt.rate_mul + + d_debt + ) + ctrl._total_debt.rate_mul = rate_mul if remove_collateral: - log RemoveCollateral(user=_for, collateral_decrease=d_collateral) + log IController.RemoveCollateral( + user=_for, collateral_decrease=d_collateral + ) else: - log Borrow(user=_for, collateral_increase=d_collateral, loan_increase=d_debt) - - log UserState(user=_for, collateral=xy[1], debt=debt, n1=n1, n2=n2, liquidation_discount=liquidation_discount) + 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, + ) @external -@nonreentrant def add_collateral(collateral: uint256, _for: address = msg.sender): """ @notice Add extra collateral to avoid bad liqidations @@ -684,12 +335,13 @@ 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) - self._save_rate() + ctrl.transferFrom( + ctrl.COLLATERAL_TOKEN, msg.sender, ctrl.AMM.address, collateral + ) + ctrl._save_rate() @external -@nonreentrant def remove_collateral(collateral: uint256, _for: address = msg.sender): """ @notice Remove some collateral without repaying the debt @@ -698,15 +350,20 @@ def remove_collateral(collateral: uint256, _for: address = msg.sender): """ if collateral == 0: return - assert self._check_approval(_for) + assert ctrl._check_approval(_for) self._add_collateral_borrow(collateral, 0, _for, True, False) - self.transferFrom(COLLATERAL_TOKEN, AMM.address, _for, collateral) - self._save_rate() + ctrl.transferFrom(ctrl.COLLATERAL_TOKEN, ctrl.AMM.address, _for, collateral) + ctrl._save_rate() @external -@nonreentrant -def borrow_more(collateral: uint256, debt: uint256, _for: address = msg.sender, callbacker: address = empty(address), calldata: Bytes[10**4] = b""): +def borrow_more( + collateral: uint256, + debt: uint256, + _for: address = msg.sender, + callbacker: address = empty(address), + calldata: Bytes[10**4] = b"", +): """ @notice Borrow more stablecoins while adding more collateral using a callback (to leverage more) @param collateral Amount of collateral to add @@ -717,42 +374,47 @@ def borrow_more(collateral: uint256, debt: uint256, _for: address = msg.sender, """ if debt == 0: return - assert self._check_approval(_for) + assert ctrl._check_approval(_for) more_collateral: uint256 = 0 if callbacker != empty(address): - self.transfer(BORROWED_TOKEN, callbacker, debt) + ctrl.transfer(ctrl.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 - - self._add_collateral_borrow(collateral + more_collateral, debt, _for, False, False) + more_collateral = ctrl.execute_callback( + callbacker, + ctrl.CALLBACK_DEPOSIT, + _for, + 0, + collateral, + debt, + calldata, + ).collateral + + self._add_collateral_borrow( + collateral + more_collateral, debt, _for, False, False + ) self.minted += debt - self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) + + ctrl.transferFrom( + ctrl.COLLATERAL_TOKEN, msg.sender, ctrl.AMM.address, collateral + ) if more_collateral > 0: - self.transferFrom(COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral) + ctrl.transferFrom( + ctrl.COLLATERAL_TOKEN, callbacker, ctrl.AMM.address, more_collateral + ) if callbacker == empty(address): - self.transfer(BORROWED_TOKEN, _for, debt) - self._save_rate() - - -@internal -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 - self.loan_ix[_for] = 0 - if loan_ix < last_loan_ix: # Need to replace - last_loan: address = self.loans[last_loan_ix] - self.loans[loan_ix] = last_loan - self.loan_ix[last_loan] = loan_ix - self.n_loans = last_loan_ix + ctrl.transfer(ctrl.BORROWED_TOKEN, _for, debt) + ctrl._save_rate() @external -@nonreentrant -def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 = max_value(int256), - callbacker: address = empty(address), calldata: Bytes[10**4] = b""): +def repay( + _d_debt: uint256, + _for: address = msg.sender, + max_active_band: int256 = max_value(int256), + callbacker: address = empty(address), + calldata: Bytes[10**4] = b"", +): """ @notice Repay debt (partially or fully) @param _d_debt The amount of debt to repay from user's wallet. If higher than the current debt - will do full repayment @@ -763,18 +425,21 @@ def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 """ debt: uint256 = 0 rate_mul: uint256 = 0 - debt, rate_mul = self._debt(_for) + debt, rate_mul = ctrl._debt(_for) assert debt > 0, "Loan doesn't exist" - approval: bool = self._check_approval(_for) + approval: bool = ctrl._check_approval(_for) xy: uint256[2] = empty(uint256[2]) - cb: CallbackData = empty(CallbackData) + cb: IController.CallbackData = empty(IController.CallbackData) if callbacker != empty(address): assert approval - xy = extcall AMM.withdraw(_for, 10 ** 18) - self.transferFrom(COLLATERAL_TOKEN, AMM.address, callbacker, xy[1]) - cb = self.execute_callback( - callbacker, CALLBACK_REPAY, _for, xy[0], xy[1], debt, calldata) + xy = extcall ctrl.AMM.withdraw(_for, 10**18) + ctrl.transferFrom( + ctrl.COLLATERAL_TOKEN, ctrl.AMM.address, callbacker, xy[1] + ) + cb = ctrl.execute_callback( + callbacker, ctrl.CALLBACK_REPAY, _for, xy[0], xy[1], debt, calldata + ) total_stablecoins: uint256 = _d_debt + xy[0] + cb.stablecoins assert total_stablecoins > 0 # dev: no coins to repay @@ -785,183 +450,122 @@ def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 d_debt = debt debt = 0 if callbacker == empty(address): - xy = extcall AMM.withdraw(_for, 10 ** 18) + xy = extcall ctrl.AMM.withdraw(_for, 10**18) - # Transfer all stablecoins to self if xy[0] > 0: # Only allow full repayment when underwater for the sender to do assert approval - self.transferFrom(BORROWED_TOKEN, AMM.address, self, xy[0]) + ctrl.transferFrom( + ctrl.BORROWED_TOKEN, ctrl.AMM.address, self, xy[0] + ) if cb.stablecoins > 0: - self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins) + ctrl.transferFrom( + ctrl.BORROWED_TOKEN, callbacker, self, cb.stablecoins + ) if _d_debt > 0: - self.transferFrom(BORROWED_TOKEN, msg.sender, self, _d_debt) + ctrl.transferFrom(ctrl.BORROWED_TOKEN, msg.sender, self, _d_debt) - # Transfer stablecoins excess to _for if total_stablecoins > d_debt: - self.transfer(BORROWED_TOKEN, _for, unsafe_sub(total_stablecoins, d_debt)) + ctrl.transfer( + ctrl.BORROWED_TOKEN, _for, unsafe_sub(total_stablecoins, d_debt) + ) # Transfer collateral to _for if callbacker == empty(address): if xy[1] > 0: - self.transferFrom(COLLATERAL_TOKEN, AMM.address, _for, xy[1]) + ctrl.transferFrom( + ctrl.COLLATERAL_TOKEN, ctrl.AMM.address, _for, xy[1] + ) else: if cb.collateral > 0: - self.transferFrom(COLLATERAL_TOKEN, callbacker, _for, cb.collateral) - - log UserState(user=_for, collateral=0, debt=0, n1=0, n2=0, liquidation_discount=0) - log Repay(user=_for, collateral_decrease=xy[1], loan_decrease=d_debt) - self._remove_from_list(_for) + ctrl.transferFrom( + ctrl.COLLATERAL_TOKEN, callbacker, _for, cb.collateral + ) + ctrl._remove_from_list(_for) + 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=d_debt + ) # Else - partial repayment else: - active_band: int256 = staticcall AMM.active_band_with_skip() + active_band: int256 = staticcall ctrl.AMM.active_band_with_skip() assert active_band <= max_active_band d_debt = total_stablecoins debt = unsafe_sub(debt, d_debt) - ns: int256[2] = staticcall AMM.read_user_tick_numbers(_for) + ns: int256[2] = staticcall ctrl.AMM.read_user_tick_numbers(_for) size: int256 = unsafe_sub(ns[1], ns[0]) - liquidation_discount: uint256 = self.liquidation_discounts[_for] + liquidation_discount: uint256 = ctrl.liquidation_discounts[_for] if ns[0] > active_band: # Not in soft-liquidation - can use callback and move bands new_collateral: uint256 = cb.collateral if callbacker == empty(address): - xy = extcall AMM.withdraw(_for, 10**18) + xy = extcall ctrl.AMM.withdraw(_for, 10**18) new_collateral = xy[1] - ns[0] = self._calculate_debt_n1(new_collateral, debt, convert(unsafe_add(size, 1), uint256), _for) + ns[0] = ctrl._calculate_debt_n1( + new_collateral, + debt, + convert(unsafe_add(size, 1), uint256), + _for, + ) ns[1] = ns[0] + size - extcall AMM.deposit_range(_for, new_collateral, ns[0], ns[1]) + extcall ctrl.AMM.deposit_range(_for, new_collateral, ns[0], ns[1]) else: # Underwater - cannot use callback or move bands but can avoid a bad liquidation - xy = staticcall AMM.get_sum_xy(_for) + xy = staticcall ctrl.AMM.get_sum_xy(_for) assert callbacker == empty(address) 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 + liquidation_discount = ctrl.liquidation_discount + ctrl.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, debt, False, liquidation_discount) > 0 + assert ctrl._health(_for, debt, False, liquidation_discount) > 0 if cb.stablecoins > 0: - self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins) + ctrl.transferFrom( + ctrl.BORROWED_TOKEN, callbacker, self, cb.stablecoins + ) if _d_debt > 0: - self.transferFrom(BORROWED_TOKEN, msg.sender, self, _d_debt) - - log UserState(user=_for, collateral=xy[1], debt=debt, n1=ns[0], n2=ns[1], liquidation_discount=liquidation_discount) - log Repay(user=_for, collateral_decrease=0, loan_decrease=d_debt) + ctrl.transferFrom(ctrl.BORROWED_TOKEN, msg.sender, self, _d_debt) + + log IController.UserState( + user=_for, + collateral=xy[1], + debt=debt, + n1=ns[0], + n2=ns[1], + liquidation_discount=liquidation_discount, + ) + log IController.Repay( + user=_for, collateral_decrease=0, loan_decrease=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 - - self._save_rate() - - -@internal -@view -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 - @param user User address to calculate health for - @param debt The amount of debt to calculate health for - @param full Whether to take into account the price difference above the highest user's band - @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(staticcall AMM.get_x_down(user), int256) * health, convert(debt, int256)) - 10**18 - - if full: - 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) * (staticcall AMM.get_sum_xy(user))[1] * COLLATERAL_PRECISION, debt * BORROWED_PRECISION), int256) - - return health - - -@external -@view -@nonreentrant -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 - @param d_collateral Change in collateral amount (signed) - @param d_debt Change in debt amount (signed) - @param full Whether it's a 'full' health or not - @param N Number of bands in case loan doesn't yet exist - @return Signed health value - """ - ns: int256[2] = staticcall 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 = 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(10**18, 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, 10**18) - 10**18 - - 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 - - -@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) + ctrl.loan[_for] = IController.Loan(initial_debt=debt, rate_mul=rate_mul) + total_debt: uint256 = ( + ctrl._total_debt.initial_debt * rate_mul // ctrl._total_debt.rate_mul + ) + ctrl._total_debt.initial_debt = unsafe_sub(max(total_debt, d_debt), d_debt) + ctrl._total_debt.rate_mul = rate_mul - return f_remove + ctrl._save_rate() @internal -def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint256, callbacker: address, calldata: Bytes[10**4]): +def _liquidate( + user: address, + min_x: uint256, + health_limit: uint256, + frac: uint256, + callbacker: address, + calldata: Bytes[10**4], +): """ @notice Perform a bad liquidation of user if the health is too bad @param user Address of the user @@ -973,10 +577,12 @@ def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint2 """ debt: uint256 = 0 rate_mul: uint256 = 0 - debt, rate_mul = self._debt(user) + debt, rate_mul = ctrl._debt(user) if health_limit != 0: - assert self._health(user, debt, True, health_limit) < 0, "Not enough rekt" + assert ( + ctrl._health(user, debt, True, health_limit) < 0 + ), "Not enough rekt" final_debt: uint256 = debt debt = unsafe_div(debt * frac + (10**18 - 1), 10**18) @@ -988,61 +594,105 @@ def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint2 # 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] = extcall AMM.withdraw(user, self._get_f_remove(frac, health_limit)) # [stable, collateral] + xy: uint256[2] = extcall ctrl.AMM.withdraw( + user, ctrl._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) + ctrl.transferFrom(ctrl.BORROWED_TOKEN, ctrl.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]) + ctrl.transferFrom( + ctrl.COLLATERAL_TOKEN, ctrl.AMM.address, msg.sender, xy[1] + ) # Request what's left from user - self.transferFrom(BORROWED_TOKEN, msg.sender, self, to_repay) + ctrl.transferFrom(ctrl.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]) + ctrl.transferFrom( + ctrl.COLLATERAL_TOKEN, ctrl.AMM.address, callbacker, xy[1] + ) # Callback - cb: CallbackData = self.execute_callback( - callbacker, CALLBACK_LIQUIDATE, user, xy[0], xy[1], debt, calldata) + cb: IController.CallbackData = ctrl.execute_callback( + callbacker, + ctrl.CALLBACK_LIQUIDATE, + user, + xy[0], + xy[1], + debt, + calldata, + ) 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) - + ctrl.transferFrom( + ctrl.BORROWED_TOKEN, + callbacker, + msg.sender, + unsafe_sub(cb.stablecoins, to_repay), + ) + ctrl.transferFrom(ctrl.BORROWED_TOKEN, callbacker, self, to_repay) + ctrl.transferFrom( + ctrl.COLLATERAL_TOKEN, callbacker, msg.sender, cb.collateral + ) else: # Withdraw collateral - self.transferFrom(COLLATERAL_TOKEN, AMM.address, msg.sender, xy[1]) + ctrl.transferFrom( + ctrl.COLLATERAL_TOKEN, ctrl.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)) - + ctrl.transferFrom( + ctrl.BORROWED_TOKEN, + ctrl.AMM.address, + msg.sender, + unsafe_sub(xy[0], debt), + ) self.redeemed += debt - self.loan[user] = Loan(initial_debt=final_debt, rate_mul=rate_mul) - log Repay(user=user, collateral_decrease=xy[1], loan_decrease=debt) - log Liquidate(liquidator=msg.sender, user=user, collateral_received=xy[1], stablecoin_received=xy[0], debt=debt) + ctrl.loan[user] = IController.Loan( + initial_debt=final_debt, rate_mul=rate_mul + ) + log IController.Repay( + user=user, collateral_decrease=xy[1], loan_decrease=debt + ) + log IController.Liquidate( + liquidator=msg.sender, + user=user, + collateral_received=xy[1], + stablecoin_received=xy[0], + debt=debt, + ) if final_debt == 0: - log 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) + 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 + ctrl._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 + d: uint256 = ( + ctrl._total_debt.initial_debt * rate_mul // ctrl._total_debt.rate_mul + ) + ctrl._total_debt.initial_debt = unsafe_sub(max(d, debt), debt) + ctrl._total_debt.rate_mul = rate_mul - self._save_rate() + ctrl._save_rate() @external -@nonreentrant -def liquidate(user: address, min_x: uint256, frac: uint256 = 10**18, callbacker: address = empty(address), calldata: Bytes[10**4] = b""): +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 (or self-liquidation) of user if health is not good @param min_x Minimal amount of stablecoin to receive (to avoid liquidators being sandwiched) @@ -1051,128 +701,26 @@ def liquidate(user: address, min_x: uint256, frac: uint256 = 10**18, callbacker: @param calldata Any data for callbacker """ 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, calldata) - - -@view -@external -@nonreentrant -def tokens_to_liquidate(user: address, frac: uint256 = 10 ** 18) -> uint256: - """ - @notice Calculate the amount of stablecoins 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 - """ - health_limit: uint256 = 0 - if not self._check_approval(user): - health_limit = self.liquidation_discounts[user] - stablecoins: uint256 = unsafe_div((staticcall 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) - - return unsafe_sub(max(debt, stablecoins), stablecoins) - - -@view -@external -@nonreentrant -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]) - - -@view -@external -@nonreentrant -def users_to_liquidate(_from: uint256=0, _limit: uint256=0) -> DynArray[Position, 1000]: - """ - @notice Returns a dynamic array of users who can be "hard-liquidated". - This method is designed for convenience of liquidation bots. - @param _from Loan index to start iteration from - @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: uint256 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] = staticcall 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 -@view -@external -def amm_price() -> uint256: - """ - @notice Current price from the AMM - """ - return staticcall AMM.get_p() - - -@view -@external -@nonreentrant -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 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 -@nonreentrant -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) - """ - 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)[0], convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256)] + if not ctrl._check_approval(user): + discount = ctrl.liquidation_discounts[user] + self._liquidate( + user, min_x, discount, min(frac, 10**18), callbacker, calldata + ) -# 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 TODO check this one + @param fee The fee which should be no higher than MAX_AMM_FEE """ assert msg.sender == staticcall FACTORY.admin() - assert fee <= MAX_AMM_FEE and fee >= MIN_AMM_FEE, "Fee" - extcall AMM.set_fee(fee) + assert fee <= ctrl.MAX_AMM_FEE and fee >= ctrl.MIN_AMM_FEE, "Fee" + extcall ctrl.AMM.set_fee(fee) -@nonreentrant @external def set_monetary_policy(monetary_policy: address): """ @@ -1180,14 +728,15 @@ def set_monetary_policy(monetary_policy: address): @param monetary_policy Address of the monetary policy contract """ assert msg.sender == staticcall FACTORY.admin() - self.monetary_policy = MonetaryPolicy(monetary_policy) - extcall MonetaryPolicy(monetary_policy).rate_write() - log SetMonetaryPolicy(monetary_policy=monetary_policy) + ctrl._monetary_policy = IMonetaryPolicy(monetary_policy) + extcall IMonetaryPolicy(monetary_policy).rate_write() + log IController.SetMonetaryPolicy(monetary_policy=monetary_policy) -@nonreentrant @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 @@ -1195,22 +744,23 @@ def set_borrowing_discounts(loan_discount: uint256, liquidation_discount: uint25 """ assert msg.sender == staticcall FACTORY.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=loan_discount, liquidation_discount=liquidation_discount) + assert liquidation_discount >= ctrl.MIN_LIQUIDATION_DISCOUNT + assert loan_discount <= ctrl.MAX_LOAN_DISCOUNT + ctrl.liquidation_discount = liquidation_discount + ctrl.loan_discount = loan_discount + log IController.SetBorrowingDiscounts( + loan_discount=loan_discount, liquidation_discount=liquidation_discount + ) @external -@nonreentrant -def set_callback(cb: address): +def set_callback(cb: ILMGauge): """ @notice Set liquidity mining callback """ assert msg.sender == staticcall FACTORY.admin() - extcall AMM.set_callback(cb) - log SetLMCallback(callback=cb) + extcall ctrl.AMM.set_callback(cb) + log IController.SetLMCallback(callback=cb) @external @@ -1220,11 +770,12 @@ def admin_fees() -> uint256: @notice Calculate the amount of fees obtained from the interest """ minted: uint256 = self.minted - return unsafe_sub(max(self._get_total_debt() + self.redeemed, minted), minted) + return unsafe_sub( + max(ctrl._get_total_debt() + self.redeemed, minted), minted + ) @external -@nonreentrant def collect_fees() -> uint256: """ @notice Collect the fees charged as interest. @@ -1232,13 +783,13 @@ def collect_fees() -> uint256: _to: address = staticcall FACTORY.fee_receiver() # Borrowing-based fees - rate_mul: uint256 = staticcall AMM.get_rate_mul() - loan: Loan = self._total_debt + rate_mul: uint256 = staticcall ctrl.AMM.get_rate_mul() + loan: IController.Loan = ctrl._total_debt loan.initial_debt = loan.initial_debt * rate_mul // loan.rate_mul loan.rate_mul = rate_mul - self._total_debt = loan + ctrl._total_debt = loan - self._save_rate() + ctrl._save_rate() # Amount which would have been redeemed if all the debt was repaid now to_be_redeemed: uint256 = loan.initial_debt + self.redeemed @@ -1247,39 +798,14 @@ def collect_fees() -> uint256: # 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(amount=to_be_redeemed, new_supply=loan.initial_debt) + to_be_redeemed = unsafe_sub( + to_be_redeemed, minted + ) # Now this is the fees to charge + ctrl.transfer(ctrl.BORROWED_TOKEN, _to, to_be_redeemed) + log IController.CollectFees( + amount=to_be_redeemed, new_supply=loan.initial_debt + ) return to_be_redeemed else: - log CollectFees(amount=0, new_supply=loan.initial_debt) + log IController.CollectFees(amount=0, new_supply=loan.initial_debt) return 0 - - -# Allowance methods - -@external -def approve(_spender: address, _allow: bool): - """ - @notice Allow another address to borrow and repay for the user - @param _spender Address to whitelist for the action - @param _allow Whether to turn the approval on or off (no amounts) - """ - self.approval[msg.sender][_spender] = _allow - log Approval(owner=msg.sender, spender=_spender, allow=_allow) - - -@internal -@view -def _check_approval(_for: address) -> bool: - return msg.sender == _for or self.approval[_for][msg.sender] - - -@external -def set_extra_health(_value: uint256): - """ - @notice Add a little bit more to loan_discount to start SL with health higher than usual - @param _value 1e18-based addition to loan_discount - """ - self.extra_health[msg.sender] = _value - log SetExtraHealth(user=msg.sender, health=_value) diff --git a/contracts/constants.vy b/contracts/constants.vy new file mode 100644 index 00000000..c21c773e --- /dev/null +++ b/contracts/constants.vy @@ -0,0 +1,6 @@ +MAX_TICKS_UINT: constant(uint256) = 50 +MAX_TICKS: constant(int256) = 50 + +DEAD_SHARES: constant(uint256) = 1000 +# TODO make sure this is used everywhere +WAD: constant(uint256) = 10**18 \ No newline at end of file diff --git a/contracts/controller_core.vy b/contracts/controller_core.vy new file mode 100644 index 00000000..e2625725 --- /dev/null +++ b/contracts/controller_core.vy @@ -0,0 +1,811 @@ +# pragma version 0.4.3 +# pragma nonreentrancy on + +from contracts.interfaces import IAMM +from contracts.interfaces import IMonetaryPolicy +from contracts.interfaces import IController +from ethereum.ercs import IERC20 +from ethereum.ercs import IERC20Detailed + +from snekmate.utils import math + +################################################################ +# IMMUTABLES # +################################################################ + +AMM: immutable(IAMM) +MAX_AMM_FEE: immutable( + uint256 +) # let's set to MIN_TICKS / A: for example, 4% max fee for A=100 +A: immutable(uint256) +Aminus1: immutable(uint256) +LOGN_A_RATIO: immutable(int256) # log(A / (A - 1)) +SQRT_BAND_RATIO: immutable(uint256) + +COLLATERAL_TOKEN: immutable(IERC20) +COLLATERAL_PRECISION: immutable(uint256) +BORROWED_TOKEN: immutable(IERC20) +BORROWED_PRECISION: immutable(uint256) + +################################################################ +# CONSTANTS # +################################################################ + + +from contracts import constants as c + +WAD: constant(uint256) = c.WAD +DEAD_SHARES: constant(uint256) = c.DEAD_SHARES + + +MIN_AMM_FEE: constant(uint256) = 10**6 # 1e-12, still needs to be above 0 +MIN_TICKS_UINT: constant(uint256) = 4 + +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, +) + +MAX_LOAN_DISCOUNT: constant(uint256) = 5 * 10**17 +MIN_LIQUIDATION_DISCOUNT: constant(uint256) = ( + 10**16 +) # Start liquidating when threshold reached +MAX_TICKS: constant(int256) = 50 +MAX_TICKS_UINT: constant(uint256) = c.MAX_TICKS_UINT +MIN_TICKS: constant(int256) = 4 +MAX_SKIP_TICKS: constant(uint256) = 1024 +MAX_P_BASE_BANDS: constant(int256) = 5 + +MAX_RATE: constant(uint256) = 43959106799 # 300% APY + +################################################################ +# STORAGE # +################################################################ + +liquidation_discount: public(uint256) +loan_discount: public(uint256) +# TODO make settable +_monetary_policy: IMonetaryPolicy +# TODO can't mark it as public, likely a compiler bug +# TODO make an issue +@external +@view +def monetary_policy() -> IMonetaryPolicy: + """ + @notice Address of the monetary policy + """ + return self._monetary_policy + + +approval: public(HashMap[address, HashMap[address, bool]]) +extra_health: public(HashMap[address, uint256]) + +loan: HashMap[address, IController.Loan] +liquidation_discounts: public(HashMap[address, uint256]) +_total_debt: IController.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 + + +@deploy +def __init__( + _AMM: IAMM, + _collateral_token: IERC20, + _borrowed_token: IERC20, + monetary_policy: IMonetaryPolicy, + loan_discount: uint256, + liquidation_discount: uint256, +): + AMM = _AMM + + A = staticcall AMM.A() + Aminus1 = A - 1 + + # TODO check math (removed unsafe) + LOGN_A_RATIO = math._wad_ln(convert(A * WAD // A - 1, int256)) + # TODO check math + SQRT_BAND_RATIO = isqrt(10**36 * A // (A - 1)) + + MAX_AMM_FEE = min(WAD * MIN_TICKS_UINT // A, 10**17) + + COLLATERAL_TOKEN = _collateral_token + collateral_decimals: uint256 = convert( + staticcall IERC20Detailed(COLLATERAL_TOKEN.address).decimals(), uint256 + ) + COLLATERAL_PRECISION = pow_mod256(10, 18 - collateral_decimals) + + BORROWED_TOKEN = _borrowed_token + borrowed_decimals: uint256 = convert( + staticcall IERC20Detailed(BORROWED_TOKEN.address).decimals(), uint256 + ) + BORROWED_PRECISION = pow_mod256(10, 18 - borrowed_decimals) + + self._monetary_policy = monetary_policy + self.liquidation_discount = liquidation_discount + self.loan_discount = loan_discount + self._total_debt.rate_mul = 10**18 + + # TODO check what this is needed for + assert extcall BORROWED_TOKEN.approve( + msg.sender, max_value(uint256), default_return_value=True + ) + + +################################################################ +# BUILDING BLOCKS # +################################################################ + + +@internal +@view +def _debt(user: address) -> (uint256, uint256): + """ + @notice Get the value of debt and rate_mul and update the rate_mul counter + @param user User address + @return (debt, rate_mul) + """ + rate_mul: uint256 = staticcall AMM.get_rate_mul() + loan: IController.Loan = self.loan[user] + if loan.initial_debt == 0: + return (0, rate_mul) + else: + # Let user repay 1 smallest decimal more so that the system doesn't lose on precision + # Use ceil div + debt: uint256 = loan.initial_debt * rate_mul + 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 + return (debt, rate_mul) + + +@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 +@view +def get_y_effective( + collateral: uint256, N: uint256, discount: uint256 +) -> uint256: + """ + @notice Intermediary method which calculates y_effective defined as x_effective / p_base, + however discounted by loan_discount. + x_effective is an amount which can be obtained from collateral when liquidating + @param collateral Amount of collateral to get the value for + @param N Number of bands the deposit is made into + @param discount Loan discount at 1e18 base (e.g. 1e18 == 100%) + @return y_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_effective * p_oracle_up(n1) + # d_y_effective = y / N / sqrt(A / (A - 1)) + # 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, + ), + ), + unsafe_mul(SQRT_BAND_RATIO, N), + ) + y_effective: uint256 = d_y_effective + for i: uint256 in range(1, MAX_TICKS_UINT): + if i == N: + break + d_y_effective = unsafe_div(d_y_effective * Aminus1, 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: + """ + @notice Calculate the upper band number for the deposit to sit in to support + the given debt. 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 + @return Upper band n1 (n1 <= n2) to deposit into. Signed integer + """ + assert debt > 0, "No loan" + 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], + ) + # p_oracle_up(n1) = base_price * ((A - 1) / A)**n1 + + # We borrow up until min band touches p_oracle, + # or it touches non-empty bands which cannot be skipped. + # We calculate required n1 for given (collateral, debt), + # and if n1 corresponds to price_oracle being too high, or unreachable band + # - 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 + + # 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 = 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_div(n1, LOGN_A_RATIO) + + n1 = min(n1, 1024 - convert(N, int256)) + n0 + if n1 <= n0: + assert staticcall AMM.can_skip_bands(n1 - 1), "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 = 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() * 10**18 // 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 = unsafe_div(p_base * A, Aminus1) + if p_base > p_oracle: + return p_base_prev + return p_base + + +@internal +@view +def _check_approval(_for: address) -> bool: + return msg.sender == _for or self.approval[_for][msg.sender] + + +@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 + ) + + return f_remove + + +@internal +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 + self.loan_ix[_for] = 0 + if loan_ix < last_loan_ix: # Need to replace + last_loan: address = self.loans[last_loan_ix] + self.loans[loan_ix] = last_loan + self.loan_ix[last_loan] = loan_ix + self.n_loans = last_loan_ix + + +@internal +def transferFrom(token: IERC20, _from: address, _to: address, amount: uint256): + if amount > 0: + assert extcall token.transferFrom( + _from, _to, amount, default_return_value=True + ) + + +@internal +def transfer(token: IERC20, _to: address, amount: uint256): + if amount > 0: + assert extcall token.transfer(_to, amount, default_return_value=True) + + +@internal +@view +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 + @param user User address to calculate health for + @param debt The amount of debt to calculate health for + @param full Whether to take into account the price difference above the highest user's band + @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(staticcall AMM.get_x_down(user), int256) * health, + convert(debt, int256), + ) + - 10**18 + ) + + if full: + 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) + * (staticcall AMM.get_sum_xy(user))[1] + * COLLATERAL_PRECISION, + debt * BORROWED_PRECISION, + ), + int256, + ) + return health + + +@internal +def _save_rate(): + """ + @notice Save current rate + """ + rate: uint256 = min(extcall self._monetary_policy.rate_write(), MAX_RATE) + extcall AMM.set_rate(rate) + + +@internal +def execute_callback( + callbacker: address, + callback_sig: bytes4, + user: address, + stablecoins: uint256, + collateral: uint256, + debt: uint256, + calldata: Bytes[10**4], +) -> IController.CallbackData: + assert callbacker != COLLATERAL_TOKEN.address + assert callbacker != BORROWED_TOKEN.address + + 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, calldata), + ), + max_outsize=64, + ) + data.stablecoins = convert(slice(response, 0, 32), uint256) + data.collateral = convert(slice(response, 32, 32), uint256) + + # Checks after callback + 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 + + +################################################################ +# FIGURE OUT A SECTION NAME # +################################################################ + +@external +def approve(_spender: address, _allow: bool): + """ + @notice Allow another address to borrow and repay for the user + @param _spender Address to whitelist for the action + @param _allow Whether to turn the approval on or off (no amounts) + """ + self.approval[msg.sender][_spender] = _allow + log IController.Approval(owner=msg.sender, spender=_spender, allow=_allow) + + +@external +def set_extra_health(_value: uint256): + """ + @notice Add a little bit more to loan_discount to start SL with health higher than usual + @param _value 1e18-based addition to loan_discount + """ + self.extra_health[msg.sender] = _value + log IController.SetExtraHealth(user=msg.sender, health=_value) + + +@external +def save_rate(): + """ + @notice Save current rate + """ + self._save_rate() + + +################################################################ +# VIEW METHODS # +################################################################ + +@external +@view +@reentrant +def amm() -> IAMM: + """ + @notice Address of the AMM + """ + return AMM + + +@external +@view +@reentrant +def collateral_token() -> IERC20: + """ + @notice Address of the collateral token + """ + return COLLATERAL_TOKEN + + +@external +@view +@reentrant +def borrowed_token() -> IERC20: + """ + @notice Address of the borrowed token + """ + return BORROWED_TOKEN + + +@external +@view +def debt(user: address) -> uint256: + """ + @notice Get the value of debt without changing the state + @param user User address + @return Value of debt + """ + return self._debt(user)[0] + + +@external +@view +def loan_exists(user: address) -> bool: + """ + @notice Check whether there is a loan of `user` in existence + """ + return self.loan[user].initial_debt > 0 + + +@external +@view +@reentrant +def total_debt() -> uint256: + """ + @notice Total debt of this controller + @dev Marked as reentrant because used by monetary policy + # TODO check if @reentrant is actually needed + """ + return self._get_total_debt() + + + + + +@external +@view +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 + @param user User to calculate the value for (only necessary for nonzero extra_health) + @return Minimal collateral required + """ + # 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, + ) + + +@external +@view +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. + @param collateral Amount of collateral (at its native precision) + @param debt Amount of requested debt + @param N Number of bands to deposit into + @param user User to calculate n1 for (only necessary for nonzero extra_health) + @return Upper band n1 (n1 <= n2) to deposit into. Signed integer + """ + return self._calculate_debt_n1(collateral, debt, N, user) + + +@view +@external +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 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 +@reentrant +def amm_price() -> uint256: + """ + @notice Current price from the AMM + @dev Marked as reentrant because AMM has a nonreentrant decorator + # TODO check if @reentrant is actually needed + """ + return staticcall AMM.get_p() + + +@view +@external +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) + """ + 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)[0], + convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256), + ] + + +@external +@view +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 + @param d_collateral Change in collateral amount (signed) + @param d_debt Change in debt amount (signed) + @param full Whether it's a 'full' health or not + @param N Number of bands in case loan doesn't yet exist + @return Signed health value + """ + ns: int256[2] = staticcall 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 = 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(10**18, 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, 10**18) - 10**18 + + 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 tokens_to_liquidate(user: address, frac: uint256 = 10**18) -> uint256: + """ + @notice Calculate the amount of stablecoins 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 + """ + health_limit: uint256 = 0 + if not self._check_approval(user): + health_limit = self.liquidation_discounts[user] + stablecoins: uint256 = unsafe_div( + (staticcall 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) + + return unsafe_sub(max(debt, stablecoins), stablecoins) + + +@view +@external +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] + ) + + +@view +@external +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. + @param _from Loan index to start iteration from + @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[IController.Position, 1000] = [] + for i: uint256 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] = staticcall AMM.get_sum_xy(user) + out.append( + IController.Position( + user=user, x=xy[0], y=xy[1], debt=debt, health=health + ) + ) + ix += 1 + return out diff --git a/contracts/interfaces/IAMM.vyi b/contracts/interfaces/IAMM.vyi index 2c51b205..54f4d854 100644 --- a/contracts/interfaces/IAMM.vyi +++ b/contracts/interfaces/IAMM.vyi @@ -271,11 +271,10 @@ def max_band() -> int256: ... -# TODO check with vyper team -# @view -# @external -# def price_oracle_contract() -> IPriceOracle: - # ... +@view +@external +def price_oracle_contract() -> IPriceOracle: + ... @view @@ -290,15 +289,14 @@ def bands_y(arg0: int256) -> uint256: ... -# TODO check with vyper team -# @view -# @external -# def user_shares(arg0: address) -> UserTicks: - # ... +@view +@external +def user_shares(arg0: address) -> UserTicks: + ... + -# TODO check with vyper team -# @view -# @external -# def liquidity_mining_callback() -> ILMGauge: - # ... +@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..f34d0bbe --- /dev/null +++ b/contracts/interfaces/IController.vyi @@ -0,0 +1,327 @@ +from ethereum.ercs import IERC20 +from contracts.interfaces import ILMGauge +from contracts.interfaces import IAMM +from contracts.interfaces import IMonetaryPolicy + +# Structs + +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 + + +# Events + +event UserState: + user: address + collateral: uint256 + debt: uint256 + n1: int256 + n2: int256 + liquidation_discount: uint256 + + +event Borrow: + user: address + collateral_increase: uint256 + loan_increase: uint256 + + +event Repay: + user: address + collateral_decrease: uint256 + loan_decrease: uint256 + + +event RemoveCollateral: + user: address + collateral_decrease: uint256 + + +event Liquidate: + liquidator: address + user: 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: address + health: uint256 + + +event CollectFees: + amount: uint256 + new_supply: uint256 + + +event SetLMCallback: + callback: ILMGauge + + +event Approval: + owner: address + spender: address + allow: bool + + +# Functions + +@view +@external +def factory() -> address: + ... + + +@view +@external +def amm() -> IAMM: + ... + + +@view +@external +def collateral_token() -> IERC20: + ... + + +@view +@external +def borrowed_token() -> IERC20: + ... + + +@external +def save_rate(): + ... + + +@view +@external +def debt(user: address) -> uint256: + ... + + +@view +@external +def loan_exists(user: address) -> bool: + ... + + +@view +@external +def total_debt() -> uint256: + ... + + +@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: + ... + + +@view +@external +def calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256, user: address) -> int256: + ... + + +@external +def create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address, callbacker: address, calldata: Bytes[10000]): + ... + + +@external +def add_collateral(collateral: uint256, _for: address): + ... + + +@external +def remove_collateral(collateral: uint256, _for: address): + ... + + +@external +def borrow_more(collateral: uint256, debt: uint256, _for: address, callbacker: address, calldata: Bytes[10000]): + ... + + +@external +def repay(_d_debt: uint256, _for: address, max_active_band: int256, callbacker: address, calldata: Bytes[10000]): + ... + + +@view +@external +def health_calculator(user: address, d_collateral: int256, d_debt: int256, full: bool, N: uint256) -> int256: + ... + + +@external +def liquidate(user: address, min_x: uint256, frac: uint256, callbacker: address, calldata: Bytes[10000]): + ... + + +@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 amm_price() -> uint256: + ... + + +@view +@external +def user_prices(user: address) -> uint256[2]: + ... + + +@view +@external +def user_state(user: address) -> uint256[4]: + ... + + +@external +def set_amm_fee(fee: uint256): + ... + + +@external +def set_monetary_policy(monetary_policy: address): + ... + + +@external +def set_borrowing_discounts(loan_discount: uint256, liquidation_discount: uint256): + ... + + +@external +def set_callback(cb: ILMGauge): + ... + + +@view +@external +def admin_fees() -> uint256: + ... + + +@external +def collect_fees() -> uint256: + ... + + +@external +def approve(_spender: address, _allow: bool): + ... + + +@external +def set_extra_health(_value: 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 monetary_policy() -> IMonetaryPolicy: + ... + + +@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: + ... + diff --git a/contracts/interfaces/IFactory.vyi b/contracts/interfaces/IFactory.vyi new file mode 100644 index 00000000..a71bd497 --- /dev/null +++ b/contracts/interfaces/IFactory.vyi @@ -0,0 +1,15 @@ +from ethereum.ercs import IERC20 + +@view +def stablecoin() -> IERC20: + ... + +@view +def admin() -> address: + ... + +@view +def fee_receiver() -> address: + ... + +# TODO double check view decorators on non generated interfaces \ No newline at end of file diff --git a/contracts/interfaces/ILMGauge.vyi b/contracts/interfaces/ILMGauge.vyi index b529d526..11d76d07 100644 --- a/contracts/interfaces/ILMGauge.vyi +++ b/contracts/interfaces/ILMGauge.vyi @@ -1,5 +1,5 @@ -# TODO import this one? -MAX_TICKS_UINT: constant(uint256) = 50 +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): ... diff --git a/contracts/interfaces/ILlamalendController.vyi b/contracts/interfaces/ILlamalendController.vyi new file mode 100644 index 00000000..d8411338 --- /dev/null +++ b/contracts/interfaces/ILlamalendController.vyi @@ -0,0 +1,2 @@ +event SetBorrowCap: + borrow_cap: uint256 \ No newline at end of file diff --git a/contracts/interfaces/IMintController.vyi b/contracts/interfaces/IMintController.vyi new file mode 100644 index 00000000..168c1cf7 --- /dev/null +++ b/contracts/interfaces/IMintController.vyi @@ -0,0 +1,10 @@ +@view +@external +def minted() -> uint256: + ... + + +@view +@external +def redeemed() -> uint256: + ... \ No newline at end of file diff --git a/contracts/interfaces/IMonetaryPolicy.vyi b/contracts/interfaces/IMonetaryPolicy.vyi new file mode 100644 index 00000000..865808ba --- /dev/null +++ b/contracts/interfaces/IMonetaryPolicy.vyi @@ -0,0 +1,2 @@ +def rate_write() -> uint256: + ... \ No newline at end of file diff --git a/contracts/interfaces/IVault.vyi b/contracts/interfaces/IVault.vyi new file mode 100644 index 00000000..bb4500ea --- /dev/null +++ b/contracts/interfaces/IVault.vyi @@ -0,0 +1,26 @@ +from ethereum.ercs import IERC20 + +@view +def collateral_token() -> IERC20: + ... + +@view +def borrowed_token() -> IERC20: + ... + +@view +def deposited() -> uint256: + ... + +@view +def withdrawn() -> uint256: + ... + +@view +def admin() -> address: + ... + +@view +def fee_receiver() -> address: + ... + diff --git a/contracts/lending/Controller.vy b/contracts/lending/Controller.vy index c321d22d..64a3a953 100644 --- a/contracts/lending/Controller.vy +++ b/contracts/lending/Controller.vy @@ -1,189 +1,83 @@ -# pragma version 0.4.1 +# pragma version 0.4.3 +# pragma nonreentrancy on # pragma optimize codesize -# pragma evm-version shanghai """ @title LlamaLend Controller @author Curve.Fi @license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved """ -from snekmate.utils import math from ethereum.ercs import IERC20 from ethereum.ercs import IERC20Detailed +from contracts.interfaces import IAMM +from contracts.interfaces import ILMGauge +from contracts.interfaces import IMonetaryPolicy +from contracts.interfaces import IVault +from contracts.interfaces import IController +implements: IController +from contracts.interfaces import ILlamalendController -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 price_oracle() -> uint256: view - def can_skip_bands(n_end: int256) -> bool: view - 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 MonetaryPolicy: - def rate_write() -> uint256: nonpayable - -interface Vault: - def admin() -> address: view - def fee_receiver() -> address: view - def borrowed_token() -> address: view - def collateral_token() -> address: view - def deposited() -> uint256: view - def withdrawn() -> uint256: 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 - -event SetBorrowCap: - borrow_cap: uint256 - - -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 - - -VAULT: immutable(Vault) -MAX_LOAN_DISCOUNT: constant(uint256) = 5 * 10**17 -MIN_LIQUIDATION_DISCOUNT: constant(uint256) = 10**16 # Start liquidating when threshold reached -MAX_TICKS: constant(int256) = 50 -MAX_TICKS_UINT: constant(uint256) = 50 -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 - -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 - -lent: public(uint256) # cumulative amount of assets ever lent -repaid: public(uint256) # cumulative amount of assets ever repaid -processed: public(uint256) # cumulative amount of assets admin fees have been taken from -collected: public(uint256) # cumulative amount of assets collected by admin - -monetary_policy: public(MonetaryPolicy) -liquidation_discount: public(uint256) -loan_discount: public(uint256) - -COLLATERAL_TOKEN: immutable(IERC20) -COLLATERAL_PRECISION: immutable(uint256) - -BORROWED_TOKEN: immutable(IERC20) -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) - -MAX_ADMIN_FEE: constant(uint256) = 2 * 10**17 # 20% -MIN_AMM_FEE: constant(uint256) = 10**6 # 1e-12, still needs to be above 0 -MAX_AMM_FEE: immutable(uint256) # let's set to MIN_TICKS / A: for example, 4% max fee for A=100 - -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) - -DEAD_SHARES: constant(uint256) = 1000 +from snekmate.utils import math +from contracts import controller_core as ctrl + +initializes: ctrl + +exports: ( + ctrl.amm, + ctrl.amm_price, + ctrl.approval, + ctrl.approve, + ctrl.borrowed_token, + ctrl.calculate_debt_n1, + ctrl.collateral_token, + ctrl.debt, + ctrl.extra_health, + ctrl.health, + ctrl.health_calculator, + ctrl.liquidation_discount, + ctrl.liquidation_discounts, + ctrl.loan_discount, + ctrl.loan_exists, + ctrl.loan_ix, + ctrl.loans, + # ctrl.max_borrowable, # TODO check this one for diffs + ctrl.min_collateral, + ctrl.monetary_policy, + ctrl.n_loans, + ctrl.save_rate, + ctrl.set_extra_health, + ctrl.tokens_to_liquidate, + ctrl.total_debt, + ctrl.user_prices, + ctrl.user_state, + ctrl.users_to_liquidate, +) + +VAULT: immutable(IVault) + +# cumulative amount of assets ever lent +lent: public(uint256) +# cumulative amount of assets ever repaid +repaid: public(uint256) +# cumulative amount of assets admin fees have been taken from +processed: public(uint256) +# cumulative amount of assets collected by admin +collected: public(uint256) -approval: public(HashMap[address, HashMap[address, bool]]) -extra_health: public(HashMap[address, uint256]) borrow_cap: public(uint256) admin_fee: public(uint256) +MAX_ADMIN_FEE: constant(uint256) = 2 * 10**17 # 20% + @deploy def __init__( - collateral_token: address, - monetary_policy: address, - loan_discount: uint256, - liquidation_discount: uint256, - amm: address): + monetary_policy: IMonetaryPolicy, + loan_discount: uint256, + liquidation_discount: uint256, + amm: IAMM, +): """ @notice Controller constructor deployed by the factory from blueprint @param collateral_token Token to use for collateral @@ -193,31 +87,19 @@ def __init__( get_x_down() for "bad liquidation" purposes @param amm AMM address (Already deployed from blueprint) """ - VAULT = Vault(msg.sender) - - self.monetary_policy = MonetaryPolicy(monetary_policy) - - self.liquidation_discount = liquidation_discount - self.loan_discount = loan_discount - self._total_debt.rate_mul = 10**18 - - AMM = LLAMMA(amm) - _A: uint256 = staticcall LLAMMA(amm).A() - A = _A - Aminus1 = unsafe_sub(_A, 1) - LOGN_A_RATIO = math._wad_ln(convert(unsafe_div(_A * 10**18, unsafe_sub(_A, 1)), int256)) - MAX_AMM_FEE = min(unsafe_div(10**18 * MIN_TICKS_UINT, A), 10**17) + VAULT = IVault(msg.sender) - COLLATERAL_TOKEN = IERC20(staticcall Vault(msg.sender).collateral_token()) - collateral_decimals: uint256 = convert(staticcall IERC20Detailed(COLLATERAL_TOKEN.address).decimals(), uint256) - COLLATERAL_PRECISION = pow_mod256(10, 18 - collateral_decimals) - BORROWED_TOKEN = IERC20(staticcall Vault(msg.sender).borrowed_token()) - borrowed_decimals: uint256 = convert(staticcall IERC20Detailed(BORROWED_TOKEN.address).decimals(), uint256) - BORROWED_PRECISION = pow_mod256(10, 18 - borrowed_decimals) + collateral_token: IERC20 = staticcall VAULT.collateral_token() + borrowed_token: IERC20 = staticcall VAULT.borrowed_token() - SQRT_BAND_RATIO = isqrt(unsafe_div(10**36 * _A, unsafe_sub(_A, 1))) - - assert extcall BORROWED_TOKEN.approve(msg.sender, max_value(uint256), default_return_value=True) + ctrl.__init__( + amm, + collateral_token, + borrowed_token, + monetary_policy, + loan_discount, + liquidation_discount, + ) @external @@ -229,116 +111,17 @@ def factory() -> address: return VAULT.address -@external -@view -def amm() -> LLAMMA: - """ - @notice Address of the AMM - """ - return AMM - - -@external -@view -def collateral_token() -> IERC20: - """ - @notice Address of the collateral token - """ - return COLLATERAL_TOKEN - - -@external -@view -def borrowed_token() -> IERC20: - """ - @notice Address of the borrowed token - """ - return BORROWED_TOKEN - - @internal -def _save_rate(): - """ - @notice Save current rate - """ - rate: uint256 = min(extcall self.monetary_policy.rate_write(), MAX_RATE) - extcall AMM.set_rate(rate) - - -@external -@nonreentrant -def save_rate(): - """ - @notice Save current rate - """ - self._save_rate() - - -@internal -@view -def _debt(user: address) -> (uint256, uint256): - """ - @notice Get the value of debt and rate_mul and update the rate_mul counter - @param user User address - @return (debt, rate_mul) - """ - rate_mul: uint256 = staticcall AMM.get_rate_mul() - loan: Loan = self.loan[user] - if loan.initial_debt == 0: - return (0, rate_mul) - else: - # Let user repay 1 smallest decimal more so that the system doesn't lose on precision - # Use ceil div - debt: uint256 = loan.initial_debt * rate_mul - 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 - return (debt, rate_mul) - - -@external -@view -@nonreentrant -def debt(user: address) -> uint256: - """ - @notice Get the value of debt without changing the state - @param user User address - @return Value of debt - """ - return self._debt(user)[0] - - -@external -@view -@nonreentrant -def loan_exists(user: address) -> bool: - """ - @notice Check whether there is a loan of `user` in existence - """ - return self.loan[user].initial_debt > 0 - - -@internal -@view -def _get_total_debt() -> uint256: - """ - @notice Total debt of this controller - """ - rate_mul: uint256 = staticcall AMM.get_rate_mul() - loan: 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) -> Loan: +def _update_total_debt( + d_debt: uint256, rate_mul: uint256, is_increase: bool +) -> IController.Loan: """ @param d_debt Change in debt amount (unsigned) @param rate_mul New rate_mul @param is_increase Whether debt increases or decreases @notice Update total debt of this controller """ - loan: Loan = self._total_debt + loan: IController.Loan = ctrl._total_debt loan.initial_debt = loan.initial_debt * rate_mul // loan.rate_mul if is_increase: loan.initial_debt += d_debt @@ -346,147 +129,38 @@ def _update_total_debt(d_debt: uint256, rate_mul: uint256, is_increase: bool) -> else: loan.initial_debt = unsafe_sub(max(loan.initial_debt, d_debt), d_debt) loan.rate_mul = rate_mul - self._total_debt = loan + ctrl._total_debt = loan return loan -# No decorator because used in monetary policy -@external -@view -def total_debt() -> uint256: - """ - @notice Total debt of this controller - """ - return self._get_total_debt() - - -@internal -@view -def get_y_effective(collateral: uint256, N: uint256, discount: uint256) -> uint256: - """ - @notice Intermediary method which calculates y_effective defined as x_effective / p_base, - however discounted by loan_discount. - x_effective is an amount which can be obtained from collateral when liquidating - @param collateral Amount of collateral to get the value for - @param N Number of bands the deposit is made into - @param discount Loan discount at 1e18 base (e.g. 1e18 == 100%) - @return y_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_effective * p_oracle_up(n1) - # d_y_effective = y / N / sqrt(A / (A - 1)) - # 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) - ), - unsafe_mul(SQRT_BAND_RATIO, N)) - y_effective: uint256 = d_y_effective - for i: uint256 in range(1, MAX_TICKS_UINT): - if i == N: - break - d_y_effective = unsafe_div(d_y_effective * Aminus1, 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: - """ - @notice Calculate the upper band number for the deposit to sit in to support - the given debt. 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 - @return Upper band n1 (n1 <= n2) to deposit into. Signed integer - """ - assert debt > 0, "No loan" - 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]) - # p_oracle_up(n1) = base_price * ((A - 1) / A)**n1 - - # We borrow up until min band touches p_oracle, - # or it touches non-empty bands which cannot be skipped. - # We calculate required n1 for given (collateral, debt), - # and if n1 corresponds to price_oracle being too high, or unreachable band - # - 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 - - # 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 = 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_div(n1, LOGN_A_RATIO) - - n1 = min(n1, 1024 - convert(N, int256)) + n0 - if n1 <= n0: - 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 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 = 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() * 10**18 // 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 = unsafe_div(p_base * A, Aminus1) - if p_base > p_oracle: - return p_base_prev - - return p_base - @internal @view def _borrowed_balance() -> uint256: # (VAULT.deposited() - VAULT.withdrawn()) - (self.lent - self.repaid) - self.collected - return staticcall VAULT.deposited() + self.repaid - staticcall VAULT.withdrawn() - self.lent - self.collected + return ( + staticcall VAULT.deposited() + + self.repaid + - staticcall VAULT.withdrawn() + - self.lent + - self.collected + ) @external @view -@nonreentrant def borrowed_balance() -> uint256: return self._borrowed_balance() @external @view -@nonreentrant -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 @@ -509,130 +183,78 @@ def max_borrowable(collateral: uint256, N: uint256, current_debt: uint256 = 0, u # 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 + assert N >= ctrl.MIN_TICKS_UINT and N <= ctrl.MAX_TICKS_UINT - y_effective: uint256 = self.get_y_effective(collateral * COLLATERAL_PRECISION, N, - self.loan_discount + self.extra_health[user]) + y_effective: uint256 = ctrl.get_y_effective( + collateral * ctrl.COLLATERAL_PRECISION, + N, + ctrl.loan_discount + ctrl.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 + x: uint256 = unsafe_sub( + max(unsafe_div(y_effective * ctrl.max_p_base(), 10**18), 1), 1 + ) + x = unsafe_div( + x * (10**18 - 10**14), unsafe_mul(10**18, ctrl.BORROWED_PRECISION) + ) # Make it a bit smaller - _total_debt: uint256 = self._get_total_debt() + _total_debt: uint256 = ctrl._get_total_debt() _cap: uint256 = unsafe_sub(max(self.borrow_cap, _total_debt), _total_debt) _cap = min(self._borrowed_balance() + current_debt, _cap) - return min(x, _cap) # Cannot borrow beyond the amount of coins Controller has or beyond borrow_cap - - -@external -@view -@nonreentrant -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 - @param user User to calculate the value for (only necessary for nonzero extra_health) - @return Minimal collateral required - """ - # 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) - - -@external -@view -@nonreentrant -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. - @param collateral Amount of collateral (at its native precision) - @param debt Amount of requested debt - @param N Number of bands to deposit into - @param user User to calculate n1 for (only necessary for nonzero extra_health) - @return Upper band n1 (n1 <= n2) to deposit into. Signed integer - """ - return self._calculate_debt_n1(collateral, debt, N, user) - - -@internal -def transferFrom(token: IERC20, _from: address, _to: address, amount: uint256): - if amount > 0: - assert extcall token.transferFrom(_from, _to, amount, default_return_value=True) - - -@internal -def transfer(token: IERC20, _to: address, amount: uint256): - if amount > 0: - assert extcall token.transfer(_to, amount, default_return_value=True) + return min( + x, _cap + ) # Cannot borrow beyond the amount of coins Controller has or beyond borrow_cap -@internal -def execute_callback(callbacker: address, callback_sig: bytes4, user: address, stablecoins: uint256, - collateral: uint256, debt: uint256, calldata: Bytes[10**4]) -> CallbackData: - assert callbacker != COLLATERAL_TOKEN.address - assert callbacker != BORROWED_TOKEN.address - - data: CallbackData = empty(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, calldata)), - max_outsize=64 - ) - data.stablecoins = convert(slice(response, 0, 32), uint256) - data.collateral = convert(slice(response, 32, 32), uint256) - - # Checks after callback - 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, _for: address): - 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" + assert ctrl.loan[_for].initial_debt == 0, "Loan already created" + assert N > ctrl.MIN_TICKS_UINT - 1, "Need more ticks" + assert N < ctrl.MAX_TICKS_UINT + 1, "Need less ticks" - n1: int256 = self._calculate_debt_n1(collateral, debt, N, _for) + n1: int256 = ctrl._calculate_debt_n1(collateral, debt, N, _for) n2: int256 = n1 + convert(unsafe_sub(N, 1), int256) - rate_mul: uint256 = staticcall 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 + rate_mul: uint256 = staticcall ctrl.AMM.get_rate_mul() + ctrl.loan[_for] = IController.Loan(initial_debt=debt, rate_mul=rate_mul) + liquidation_discount: uint256 = ctrl.liquidation_discount + ctrl.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) + n_loans: uint256 = ctrl.n_loans + ctrl.loans[n_loans] = _for + ctrl.loan_ix[_for] = n_loans + ctrl.n_loans = unsafe_add(n_loans, 1) self._update_total_debt(debt, rate_mul, True) - extcall AMM.deposit_range(_for, collateral, n1, n2) + extcall ctrl.AMM.deposit_range(_for, collateral, n1, n2) self.lent += debt self.processed += debt - self._save_rate() + ctrl._save_rate() - log UserState(user=_for, collateral=collateral, debt=debt, n1=n1, n2=n2, liquidation_discount=liquidation_discount) - log Borrow(user=_for, collateral_increase=collateral, loan_increase=debt) + log IController.UserState( + user=_for, + collateral=collateral, + debt=debt, + n1=n1, + n2=n2, + liquidation_discount=liquidation_discount, + ) + log IController.Borrow( + user=_for, collateral_increase=collateral, loan_increase=debt + ) @external -@nonreentrant -def create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address = msg.sender, callbacker: address = empty(address), calldata: Bytes[10**4] = b""): +def create_loan( + collateral: uint256, + debt: uint256, + N: uint256, + _for: address = msg.sender, + callbacker: address = empty(address), + calldata: Bytes[10**4] = b"", +): """ @notice Create loan but pass stablecoin to a callback first so that it can build leverage @param collateral Amount of collateral to use @@ -646,27 +268,43 @@ def create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address = 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) + assert ctrl._check_approval(_for) more_collateral: uint256 = 0 if callbacker != empty(address): - self.transfer(BORROWED_TOKEN, callbacker, debt) + ctrl.transfer(ctrl.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 + more_collateral = ctrl.execute_callback( + callbacker, + ctrl.CALLBACK_DEPOSIT, + _for, + 0, + collateral, + debt, + calldata, + ).collateral self._create_loan(collateral + more_collateral, debt, N, _for) - self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) + ctrl.transferFrom( + ctrl.COLLATERAL_TOKEN, msg.sender, ctrl.AMM.address, collateral + ) if more_collateral > 0: - self.transferFrom(COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral) + ctrl.transferFrom( + ctrl.COLLATERAL_TOKEN, callbacker, ctrl.AMM.address, more_collateral + ) if callbacker == empty(address): - self.transfer(BORROWED_TOKEN, _for, debt) + ctrl.transfer(ctrl.BORROWED_TOKEN, _for, 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, +): """ @notice Internal method to borrow and add or remove collateral @param d_collateral Amount of collateral to add @@ -677,11 +315,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) + debt, rate_mul = ctrl._debt(_for) assert debt > 0, "Loan doesn't exist" debt += d_debt - xy: uint256[2] = extcall AMM.withdraw(_for, 10**18) + xy: uint256[2] = extcall ctrl.AMM.withdraw(_for, 10**18) assert xy[0] == 0, "Already in underwater mode" if remove_collateral: xy[1] -= d_collateral @@ -692,36 +330,50 @@ 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 * staticcall AMM.price_oracle() > 2 * 10**18 * BORROWED_PRECISION // COLLATERAL_PRECISION - - ns: int256[2] = staticcall AMM.read_user_tick_numbers(_for) + assert ( + d_collateral * staticcall ctrl.AMM.price_oracle() + > 2 + * 10**18 + * ctrl.BORROWED_PRECISION // ctrl.COLLATERAL_PRECISION + ) + ns: int256[2] = staticcall ctrl.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) + n1: int256 = ctrl._calculate_debt_n1(xy[1], debt, size, _for) n2: int256 = n1 + unsafe_sub(ns[1], ns[0]) - extcall AMM.deposit_range(_for, xy[1], n1, n2) - self.loan[_for] = Loan(initial_debt=debt, rate_mul=rate_mul) + extcall ctrl.AMM.deposit_range(_for, xy[1], n1, n2) + ctrl.loan[_for] = IController.Loan(initial_debt=debt, rate_mul=rate_mul) liquidation_discount: uint256 = 0 if _for == msg.sender: - liquidation_discount = self.liquidation_discount - self.liquidation_discounts[_for] = liquidation_discount + liquidation_discount = ctrl.liquidation_discount + ctrl.liquidation_discounts[_for] = liquidation_discount else: - liquidation_discount = self.liquidation_discounts[_for] + liquidation_discount = ctrl.liquidation_discounts[_for] if d_debt != 0: self._update_total_debt(d_debt, rate_mul, True) if remove_collateral: - log RemoveCollateral(user=_for, collateral_decrease=d_collateral) + log IController.RemoveCollateral( + user=_for, collateral_decrease=d_collateral + ) else: - log Borrow(user=_for, collateral_increase=d_collateral, loan_increase=d_debt) - - log UserState(user=_for, collateral=xy[1], debt=debt, n1=n1, n2=n2, liquidation_discount=liquidation_discount) + 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, + ) @external -@nonreentrant def add_collateral(collateral: uint256, _for: address = msg.sender): """ @notice Add extra collateral to avoid bad liqidations @@ -731,12 +383,13 @@ 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) - self._save_rate() + ctrl.transferFrom( + ctrl.COLLATERAL_TOKEN, msg.sender, ctrl.AMM.address, collateral + ) + ctrl._save_rate() @external -@nonreentrant def remove_collateral(collateral: uint256, _for: address = msg.sender): """ @notice Remove some collateral without repaying the debt @@ -745,15 +398,20 @@ def remove_collateral(collateral: uint256, _for: address = msg.sender): """ if collateral == 0: return - assert self._check_approval(_for) + assert ctrl._check_approval(_for) self._add_collateral_borrow(collateral, 0, _for, True, False) - self.transferFrom(COLLATERAL_TOKEN, AMM.address, _for, collateral) - self._save_rate() + ctrl.transferFrom(ctrl.COLLATERAL_TOKEN, ctrl.AMM.address, _for, collateral) + ctrl._save_rate() @external -@nonreentrant -def borrow_more(collateral: uint256, debt: uint256, _for: address = msg.sender, callbacker: address = empty(address), calldata: Bytes[10**4] = b""): +def borrow_more( + collateral: uint256, + debt: uint256, + _for: address = msg.sender, + callbacker: address = empty(address), + calldata: Bytes[10**4] = b"", +): """ @notice Borrow more stablecoins while adding more collateral using a callback (to leverage more) @param collateral Amount of collateral to add @@ -764,43 +422,48 @@ def borrow_more(collateral: uint256, debt: uint256, _for: address = msg.sender, """ if debt == 0: return - assert self._check_approval(_for) + assert ctrl._check_approval(_for) more_collateral: uint256 = 0 if callbacker != empty(address): - self.transfer(BORROWED_TOKEN, callbacker, debt) + ctrl.transfer(ctrl.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 - - self._add_collateral_borrow(collateral + more_collateral, debt, _for, False, False) + more_collateral = ctrl.execute_callback( + callbacker, + ctrl.CALLBACK_DEPOSIT, + _for, + 0, + collateral, + debt, + calldata, + ).collateral + + self._add_collateral_borrow( + collateral + more_collateral, debt, _for, False, False + ) self.lent += debt self.processed += debt - self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) + + ctrl.transferFrom( + ctrl.COLLATERAL_TOKEN, msg.sender, ctrl.AMM.address, collateral + ) if more_collateral > 0: - self.transferFrom(COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral) + ctrl.transferFrom( + ctrl.COLLATERAL_TOKEN, callbacker, ctrl.AMM.address, more_collateral + ) if callbacker == empty(address): - self.transfer(BORROWED_TOKEN, _for, debt) - self._save_rate() - - -@internal -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 - self.loan_ix[_for] = 0 - if loan_ix < last_loan_ix: # Need to replace - last_loan: address = self.loans[last_loan_ix] - self.loans[loan_ix] = last_loan - self.loan_ix[last_loan] = loan_ix - self.n_loans = last_loan_ix + ctrl.transfer(ctrl.BORROWED_TOKEN, _for, debt) + ctrl._save_rate() @external -@nonreentrant -def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 = max_value(int256), - callbacker: address = empty(address), calldata: Bytes[10**4] = b""): +def repay( + _d_debt: uint256, + _for: address = msg.sender, + max_active_band: int256 = max_value(int256), + callbacker: address = empty(address), + calldata: Bytes[10**4] = b"", +): """ @notice Repay debt (partially or fully) @param _d_debt The amount of debt to repay from user's wallet. If higher than the current debt - will do full repayment @@ -811,18 +474,21 @@ def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 """ debt: uint256 = 0 rate_mul: uint256 = 0 - debt, rate_mul = self._debt(_for) + debt, rate_mul = ctrl._debt(_for) assert debt > 0, "Loan doesn't exist" - approval: bool = self._check_approval(_for) + approval: bool = ctrl._check_approval(_for) xy: uint256[2] = empty(uint256[2]) - cb: CallbackData = empty(CallbackData) + cb: IController.CallbackData = empty(IController.CallbackData) if callbacker != empty(address): assert approval - xy = extcall AMM.withdraw(_for, 10 ** 18) - self.transferFrom(COLLATERAL_TOKEN, AMM.address, callbacker, xy[1]) - cb = self.execute_callback( - callbacker, CALLBACK_REPAY, _for, xy[0], xy[1], debt, calldata) + xy = extcall ctrl.AMM.withdraw(_for, 10**18) + ctrl.transferFrom( + ctrl.COLLATERAL_TOKEN, ctrl.AMM.address, callbacker, xy[1] + ) + cb = ctrl.execute_callback( + callbacker, ctrl.CALLBACK_REPAY, _for, xy[0], xy[1], debt, calldata + ) total_stablecoins: uint256 = _d_debt + xy[0] + cb.stablecoins assert total_stablecoins > 0 # dev: no coins to repay @@ -833,181 +499,118 @@ def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 d_debt = debt debt = 0 if callbacker == empty(address): - xy = extcall AMM.withdraw(_for, 10 ** 18) + xy = extcall ctrl.AMM.withdraw(_for, 10**18) - # Transfer all stablecoins to self if xy[0] > 0: # Only allow full repayment when underwater for the sender to do assert approval - self.transferFrom(BORROWED_TOKEN, AMM.address, self, xy[0]) + ctrl.transferFrom( + ctrl.BORROWED_TOKEN, ctrl.AMM.address, self, xy[0] + ) if cb.stablecoins > 0: - self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins) + ctrl.transferFrom( + ctrl.BORROWED_TOKEN, callbacker, self, cb.stablecoins + ) if _d_debt > 0: - self.transferFrom(BORROWED_TOKEN, msg.sender, self, _d_debt) + ctrl.transferFrom(ctrl.BORROWED_TOKEN, msg.sender, self, _d_debt) - # Transfer stablecoins excess to _for if total_stablecoins > d_debt: - self.transfer(BORROWED_TOKEN, _for, unsafe_sub(total_stablecoins, d_debt)) + ctrl.transfer( + ctrl.BORROWED_TOKEN, _for, unsafe_sub(total_stablecoins, d_debt) + ) # Transfer collateral to _for if callbacker == empty(address): if xy[1] > 0: - self.transferFrom(COLLATERAL_TOKEN, AMM.address, _for, xy[1]) + ctrl.transferFrom( + ctrl.COLLATERAL_TOKEN, ctrl.AMM.address, _for, xy[1] + ) else: if cb.collateral > 0: - self.transferFrom(COLLATERAL_TOKEN, callbacker, _for, cb.collateral) - - self._remove_from_list(_for) - log UserState(user=_for, collateral=0, debt=0, n1=0, n2=0, liquidation_discount=0) - log Repay(user=_for, collateral_decrease=xy[1], loan_decrease=d_debt) + ctrl.transferFrom( + ctrl.COLLATERAL_TOKEN, callbacker, _for, cb.collateral + ) + ctrl._remove_from_list(_for) + 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=d_debt + ) # Else - partial repayment else: - active_band: int256 = staticcall AMM.active_band_with_skip() + active_band: int256 = staticcall ctrl.AMM.active_band_with_skip() assert active_band <= max_active_band d_debt = total_stablecoins debt = unsafe_sub(debt, d_debt) - ns: int256[2] = staticcall AMM.read_user_tick_numbers(_for) + ns: int256[2] = staticcall ctrl.AMM.read_user_tick_numbers(_for) size: int256 = unsafe_sub(ns[1], ns[0]) - liquidation_discount: uint256 = self.liquidation_discounts[_for] + liquidation_discount: uint256 = ctrl.liquidation_discounts[_for] if ns[0] > active_band: # Not in soft-liquidation - can use callback and move bands new_collateral: uint256 = cb.collateral if callbacker == empty(address): - xy = extcall AMM.withdraw(_for, 10**18) + xy = extcall ctrl.AMM.withdraw(_for, 10**18) new_collateral = xy[1] - ns[0] = self._calculate_debt_n1(new_collateral, debt, convert(unsafe_add(size, 1), uint256), _for) + ns[0] = ctrl._calculate_debt_n1( + new_collateral, + debt, + convert(unsafe_add(size, 1), uint256), + _for, + ) ns[1] = ns[0] + size - extcall AMM.deposit_range(_for, new_collateral, ns[0], ns[1]) + extcall ctrl.AMM.deposit_range(_for, new_collateral, ns[0], ns[1]) else: # Underwater - cannot use callback or move bands but can avoid a bad liquidation - xy = staticcall AMM.get_sum_xy(_for) + xy = staticcall ctrl.AMM.get_sum_xy(_for) assert callbacker == empty(address) 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 + liquidation_discount = ctrl.liquidation_discount + ctrl.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, debt, False, liquidation_discount) > 0 + assert ctrl._health(_for, debt, False, liquidation_discount) > 0 if cb.stablecoins > 0: - self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins) + ctrl.transferFrom( + ctrl.BORROWED_TOKEN, callbacker, self, cb.stablecoins + ) if _d_debt > 0: - self.transferFrom(BORROWED_TOKEN, msg.sender, self, _d_debt) - - log UserState(user=_for, collateral=xy[1], debt=debt, n1=ns[0], n2=ns[1], liquidation_discount=liquidation_discount) - log Repay(user=_for, collateral_decrease=0, loan_decrease=d_debt) + ctrl.transferFrom(ctrl.BORROWED_TOKEN, msg.sender, self, _d_debt) + + log IController.UserState( + user=_for, + collateral=xy[1], + debt=debt, + n1=ns[0], + n2=ns[1], + liquidation_discount=liquidation_discount, + ) + log IController.Repay( + user=_for, collateral_decrease=0, loan_decrease=d_debt + ) self.repaid += d_debt - self.loan[_for] = Loan(initial_debt=debt, rate_mul=rate_mul) + ctrl.loan[_for] = IController.Loan(initial_debt=debt, rate_mul=rate_mul) self._update_total_debt(d_debt, rate_mul, False) - self._save_rate() - - -@internal -@view -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 - @param user User address to calculate health for - @param debt The amount of debt to calculate health for - @param full Whether to take into account the price difference above the highest user's band - @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(staticcall AMM.get_x_down(user), int256) * health, convert(debt, int256)) - 10**18 - - if full: - 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) * (staticcall AMM.get_sum_xy(user))[1] * COLLATERAL_PRECISION, debt * BORROWED_PRECISION), int256) - - return health - - -@external -@view -@nonreentrant -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 - @param d_collateral Change in collateral amount (signed) - @param d_debt Change in debt amount (signed) - @param full Whether it's a 'full' health or not - @param N Number of bands in case loan doesn't yet exist - @return Signed health value - """ - ns: int256[2] = staticcall 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 = 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(10**18, 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, 10**18) - 10**18 - - 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 + ctrl._save_rate() @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) - - return f_remove - - -@internal -def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint256, callbacker: address, calldata: Bytes[10**4]): +def _liquidate( + user: address, + min_x: uint256, + health_limit: uint256, + frac: uint256, + callbacker: address, + calldata: Bytes[10**4], +): """ @notice Perform a bad liquidation of user if the health is too bad @param user Address of the user @@ -1019,10 +622,12 @@ def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint2 """ debt: uint256 = 0 rate_mul: uint256 = 0 - debt, rate_mul = self._debt(user) + debt, rate_mul = ctrl._debt(user) if health_limit != 0: - assert self._health(user, debt, True, health_limit) < 0, "Not enough rekt" + assert ( + ctrl._health(user, debt, True, health_limit) < 0 + ), "Not enough rekt" final_debt: uint256 = debt debt = unsafe_div(debt * frac + (10**18 - 1), 10**18) @@ -1034,59 +639,101 @@ def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint2 # 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] = extcall AMM.withdraw(user, self._get_f_remove(frac, health_limit)) # [stable, collateral] + xy: uint256[2] = extcall ctrl.AMM.withdraw( + user, ctrl._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) + ctrl.transferFrom(ctrl.BORROWED_TOKEN, ctrl.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]) + ctrl.transferFrom( + ctrl.COLLATERAL_TOKEN, ctrl.AMM.address, msg.sender, xy[1] + ) # Request what's left from user - self.transferFrom(BORROWED_TOKEN, msg.sender, self, to_repay) + ctrl.transferFrom(ctrl.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]) + ctrl.transferFrom( + ctrl.COLLATERAL_TOKEN, ctrl.AMM.address, callbacker, xy[1] + ) # Callback - cb: CallbackData = self.execute_callback( - callbacker, CALLBACK_LIQUIDATE, user, xy[0], xy[1], debt, calldata) + cb: IController.CallbackData = ctrl.execute_callback( + callbacker, + ctrl.CALLBACK_LIQUIDATE, + user, + xy[0], + xy[1], + debt, + calldata, + ) 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) - + ctrl.transferFrom( + ctrl.BORROWED_TOKEN, + callbacker, + msg.sender, + unsafe_sub(cb.stablecoins, to_repay), + ) + ctrl.transferFrom(ctrl.BORROWED_TOKEN, callbacker, self, to_repay) + ctrl.transferFrom( + ctrl.COLLATERAL_TOKEN, callbacker, msg.sender, cb.collateral + ) else: # Withdraw collateral - self.transferFrom(COLLATERAL_TOKEN, AMM.address, msg.sender, xy[1]) + ctrl.transferFrom( + ctrl.COLLATERAL_TOKEN, ctrl.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)) - + ctrl.transferFrom( + ctrl.BORROWED_TOKEN, + ctrl.AMM.address, + msg.sender, + unsafe_sub(xy[0], debt), + ) self.repaid += debt - self.loan[user] = Loan(initial_debt=final_debt, rate_mul=rate_mul) - log Repay(user=user, collateral_decrease=xy[1], loan_decrease=debt) - log Liquidate(liquidator=msg.sender, user=user, collateral_received=xy[1], stablecoin_received=xy[0], debt=debt) + ctrl.loan[user] = IController.Loan( + initial_debt=final_debt, rate_mul=rate_mul + ) + log IController.Repay( + user=user, collateral_decrease=xy[1], loan_decrease=debt + ) + log IController.Liquidate( + liquidator=msg.sender, + user=user, + collateral_received=xy[1], + stablecoin_received=xy[0], + debt=debt, + ) if final_debt == 0: - log 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) + 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 + ctrl._remove_from_list(user) self._update_total_debt(debt, rate_mul, False) - self._save_rate() + ctrl._save_rate() @external -@nonreentrant -def liquidate(user: address, min_x: uint256, frac: uint256 = 10**18, callbacker: address = empty(address), calldata: Bytes[10**4] = b""): +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 (or self-liquidation) of user if health is not good @param min_x Minimal amount of stablecoin to receive (to avoid liquidators being sandwiched) @@ -1095,128 +742,26 @@ def liquidate(user: address, min_x: uint256, frac: uint256 = 10**18, callbacker: @param calldata Any data for callbacker """ 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, calldata) - - -@view -@external -@nonreentrant -def tokens_to_liquidate(user: address, frac: uint256 = 10 ** 18) -> uint256: - """ - @notice Calculate the amount of stablecoins 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 - """ - health_limit: uint256 = 0 - if not self._check_approval(user): - health_limit = self.liquidation_discounts[user] - stablecoins: uint256 = unsafe_div((staticcall 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) - - return unsafe_sub(max(debt, stablecoins), stablecoins) - - -@view -@external -@nonreentrant -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]) - - -@view -@external -@nonreentrant -def users_to_liquidate(_from: uint256=0, _limit: uint256=0) -> DynArray[Position, 1000]: - """ - @notice Returns a dynamic array of users who can be "hard-liquidated". - This method is designed for convenience of liquidation bots. - @param _from Loan index to start iteration from - @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: uint256 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] = staticcall 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 -@view -@external -def amm_price() -> uint256: - """ - @notice Current price from the AMM - """ - return staticcall AMM.get_p() - - -@view -@external -@nonreentrant -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 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 -@nonreentrant -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) - """ - 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)[0], convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256)] + if not ctrl._check_approval(user): + discount = ctrl.liquidation_discounts[user] + self._liquidate( + user, min_x, discount, min(frac, 10**18), callbacker, calldata + ) -# AMM has nonreentrant decorator @external +@reentrant def set_amm_fee(fee: uint256): """ @notice Set the AMM fee (factory admin only) + @dev Reentrant because AMM is nonreentrant TODO check this one @param fee The fee which should be no higher than MAX_AMM_FEE """ assert msg.sender == staticcall VAULT.admin() - assert fee <= MAX_AMM_FEE and fee >= MIN_AMM_FEE, "Fee" - extcall AMM.set_fee(fee) + assert fee <= ctrl.MAX_AMM_FEE and fee >= ctrl.MIN_AMM_FEE, "Fee" + extcall ctrl.AMM.set_fee(fee) -@nonreentrant @external def set_monetary_policy(monetary_policy: address): """ @@ -1224,14 +769,15 @@ def set_monetary_policy(monetary_policy: address): @param monetary_policy Address of the monetary policy contract """ assert msg.sender == staticcall VAULT.admin() - self.monetary_policy = MonetaryPolicy(monetary_policy) - extcall MonetaryPolicy(monetary_policy).rate_write() - log SetMonetaryPolicy(monetary_policy=monetary_policy) + ctrl._monetary_policy = IMonetaryPolicy(monetary_policy) + extcall IMonetaryPolicy(monetary_policy).rate_write() + log IController.SetMonetaryPolicy(monetary_policy=monetary_policy) -@nonreentrant @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 @@ -1239,26 +785,26 @@ def set_borrowing_discounts(loan_discount: uint256, liquidation_discount: uint25 """ assert msg.sender == staticcall VAULT.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=loan_discount, liquidation_discount=liquidation_discount) + assert liquidation_discount >= ctrl.MIN_LIQUIDATION_DISCOUNT + assert loan_discount <= ctrl.MAX_LOAN_DISCOUNT + ctrl.liquidation_discount = liquidation_discount + ctrl.loan_discount = loan_discount + log IController.SetBorrowingDiscounts( + loan_discount=loan_discount, liquidation_discount=liquidation_discount + ) @external -@nonreentrant -def set_callback(cb: address): +def set_callback(cb: ILMGauge): """ @notice Set liquidity mining callback """ assert msg.sender == staticcall VAULT.admin() - extcall AMM.set_callback(cb) - log SetLMCallback(callback=cb) + extcall ctrl.AMM.set_callback(cb) + log IController.SetLMCallback(callback=cb) @external -@nonreentrant def set_borrow_cap(_borrow_cap: uint256): """ @notice Set the borrow cap for this market @@ -1267,7 +813,7 @@ def set_borrow_cap(_borrow_cap: uint256): """ assert msg.sender == staticcall VAULT.admin() self.borrow_cap = _borrow_cap - log SetBorrowCap(borrow_cap=_borrow_cap) + log ILlamalendController.SetBorrowCap(borrow_cap=_borrow_cap) @external @@ -1287,11 +833,12 @@ def admin_fees() -> uint256: @notice Calculate the amount of fees obtained from the interest """ processed: uint256 = self.processed - return unsafe_sub(max(self._get_total_debt() + self.repaid, processed), processed) + return unsafe_sub( + max(ctrl._get_total_debt() + self.repaid, processed), processed + ) @external -@nonreentrant def collect_fees() -> uint256: """ @notice Collect the fees charged as a fraction of interest. @@ -1299,9 +846,9 @@ def collect_fees() -> uint256: _to: address = staticcall VAULT.fee_receiver() # Borrowing-based fees - rate_mul: uint256 = staticcall AMM.get_rate_mul() - loan: Loan = self._update_total_debt(0, rate_mul, False) - self._save_rate() + rate_mul: uint256 = staticcall ctrl.AMM.get_rate_mul() + loan: IController.Loan = self._update_total_debt(0, rate_mul, False) + ctrl._save_rate() # Cumulative amount which would have been repaid if all the debt was repaid now to_be_repaid: uint256 = loan.initial_debt + self.repaid @@ -1310,40 +857,13 @@ def collect_fees() -> uint256: # Difference between to_be_redeemed and minted amount is exactly due to interest charged if to_be_repaid > processed: self.processed = to_be_repaid - fees: uint256 = unsafe_sub(to_be_repaid, processed) * self.admin_fee // 10**18 + fees: uint256 = ( + unsafe_sub(to_be_repaid, processed) * self.admin_fee // 10**18 + ) self.collected += fees - self.transfer(BORROWED_TOKEN, _to, fees) - log CollectFees(amount=fees, new_supply=loan.initial_debt) + ctrl.transfer(ctrl.BORROWED_TOKEN, _to, fees) + log IController.CollectFees(amount=fees, new_supply=loan.initial_debt) return fees else: - log CollectFees(amount=0, new_supply=loan.initial_debt) + log IController.CollectFees(amount=0, new_supply=loan.initial_debt) return 0 - - -# Allowance methods - -@external -def approve(_spender: address, _allow: bool): - """ - @notice Allow another address to borrow and repay for the user - @param _spender Address to whitelist for the action - @param _allow Whether to turn the approval on or off (no amounts) - """ - self.approval[msg.sender][_spender] = _allow - log Approval(owner=msg.sender, spender=_spender, allow=_allow) - - -@internal -@view -def _check_approval(_for: address) -> bool: - return msg.sender == _for or self.approval[_for][msg.sender] - - -@external -def set_extra_health(_value: uint256): - """ - @notice Add a little bit more to loan_discount to start SL with health higher than usual - @param _value 1e18-based addition to loan_discount - """ - self.extra_health[msg.sender] = _value - log SetExtraHealth(user=msg.sender, health=_value) diff --git a/poetry.lock b/poetry.lock index 016c89cd..93f37518 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. [[package]] name = "annotated-types" @@ -13,40 +13,37 @@ files = [ [[package]] name = "asttokens" -version = "2.4.1" +version = "3.0.0" description = "Annotate AST trees with source code positions" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, - {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, + {file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"}, + {file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"}, ] -[package.dependencies] -six = ">=1.12.0" - [package.extras] -astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] -test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] +astroid = ["astroid (>=2,<4)"] +test = ["astroid (>=2,<4)", "pytest", "pytest-cov", "pytest-xdist"] [[package]] name = "attrs" -version = "23.2.0" +version = "25.3.0" description = "Classes Without Boilerplate" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, + {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, + {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, ] [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]"] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] [[package]] name = "babel" @@ -64,144 +61,156 @@ dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest [[package]] name = "bitarray" -version = "2.9.2" +version = "3.6.0" 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"}, + {file = "bitarray-3.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6841c08b51417f8ffe398b2828fc0593440c99525c868f640e0302476745320b"}, + {file = "bitarray-3.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a04b7a9017b8d0341ebbe77f61b74df1cf1b714f42b671a06f4912dc93d82597"}, + {file = "bitarray-3.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:664d462a4c0783fd755fe3440f07b7e46d149859c96caacadf3f28890f19a8de"}, + {file = "bitarray-3.6.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e997d22e0d1e08c8752f61675a75d93659f7aa4dbeaee54207f8d877817b4a0c"}, + {file = "bitarray-3.6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6755cfcfa7d8966e704d580c831e39818f85e7b2b7852ad22708973176f0009e"}, + {file = "bitarray-3.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4798f6744fa2633666e17b4ea8ff70250781b52a25afdbf5ffb5e176c58848f1"}, + {file = "bitarray-3.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:efa5834ba5e6c70b22afdca3894097e5a592d8d483c976359654ba990477799a"}, + {file = "bitarray-3.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d47e2bdeba4fb1986af2ba395ce51223f4d460e6e77119439e78f2b592cafade"}, + {file = "bitarray-3.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2a324e3007afb5c667026f5235b35efe3c4a95f1b83cd93aa9fce67b42f08e7c"}, + {file = "bitarray-3.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:080a7bf55c432abdae74f25dc3dbff407418346aeae1d43e31f65e8ef114f785"}, + {file = "bitarray-3.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3eb1390a8b062fe9125e5cc4c5eba990b5d383eec54f2b996e7ce73ac43150f9"}, + {file = "bitarray-3.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2020102a40edd094c0aa80e09203af71c533c41f76ce3237c99fd194a473ea33"}, + {file = "bitarray-3.6.0-cp310-cp310-win32.whl", hash = "sha256:01d6dc548e7fe5c66913c2274f44855b0f8474935acff7811e84fe1f4024c94f"}, + {file = "bitarray-3.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:8d759cecfa8aab4a1eb4e23b6420126b15c7743e85b33f389916bb98c4ecbb84"}, + {file = "bitarray-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7c20d6e6cafce5027e7092beb2ac6eec0d71045d6318b34f36e1387a8c8859a3"}, + {file = "bitarray-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cf36cadeb9c989f760a13058dbc455e5406ec3d2d247c705c8d4bc6dd1b0fcc6"}, + {file = "bitarray-3.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30ba4fba3de1dca653de41c879349ec6ca521d85cff6a7ca5d2fdd8f76c93781"}, + {file = "bitarray-3.6.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77d2368a06a86a18919c05a9b4b0ee9869f770e6a5f414b0fecc911870fe3974"}, + {file = "bitarray-3.6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a39be79a7c36e9a2e20376261c30deb3cdca86b50f7462ae9ff10a755c6720b9"}, + {file = "bitarray-3.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4695fcd37478988b1d0a16d5bc0df56dcb677fd5db37f1893d993fd3ebef914b"}, + {file = "bitarray-3.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52328192d454ca2ddad09fbc088872b014c74b22ecdd5164717dc7e6442014fa"}, + {file = "bitarray-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96117212229905da864794df9ea7bd54987c30a5dcbab3432edc3f344231adae"}, + {file = "bitarray-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:68f6e64d4867ee79e25c49d7f35b2b1f04a6d6f778176dcf5b759f3b17a02b2b"}, + {file = "bitarray-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:29ed022189a7997de46cb9bd4e2e49d6163d4f8d78dea72ac5a0e0293b856810"}, + {file = "bitarray-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e71c9dba78671d38a549e3b2d52514f50e199f9d7e18ed9b0180adeef0d04130"}, + {file = "bitarray-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ddb319f869d497ef2d3d56319360b61284a9a1d8b3de3bc936748698acfda6be"}, + {file = "bitarray-3.6.0-cp311-cp311-win32.whl", hash = "sha256:25060e7162e44242a449ed1a14a4e94b5aef340812754c443459f19c7954be91"}, + {file = "bitarray-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2d951002b11962b26afb31f758c18ad39771f287b100fa5adb1d09a47eaaf5b"}, + {file = "bitarray-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9616ea14917d06736339cf36bb9eaf4eb52110a74136b0dc5eff94e92417d22"}, + {file = "bitarray-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7e2e1ff784c2cdfd863bad31985851427f2d2796e445cec85080c7510cba4315"}, + {file = "bitarray-3.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:911b4a16dce370657e5b8d8b6ba0fbb50dd5e2b24c4416f4b9e664503d3f0502"}, + {file = "bitarray-3.6.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b47843f2f288fa746dead4394591a3432a358aaad48240283fa230d6e74b0e7"}, + {file = "bitarray-3.6.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8f95daf0ce2b24815ddf62667229ba5dfc0cfee43eb43b2549766170d0f24ae9"}, + {file = "bitarray-3.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c15b9e37bbca59657e4dcc63ad068c821a4676def15f04742c406748a0a11b9c"}, + {file = "bitarray-3.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9d247fcc33c90f2758f4162693250341e3f38cd094f64390076ef33ad0887f9"}, + {file = "bitarray-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:84bb57010a1ab76cf880424a2e0bce8dd26989849d2122ff073aa11bfc271c27"}, + {file = "bitarray-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:27d13c7b886afc5d2fc49d6e92f9c96b1f0a14dc7b5502520c29f3da7550d401"}, + {file = "bitarray-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c4e75bbf9ade3d2cdf1b607a8b353b17d9b3cf54e88b2a5a773f50ae6f1bfbc"}, + {file = "bitarray-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:975a118aa019d745f1398613b27fd8789f60a8cea057a00cdc1abedee123ffe6"}, + {file = "bitarray-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9ed4a2852b3de7a64884afcc6936db771707943249a060aec8e551c16361d478"}, + {file = "bitarray-3.6.0-cp312-cp312-win32.whl", hash = "sha256:5dd9edcab8979a50c2c4dec6d5b66789fb6f630bb52ab90a4548111075a75e48"}, + {file = "bitarray-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:552a93be286ca485914777461b384761519db313e0a7f3012dca424c9610a4d5"}, + {file = "bitarray-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3f96f57cea35ba19fd23a20b38fa0dfa3d87d582507129b8c8e314aa298f59b"}, + {file = "bitarray-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:81e84054b22babcd6c5cc1eac0de2bfc1054ecdf742720cbfb36efbe89ec6c30"}, + {file = "bitarray-3.6.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca643295bf5441dd38dadf7571ca4b63961820eedbffbe46ceba0893bf226203"}, + {file = "bitarray-3.6.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:139963494fc3dd5caee5e38c0a03783ef50be118565e94b1dbb0210770f0b32d"}, + {file = "bitarray-3.6.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:243825f56b58bef28bfc602992a8c6d09bbc625628c195498d6020120d632a09"}, + {file = "bitarray-3.6.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:583b46b3ba44121de5e87e95ae379932dc5fd2e37ebdf2c11a6d7975891425c1"}, + {file = "bitarray-3.6.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f0be27d06732e2833b672a8fcc32fa195bdb22161eb88f8890de15e30264a01"}, + {file = "bitarray-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:507e567aee4806576e20752f22533e8b7ec61e7e75062a7ce9222a0675aa0da6"}, + {file = "bitarray-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:22188943a29072b684cd7c99e0b2cfc0af317cea3366c583d820507e6d1f2ed4"}, + {file = "bitarray-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f92462ea3888c99439f58f7561ecd5dd4cf8b8b1b259ccf5376667b8c46ee747"}, + {file = "bitarray-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3800f3c8c9780f281cf590543fd4b3278fea6988202273a260ecc58136895efb"}, + {file = "bitarray-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a50a66fa34dd7f9dcdbc7602a1b7bf6f9ab030b4f43e892324193423d9ede180"}, + {file = "bitarray-3.6.0-cp313-cp313-win32.whl", hash = "sha256:afa24e5750c9b89ad5a7efef037efe49f4e339f20a94bf678c422c0c71e1207a"}, + {file = "bitarray-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4c5e7edf1e7bcbde3b52058f171a411e2a24a081b3e951d685dfea4c3c383d5"}, + {file = "bitarray-3.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fefd18b29f3b84a0cdea1d86340219d9871c3b0673a38e722a73a2c39591eaa7"}, + {file = "bitarray-3.6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:679856547f0b27b98811b73756bdf53769c23b045a6f95177cae634daabf1ddf"}, + {file = "bitarray-3.6.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51947a00ae9924584fb14c0c1b1f4c1fd916d9abd6f47582f318ab9c9cb9f3d0"}, + {file = "bitarray-3.6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0956322bf4d5e2293e57600aa929c241edf1e209e94e12483bf58c5c691432db"}, + {file = "bitarray-3.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3b521e117ab991d6b3b830656f464b354a42dbea2ca16a0e7d93d573f7ab7ff"}, + {file = "bitarray-3.6.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27eeee915258b105a21a4b0f8aebc5f77bb4dc4fb4063a09dd329fa1fdcbd234"}, + {file = "bitarray-3.6.0-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:f738051052abc95dc17f9a4c92044294a263fb7f762efdb13e528d419005c0e4"}, + {file = "bitarray-3.6.0-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:1971050b447023288a2b694a03b400bd5163829cd67b10f19e757fe87cd1161e"}, + {file = "bitarray-3.6.0-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:a290a417608f50137bec731d1f22ff3efebac72845530807a8433b2db9358c95"}, + {file = "bitarray-3.6.0-cp36-cp36m-musllinux_1_2_s390x.whl", hash = "sha256:8ef3f0977c21190f949d5cfd71ded09de87d330c6d98bd5ecb5bb1135d666d0d"}, + {file = "bitarray-3.6.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:357e07c827bad01f98d0bd0dfdc722f483febeed39140fd75ffd016a451b60b9"}, + {file = "bitarray-3.6.0-cp36-cp36m-win32.whl", hash = "sha256:bdd6412c1f38da7565126b174f4e644f362e317ef0560fac1fb9d0c70900ff4d"}, + {file = "bitarray-3.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a1b3c4ca3bec8e0ad9d32ce62444c5f3913588124a922629aa7d39357b2adf3f"}, + {file = "bitarray-3.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:531e6dfec8058fcf5d69e863b61e6b28e3749b615a4dcc0ab8ad36307c4017fc"}, + {file = "bitarray-3.6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6f9e897907757e9c2d722ae6c203d48a04826a14e1495e33935c8583c163a9"}, + {file = "bitarray-3.6.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c9f36055a89b9517db66eb8e80137126bf629c767ceeade4d004e40bc8bcd99"}, + {file = "bitarray-3.6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d6f3a94abd8b44b2bf346ca81ab2ff41ab9146c53905eedf5178b19d9fe53bf"}, + {file = "bitarray-3.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79db23eda81627132327ed292bd813a9af64399b98aaac3d42ad8deeed24cd5e"}, + {file = "bitarray-3.6.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d6c9bc14bacdfbfd51fed85f0576973eaaa7b30d81ef93264f8e22b86a9c9f7"}, + {file = "bitarray-3.6.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:60408ec9c0bd76f1fa00d28034429a0316246d31069b982a86aec8d5c99e910a"}, + {file = "bitarray-3.6.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:870ed23361e2918ab1ffc23fe0ab293abf3c372a68ee7387456d13da3e213008"}, + {file = "bitarray-3.6.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:c677849947d523a082be6e0b5c9137f443a54e951a1711ef003ec52910c41ece"}, + {file = "bitarray-3.6.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:a5ce1bdee102f7e60c075274df10b892d9ff5183ad6f5f515973eda8903dfe4c"}, + {file = "bitarray-3.6.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:a33f7c5acf44961f29018b13f0b5f5e1589ac0cfdf75a97c9774cf7ec84d09e0"}, + {file = "bitarray-3.6.0-cp37-cp37m-win32.whl", hash = "sha256:16d0edab54bb9d214319418f65bd15cfc4210ec41a16c3dd0b71e626c803212d"}, + {file = "bitarray-3.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f76784355060999c36fa807b59faecb38f5769ae58283d00270835773f95e35b"}, + {file = "bitarray-3.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cc060bc17b9de27874997d612e37d52f72092f9b59d1e04284a90ed8113cadca"}, + {file = "bitarray-3.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bfc417e58f277e949ed662d9cd050ddbb00c0dea8a828abaccc93dc357b7a6d1"}, + {file = "bitarray-3.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:129165b68a3e0c2a633ed0d8557cf5ade24a0b37ca97d7805fa6fc5fb73c19d5"}, + {file = "bitarray-3.6.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50d702149747852923be60cae125285eca8d189d4c7d8832c0c958d4071a0f78"}, + {file = "bitarray-3.6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ccf4a73e07bfbd790443d6b3c1f1447ffda23cc9391e40c035d9b7d3514b57b8"}, + {file = "bitarray-3.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7d2dbe628f3db935622a5b80a5c4d95665cdefc4904372aa3c4d786289477f"}, + {file = "bitarray-3.6.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5da4939e241301f5e1d18118695e8d2c300be90431b66bd43a00376acec45e1e"}, + {file = "bitarray-3.6.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9930853d451086c4c084d83a87294bdb0c5bc0fa4105a26c487ac09ea62e565b"}, + {file = "bitarray-3.6.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:e0e4fdeae6c0a9d886749780ec5dcf469e98f27b312efa93008d03eaa2426fd5"}, + {file = "bitarray-3.6.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:79ab1c5f26f23e51d4a44c4397c8a3bf56c306c125dfab6b3eebdfa13d1dca6f"}, + {file = "bitarray-3.6.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:b9a03767c937b621ee267507bc394df97fb2f8f61130f39f2033f3c6c191f124"}, + {file = "bitarray-3.6.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:54bd71f14a5fa9bae73ef92f2e2be894dc36c7a6d1c4962e5969bd8a9aa39325"}, + {file = "bitarray-3.6.0-cp38-cp38-win32.whl", hash = "sha256:7e0851a985a7b10f634188117c825ef99d63402555cca5bc32c7bfc5adaf0d6f"}, + {file = "bitarray-3.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:00628196dd3592972a5183194ab1475dadf9ef2a4cf3fd8c7c184a94934012e8"}, + {file = "bitarray-3.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:69d2d507c1174330c71c834b5d65e66181ad7b42b0d88b5b31804ee9b4f5dae7"}, + {file = "bitarray-3.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:727f7a969416f02ef5c1256541e06f0836fb615022699fa8e2591e85296c5570"}, + {file = "bitarray-3.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42622c42c159ea4535bba7e1e3c97f1fec79505bc6873ae657dc0a8f861c60de"}, + {file = "bitarray-3.6.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f8b12424f8fdf29d1c0749c628bd1530cecfc77374935d096cccc0e4eada232"}, + {file = "bitarray-3.6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:963cbcf296943f7017470d0b705e63e908f32b4f7dbe43f72c22f6fe1bd9ef66"}, + {file = "bitarray-3.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e304f94c0353f6ae5711533b5793b3a45b17aa2c5b07e656649b0af4e0939b5"}, + {file = "bitarray-3.6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c533c828d0007fac27cf45e5c1a711e5914dd469db5fe6be5f4e606bf2d7f63"}, + {file = "bitarray-3.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:220d4b8649ef54ac98e5e0e3dd92230247f67270d1524a8b31aa9859007affb0"}, + {file = "bitarray-3.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:407920e9318d94cc6c9611aaa5b5e5963a09f1cbfa17b16b66edea453b3754f4"}, + {file = "bitarray-3.6.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:056fe779f01a867d572e071c0944ac2f3bf58d8bced326040f0bd060af33a209"}, + {file = "bitarray-3.6.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ca87f639094c72268e17bc7f57c1225cc38f9e191a489a0134762e3fec402c1a"}, + {file = "bitarray-3.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b5ad8261f47c2a72d0f676bc40f752db8cfdcab911e970753343836e41d5a9a7"}, + {file = "bitarray-3.6.0-cp39-cp39-win32.whl", hash = "sha256:a773199dc42b5d02fcd46c8add552da2c4725ce2caa069527c7e27b5b6089e85"}, + {file = "bitarray-3.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:157313a124287cbc8a11b55a75def0dd59e68badbc82c2dc2d204dc852742874"}, + {file = "bitarray-3.6.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a763dd33d6e27c9b4db3f8089a5fa39179a8a3cf48ce702b24a857d7c621333c"}, + {file = "bitarray-3.6.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8cf44b012e7493127ce7ca6e469138ac96b3295a117877d5469aabe7c8728d87"}, + {file = "bitarray-3.6.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e297fd2e58afe17e33dd80c231c3a9d850279a2a8625aed1d39f9be9534809e"}, + {file = "bitarray-3.6.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fc8bc65f964c7278deb1b7a69379dab3ecc90095f252deb17365637ebb274d"}, + {file = "bitarray-3.6.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa3c925502bd0b957a96a5619134bcdc0382ef73cffd40bad218ced3586bcf8d"}, + {file = "bitarray-3.6.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9f7796959c9c036a115d34696563f75d4a2912d3b97c15c15f2a36bdd5496ce9"}, + {file = "bitarray-3.6.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b02cc1cac9099c0ec72da09593e7fadb1b6cf88a101acc8153592c700d732d80"}, + {file = "bitarray-3.6.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26691454a6770628882b68fe74e9f84ca2a51512edd49cbb025b14df5a9dd85a"}, + {file = "bitarray-3.6.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a5b0d277087a5bf261a607fc6ff4aaffcf80b300cd19b7a1e9754a4649f5fd4"}, + {file = "bitarray-3.6.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af670708e145b048ead87375b899229443f2d0b4af2d1450d7701c74cd932b03"}, + {file = "bitarray-3.6.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:aeb6db2f4ab54ac21a3851d05130a2aa78a6f6a5f14003f9ae3114fb8b210850"}, + {file = "bitarray-3.6.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:99d16862e802e7c50c3b6cdd1bf041b6142335c9c2b426631f731257adfe5a15"}, + {file = "bitarray-3.6.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:66d8b7a89fac6042f7df9ea97d97ed0f5e404281110a882e3babd909161f85b6"}, + {file = "bitarray-3.6.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62f71b268f14ee6cc3045b95441bfe0518cef1d0b2ffbc6f3e9758f786ff5a03"}, + {file = "bitarray-3.6.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72760411d60d8d76979a20ed3f15586d824db04668b581b86e61158c2b616db0"}, + {file = "bitarray-3.6.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc5029c61f9ebb2d4c247f13584a0ef0e8e49abb13e56460310821aca3ffcaf"}, + {file = "bitarray-3.6.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:0ac446f557eb28e3f7c65372608810ff073840627e9037e22ed10bd081793a34"}, + {file = "bitarray-3.6.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b9ae0008cff25e154ef1e3975a1705d344e844ffdeb34c25b007fd48c876e95d"}, + {file = "bitarray-3.6.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:db78cc5c03b446a43413165aa873e2f408e9fd5ddb45533e7bd3b638bace867c"}, + {file = "bitarray-3.6.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cfbdccddaa0ff07789e9e180db127906c676e479e05c04830cd458945de3511"}, + {file = "bitarray-3.6.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:222cb27ff05bc0aec72498d075dba1facec49a76a7da45740690cebbe3e81e43"}, + {file = "bitarray-3.6.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b58a672ec448fb36839a5fc7bf2b2f60df9a97b872d8bd6ca1a28da6126f5c7"}, + {file = "bitarray-3.6.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b37c9ea942395de029be270f0eca8c141eb14e8455941495cd3b6f95bbe465f4"}, + {file = "bitarray-3.6.0.tar.gz", hash = "sha256:20febc849a1f858e6a57a7d47b323fe9e727c579ddd526d317ad8831748a66a8"}, ] [[package]] name = "build" -version = "1.2.1" +version = "1.2.2.post1" 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"}, + {file = "build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5"}, + {file = "build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7"}, ] [package.dependencies] @@ -220,13 +229,13 @@ virtualenv = ["virtualenv (>=20.0.35)"] [[package]] name = "cachecontrol" -version = "0.14.0" +version = "0.14.3" description = "httplib2 caching for requests" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "cachecontrol-0.14.0-py3-none-any.whl", hash = "sha256:f5bf3f0620c38db2e5122c0726bdebb0d16869de966ea6a2befe92470b740ea0"}, - {file = "cachecontrol-0.14.0.tar.gz", hash = "sha256:7db1195b41c81f8274a7bbd97c956f44e8348265a1bc7641c37dfebc39f0c938"}, + {file = "cachecontrol-0.14.3-py3-none-any.whl", hash = "sha256:b35e44a3113f17d2a31c1e6b27b9de6d4405f84ae51baa8c1d3cc5b633010cae"}, + {file = "cachecontrol-0.14.3.tar.gz", hash = "sha256:73e7efec4b06b20d9267b441c1f733664f989fb8688391b670ca812d70795d11"}, ] [package.dependencies] @@ -235,65 +244,72 @@ 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"] +dev = ["CacheControl[filecache,redis]", "build", "cherrypy", "codespell[tomli]", "furo", "mypy", "pytest", "pytest-cov", "ruff", "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" +version = "2.0.1" description = "A decorator for caching properties in classes." optional = false -python-versions = "*" +python-versions = ">=3.8" 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"}, + {file = "cached_property-2.0.1-py3-none-any.whl", hash = "sha256:f617d70ab1100b7bcf6e42228f9ddcb78c676ffa167278d9f730d1c2fba69ccb"}, + {file = "cached_property-2.0.1.tar.gz", hash = "sha256:484d617105e3ee0e4f1f58725e72a8ef9e93deee462222dbd51cd91230897641"}, ] [[package]] name = "cbor2" -version = "5.6.4" +version = "5.6.5" 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"}, + {file = "cbor2-5.6.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e16c4a87fc999b4926f5c8f6c696b0d251b4745bc40f6c5aee51d69b30b15ca2"}, + {file = "cbor2-5.6.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87026fc838370d69f23ed8572939bd71cea2b3f6c8f8bb8283f573374b4d7f33"}, + {file = "cbor2-5.6.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a88f029522aec5425fc2f941b3df90da7688b6756bd3f0472ab886d21208acbd"}, + {file = "cbor2-5.6.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9d15b638539b68aa5d5eacc56099b4543a38b2d2c896055dccf7e83d24b7955"}, + {file = "cbor2-5.6.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:47261f54a024839ec649b950013c4de5b5f521afe592a2688eebbe22430df1dc"}, + {file = "cbor2-5.6.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:559dcf0d897260a9e95e7b43556a62253e84550b77147a1ad4d2c389a2a30192"}, + {file = "cbor2-5.6.5-cp310-cp310-win_amd64.whl", hash = "sha256:5b856fda4c50c5bc73ed3664e64211fa4f015970ed7a15a4d6361bd48462feaf"}, + {file = "cbor2-5.6.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:863e0983989d56d5071270790e7ed8ddbda88c9e5288efdb759aba2efee670bc"}, + {file = "cbor2-5.6.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5cff06464b8f4ca6eb9abcba67bda8f8334a058abc01005c8e616728c387ad32"}, + {file = "cbor2-5.6.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4c7dbcdc59ea7f5a745d3e30ee5e6b6ff5ce7ac244aa3de6786391b10027bb3"}, + {file = "cbor2-5.6.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34cf5ab0dc310c3d0196caa6ae062dc09f6c242e2544bea01691fe60c0230596"}, + {file = "cbor2-5.6.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6797b824b26a30794f2b169c0575301ca9b74ae99064e71d16e6ba0c9057de51"}, + {file = "cbor2-5.6.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:73b9647eed1493097db6aad61e03d8f1252080ee041a1755de18000dd2c05f37"}, + {file = "cbor2-5.6.5-cp311-cp311-win_amd64.whl", hash = "sha256:6e14a1bf6269d25e02ef1d4008e0ce8880aa271d7c6b4c329dba48645764f60e"}, + {file = "cbor2-5.6.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e25c2aebc9db99af7190e2261168cdde8ed3d639ca06868e4f477cf3a228a8e9"}, + {file = "cbor2-5.6.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fde21ac1cf29336a31615a2c469a9cb03cf0add3ae480672d4d38cda467d07fc"}, + {file = "cbor2-5.6.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8947c102cac79d049eadbd5e2ffb8189952890df7cbc3ee262bbc2f95b011a9"}, + {file = "cbor2-5.6.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38886c41bebcd7dca57739439455bce759f1e4c551b511f618b8e9c1295b431b"}, + {file = "cbor2-5.6.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae2b49226224e92851c333b91d83292ec62eba53a19c68a79890ce35f1230d70"}, + {file = "cbor2-5.6.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2764804ffb6553283fc4afb10a280715905a4cea4d6dc7c90d3e89c4a93bc8d"}, + {file = "cbor2-5.6.5-cp312-cp312-win_amd64.whl", hash = "sha256:a3ac50485cf67dfaab170a3e7b527630e93cb0a6af8cdaa403054215dff93adf"}, + {file = "cbor2-5.6.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f0d0a9c5aabd48ecb17acf56004a7542a0b8d8212be52f3102b8218284bd881e"}, + {file = "cbor2-5.6.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61ceb77e6aa25c11c814d4fe8ec9e3bac0094a1f5bd8a2a8c95694596ea01e08"}, + {file = "cbor2-5.6.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97a7e409b864fecf68b2ace8978eb5df1738799a333ec3ea2b9597bfcdd6d7d2"}, + {file = "cbor2-5.6.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f6d69f38f7d788b04c09ef2b06747536624b452b3c8b371ab78ad43b0296fab"}, + {file = "cbor2-5.6.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f91e6d74fa6917df31f8757fdd0e154203b0dd0609ec53eb957016a2b474896a"}, + {file = "cbor2-5.6.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5ce13a27ef8fddf643fc17a753fe34aa72b251d03c23da6a560c005dc171085b"}, + {file = "cbor2-5.6.5-cp313-cp313-win_amd64.whl", hash = "sha256:54c72a3207bb2d4480c2c39dad12d7971ce0853a99e3f9b8d559ce6eac84f66f"}, + {file = "cbor2-5.6.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4586a4f65546243096e56a3f18f29d60752ee9204722377021b3119a03ed99ff"}, + {file = "cbor2-5.6.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d1a18b3a58dcd9b40ab55c726160d4a6b74868f2a35b71f9e726268b46dc6a2"}, + {file = "cbor2-5.6.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a83b76367d1c3e69facbcb8cdf65ed6948678e72f433137b41d27458aa2a40cb"}, + {file = "cbor2-5.6.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90bfa36944caccec963e6ab7e01e64e31cc6664535dc06e6295ee3937c999cbb"}, + {file = "cbor2-5.6.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:37096663a5a1c46a776aea44906cbe5fa3952f29f50f349179c00525d321c862"}, + {file = "cbor2-5.6.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:93676af02bd9a0b4a62c17c5b20f8e9c37b5019b1a24db70a2ee6cb770423568"}, + {file = "cbor2-5.6.5-cp38-cp38-win_amd64.whl", hash = "sha256:8f747b7a9aaa58881a0c5b4cd4a9b8fb27eca984ed261a769b61de1f6b5bd1e6"}, + {file = "cbor2-5.6.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:94885903105eec66d7efb55f4ce9884fdc5a4d51f3bd75b6fedc68c5c251511b"}, + {file = "cbor2-5.6.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fe11c2eb518c882cfbeed456e7a552e544893c17db66fe5d3230dbeaca6b615c"}, + {file = "cbor2-5.6.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66dd25dd919cddb0b36f97f9ccfa51947882f064729e65e6bef17c28535dc459"}, + {file = "cbor2-5.6.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa61a02995f3a996c03884cf1a0b5733f88cbfd7fa0e34944bf678d4227ee712"}, + {file = "cbor2-5.6.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:824f202b556fc204e2e9a67d6d6d624e150fbd791278ccfee24e68caec578afd"}, + {file = "cbor2-5.6.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7488aec919f8408f9987a3a32760bd385d8628b23a35477917aa3923ff6ad45f"}, + {file = "cbor2-5.6.5-cp39-cp39-win_amd64.whl", hash = "sha256:a34ee99e86b17444ecbe96d54d909dd1a20e2da9f814ae91b8b71cf1ee2a95e4"}, + {file = "cbor2-5.6.5-py3-none-any.whl", hash = "sha256:3038523b8fc7de312bb9cdcbbbd599987e64307c4db357cd2030c472a6c7d468"}, + {file = "cbor2-5.6.5.tar.gz", hash = "sha256:b682820677ee1dbba45f7da11898d2720f92e06be36acec290867d5ebf3d7e09"}, ] [package.extras] @@ -303,74 +319,89 @@ test = ["coverage (>=7)", "hypothesis", "pytest"] [[package]] name = "certifi" -version = "2024.6.2" +version = "2025.7.14" description = "Python package for providing Mozilla's CA Bundle." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, - {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, + {file = "certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2"}, + {file = "certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995"}, ] [[package]] name = "cffi" -version = "1.16.0" +version = "1.17.1" 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"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] [package.dependencies] @@ -378,195 +409,212 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "3.3.2" +version = "3.4.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"}, +python-versions = ">=3.7" +files = [ + {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, + {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, + {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, ] [[package]] name = "ckzg" -version = "1.0.2" +version = "2.1.1" 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"}, + {file = "ckzg-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b9825a1458219e8b4b023012b8ef027ef1f47e903f9541cbca4615f80132730"}, + {file = "ckzg-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e2a40a3ba65cca4b52825d26829e6f7eb464aa27a9e9efb6b8b2ce183442c741"}, + {file = "ckzg-2.1.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1d753fbe85be7c21602eddc2d40e0915e25fce10329f4f801a0002a4f886cc7"}, + {file = "ckzg-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d76b50527f1d12430bf118aff6fa4051e9860eada43f29177258b8d399448ea"}, + {file = "ckzg-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44c8603e43c021d100f355f50189183135d1df3cbbddb8881552d57fbf421dde"}, + {file = "ckzg-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:38707a638c9d715b3c30b29352b969f78d8fc10faed7db5faf517f04359895c0"}, + {file = "ckzg-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:52c4d257bdcbe822d20c5cd24c8154ec5aac33c49a8f5a19e716d9107a1c8785"}, + {file = "ckzg-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1507f7bfb9bcf51d816db5d8d0f0ed53c8289605137820d437b69daea8333e16"}, + {file = "ckzg-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:d02eaaf4f841910133552b3a051dea53bcfe60cd98199fc4cf80b27609d8baa2"}, + {file = "ckzg-2.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:465e2b71cf9dc383f66f1979269420a0da9274a3a9e98b1a4455e84927dfe491"}, + {file = "ckzg-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ee2f26f17a64ad0aab833d637b276f28486b82a29e34f32cf54b237b8f8ab72d"}, + {file = "ckzg-2.1.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99cc2c4e9fb8c62e3e0862c7f4df9142f07ba640da17fded5f6e0fd09f75909f"}, + {file = "ckzg-2.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773dd016693d74aca1f5d7982db2bad7dde2e147563aeb16a783f7e5f69c01fe"}, + {file = "ckzg-2.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af2b2144f87ba218d8db01382a961b3ecbdde5ede4fa0d9428d35f8c8a595ba"}, + {file = "ckzg-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8f55e63d3f7c934a2cb53728ed1d815479e177aca8c84efe991c2920977cff6"}, + {file = "ckzg-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ecb42aaa0ffa427ff14a9dde9356ba69e5ae6014650b397af55b31bdae7a9b6e"}, + {file = "ckzg-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5a01514239f12fb1a7ad9009c20062a4496e13b09541c1a65f97e295da648c70"}, + {file = "ckzg-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:6516b9684aae262c85cf7fddd8b585b8139ad20e08ec03994e219663abbb0916"}, + {file = "ckzg-2.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c60e8903344ce98ce036f0fabacce952abb714cad4607198b2f0961c28b8aa72"}, + {file = "ckzg-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4299149dd72448e5a8d2d1cc6cc7472c92fc9d9f00b1377f5b017c089d9cd92"}, + {file = "ckzg-2.1.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:025dd31ffdcc799f3ff842570a2a6683b6c5b01567da0109c0c05d11768729c4"}, + {file = "ckzg-2.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b42ab8385c273f40a693657c09d2bba40cb4f4666141e263906ba2e519e80bd"}, + {file = "ckzg-2.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1be3890fc1543f4fcfc0063e4baf5c036eb14bcf736dabdc6171ab017e0f1671"}, + {file = "ckzg-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b754210ded172968b201e2d7252573af6bf52d6ad127ddd13d0b9a45a51dae7b"}, + {file = "ckzg-2.1.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2f8fda87865897a269c4e951e3826c2e814427a6cdfed6731cccfe548f12b36"}, + {file = "ckzg-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:98e70b5923d77c7359432490145e9d1ab0bf873eb5de56ec53f4a551d7eaec79"}, + {file = "ckzg-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:42af7bde4ca45469cd93a96c3d15d69d51d40e7f0d30e3a20711ebd639465fcb"}, + {file = "ckzg-2.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e4edfdaf87825ff43b9885fabfdea408737a714f4ce5467100d9d1d0a03b673"}, + {file = "ckzg-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:815fd2a87d6d6c57d669fda30c150bc9bf387d47e67d84535aa42b909fdc28ea"}, + {file = "ckzg-2.1.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c32466e809b1ab3ff01d3b0bb0b9912f61dcf72957885615595f75e3f7cc10e5"}, + {file = "ckzg-2.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f11b73ccf37b12993f39a7dbace159c6d580aacacde6ee17282848476550ddbc"}, + {file = "ckzg-2.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3b9433a1f2604bd9ac1646d3c83ad84a850d454d3ac589fe8e70c94b38a6b0"}, + {file = "ckzg-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b7d7e1b5ea06234558cd95c483666fd785a629b720a7f1622b3cbffebdc62033"}, + {file = "ckzg-2.1.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9f5556e6675866040cc4335907be6c537051e7f668da289fa660fdd8a30c9ddb"}, + {file = "ckzg-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55b2ba30c5c9daac0c55f1aac851f1b7bf1f7aa0028c2db4440e963dd5b866d6"}, + {file = "ckzg-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:10d201601fc8f28c0e8cec3406676797024dd374c367bbeec5a7a9eac9147237"}, + {file = "ckzg-2.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:5f46c8fd5914db62b446baf62c8599da07e6f91335779a9709c554ef300a7b60"}, + {file = "ckzg-2.1.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60f14612c2be84f405755d734b0ad4e445db8af357378b95b72339b59e1f4fcf"}, + {file = "ckzg-2.1.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:929e6e793039f42325988004a90d16b0ef4fc7e1330142e180f0298f2ed4527c"}, + {file = "ckzg-2.1.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2beac2af53ea181118179570ecc81d8a8fc52c529553d7fd8786fd100a2aa39b"}, + {file = "ckzg-2.1.1-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:2432d48aec296baee79556bfde3bddd2799bcc7753cd1f0d0c9a3b0333935637"}, + {file = "ckzg-2.1.1-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:4c2e8180b54261ccae2bf8acd003ccee7394d88d073271af19c5f2ac4a54c607"}, + {file = "ckzg-2.1.1-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:c44e36bd53d9dd0ab29bd6ed2d67ea43c48eecd57f8197854a75742213938bf5"}, + {file = "ckzg-2.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:10befd86e643d38ac468151cdfb71e79b2d46aa6397b81db4224f4f6995262eb"}, + {file = "ckzg-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:138a9324ad8e8a9ade464043dc3a84afe12996516788f2ed841bdbe5d123af81"}, + {file = "ckzg-2.1.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:635af0a33a10c9ac275f3efc142880a6b46ac63f4495f600aae05266af4fadff"}, + {file = "ckzg-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:360e263677ee5aedb279b42cf54b51c905ddcac9181c65d89ec0b298d3f31ec0"}, + {file = "ckzg-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f81395f77bfd069831cbb1de9d473c7044abe9ce6cd562ef6ccd76d23abcef43"}, + {file = "ckzg-2.1.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:db1ff122f8dc10c9500a00a4d680c3c38f4e19b01d95f38e0f5bc55a77c8ab98"}, + {file = "ckzg-2.1.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:1f82f539949ff3c6a5accfdd211919a3e374d354b3665d062395ebdbf8befaeb"}, + {file = "ckzg-2.1.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:5bc8ae85df97467e84abb491b516e25dbca36079e766eafce94d1bc45e4aaa35"}, + {file = "ckzg-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:e749ce9fcb26e37101f2af8ba9c6376b66eb598880d35e457890044ba77c1cf7"}, + {file = "ckzg-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b00201979a64fd7e6029f64d791af42374febb42452537933e881b49d4e8c77"}, + {file = "ckzg-2.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c61c437ba714ab7c802b51fb30125e8f8550e1320fe9050d20777420c153a2b3"}, + {file = "ckzg-2.1.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8bd54394376598a7c081df009cfde3cc447beb640b6c6b7534582a31e6290ac7"}, + {file = "ckzg-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67d8c6680a7b370718af59cc17a983752706407cfbcace013ee707646d1f7b00"}, + {file = "ckzg-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55f6c57b24bc4fe16b1b50324ef8548f2a5053ad76bf90c618e2f88c040120d7"}, + {file = "ckzg-2.1.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f55fc10fb1b217c66bfe14e05535e5e61cfbb2a95dbb9b93a80984fa2ab4a7c0"}, + {file = "ckzg-2.1.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:2e23e3198f8933f0140ef8b2aeba717d8de03ec7b8fb1ee946f8d39986ce0811"}, + {file = "ckzg-2.1.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2f9caf88bf216756bb1361b92616c750a933c9afb67972ad05c212649a9be520"}, + {file = "ckzg-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:30e0c2d258bbc0c099c2d1854c6ffa2fd9abf6138b9c81f855e1936f6cb259aa"}, + {file = "ckzg-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6239d3d2e30cb894ca4e7765b1097eb6a70c0ecbe5f8e0b023fbf059472d4ac"}, + {file = "ckzg-2.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:909ebabc253a98d9dc1d51f93dc75990134bfe296c947e1ecf3b7142aba5108e"}, + {file = "ckzg-2.1.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0700dace6559b288b42ca8622be89c2a43509881ed6f4f0bfb6312bcceed0cb9"}, + {file = "ckzg-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a36aeabd243e906314694b4a107de99b0c4473ff1825fcb06acd147ffb1951a"}, + {file = "ckzg-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d884e8f9c7d7839f1a95561f4479096dce21d45b0c5dd013dc0842550cea1cad"}, + {file = "ckzg-2.1.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:338fdf4a0b463973fc7b7e4dc289739db929e61d7cb9ba984ebbe9c49d3aa6f9"}, + {file = "ckzg-2.1.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c594036d3408eebdcd8ab2c7aab7308239ed4df3d94f3211b7cf253f228fb0b7"}, + {file = "ckzg-2.1.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b0912ebb328ced510250a2325b095917db19c1a014792a0bf4c389f0493e39de"}, + {file = "ckzg-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:5046aceb03482ddf7200f2f5c643787b100e6fb96919852faf1c79f8870c80a1"}, + {file = "ckzg-2.1.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:375918e25eafb9bafe5215ab91698504cba3fe51b4fe92f5896af6c5663f50c6"}, + {file = "ckzg-2.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:38b3b7802c76d4ad015db2b7a79a49c193babae50ee5f77e9ac2865c9e9ddb09"}, + {file = "ckzg-2.1.1-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:438a5009fd254ace0bc1ad974d524547f1a41e6aa5e778c5cd41f4ee3106bcd6"}, + {file = "ckzg-2.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ce11cc163a2e0dab3af7455aca7053f9d5bb8d157f231acc7665fd230565d48"}, + {file = "ckzg-2.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b53964c07f6a076e97eaa1ef35045e935d7040aff14f80bae7e9105717702d05"}, + {file = "ckzg-2.1.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cf085f15ae52ab2599c9b5a3d5842794bcf5613b7f58661fbfb0c5d9eac988b9"}, + {file = "ckzg-2.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4b0c850bd6cad22ac79b2a2ab884e0e7cd2b54a67d643cd616c145ebdb535a11"}, + {file = "ckzg-2.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:26951f36bb60c9150bbd38110f5e1625596f9779dad54d1d492d8ec38bc84e3a"}, + {file = "ckzg-2.1.1-pp311-pypy311_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbe12445e49c4bee67746b7b958e90a973b0de116d0390749b0df351d94e9a8c"}, + {file = "ckzg-2.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71c5d4f66f09de4a99271acac74d2acb3559a77de77a366b34a91e99e8822667"}, + {file = "ckzg-2.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42673c1d007372a4e8b48f6ef8f0ce31a9688a463317a98539757d1e2fb1ecc7"}, + {file = "ckzg-2.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:57a7dc41ec6b69c1d9117eb61cf001295e6b4f67a736020442e71fb4367fb1a5"}, + {file = "ckzg-2.1.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:22e4606857660b2ffca2f7b96c01d0b18b427776d8a93320caf2b1c7342881fe"}, + {file = "ckzg-2.1.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b55475126a9efc82d61718b2d2323502e33d9733b7368c407954592ccac87faf"}, + {file = "ckzg-2.1.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5939ae021557c64935a7649b13f4a58f1bd35c39998fd70d0cefb5cbaf77d1be"}, + {file = "ckzg-2.1.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad1ec5f9726a9946508a4a2aace298172aa778de9ebbe97e21c873c3688cc87"}, + {file = "ckzg-2.1.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93d7edea3bb1602b18b394ebeec231d89dfd8d48fdd06571cb7656107aa62226"}, + {file = "ckzg-2.1.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c450d77af61011ced3777f97431d5f1bc148ca5362c67caf516aa2f6ef7e4817"}, + {file = "ckzg-2.1.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:8fc8df4e17e08974961d6c14f6c57ccfd3ad5aede74598292ec6e5d6fc2dbcac"}, + {file = "ckzg-2.1.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93338da8011790ef53a68475678bc951fa7b337db027d8edbf1889e59691161c"}, + {file = "ckzg-2.1.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4889f24b4ff614f39e3584709de1a3b0f1556675b33e360dbcb28cda827296d4"}, + {file = "ckzg-2.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b58fbb1a9be4ae959feede8f103e12d80ef8453bdc6483bfdaf164879a2b80"}, + {file = "ckzg-2.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6136c5b5377c7f7033323b25bc2c7b43c025d44ed73e338c02f9f59df9460e5b"}, + {file = "ckzg-2.1.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fa419b92a0e8766deb7157fb28b6542c1c3f8dde35d2a69d1f91ec8e41047d35"}, + {file = "ckzg-2.1.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:95cd6c8eb3ab5148cd97ab5bf44b84fd7f01adf4b36ffd070340ad2d9309b3f9"}, + {file = "ckzg-2.1.1-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:848191201052b48bdde18680ebb77bf8da99989270e5aea8b0290051f5ac9468"}, + {file = "ckzg-2.1.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4716c0564131b0d609fb8856966e83892b9809cf6719c7edd6495b960451f8b"}, + {file = "ckzg-2.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c399168ba199827dee3104b00cdc7418d4dbdf47a5fcbe7cf938fc928037534"}, + {file = "ckzg-2.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:724f29f9f110d9ef42a6a1a1a7439548c61070604055ef96b2ab7a884cad4192"}, + {file = "ckzg-2.1.1.tar.gz", hash = "sha256:d6b306b7ec93a24e4346aa53d07f7f75053bc0afc7398e35fa649e5f9d48fcc4"}, ] [[package]] @@ -586,13 +634,13 @@ rapidfuzz = ">=3.0.0,<4.0.0" [[package]] name = "click" -version = "8.1.8" +version = "8.2.1" description = "Composable command line interface toolkit" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" files = [ - {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, - {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, + {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, + {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, ] [package.dependencies] @@ -611,63 +659,99 @@ files = [ [[package]] name = "coverage" -version = "7.5.3" +version = "7.10.1" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" 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"}, + {file = "coverage-7.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1c86eb388bbd609d15560e7cc0eb936c102b6f43f31cf3e58b4fd9afe28e1372"}, + {file = "coverage-7.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b4ba0f488c1bdb6bd9ba81da50715a372119785458831c73428a8566253b86b"}, + {file = "coverage-7.10.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083442ecf97d434f0cb3b3e3676584443182653da08b42e965326ba12d6b5f2a"}, + {file = "coverage-7.10.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c1a40c486041006b135759f59189385da7c66d239bad897c994e18fd1d0c128f"}, + {file = "coverage-7.10.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3beb76e20b28046989300c4ea81bf690df84ee98ade4dc0bbbf774a28eb98440"}, + {file = "coverage-7.10.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bc265a7945e8d08da28999ad02b544963f813a00f3ed0a7a0ce4165fd77629f8"}, + {file = "coverage-7.10.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:47c91f32ba4ac46f1e224a7ebf3f98b4b24335bad16137737fe71a5961a0665c"}, + {file = "coverage-7.10.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1a108dd78ed185020f66f131c60078f3fae3f61646c28c8bb4edd3fa121fc7fc"}, + {file = "coverage-7.10.1-cp310-cp310-win32.whl", hash = "sha256:7092cc82382e634075cc0255b0b69cb7cada7c1f249070ace6a95cb0f13548ef"}, + {file = "coverage-7.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:ac0c5bba938879c2fc0bc6c1b47311b5ad1212a9dcb8b40fe2c8110239b7faed"}, + {file = "coverage-7.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b45e2f9d5b0b5c1977cb4feb5f594be60eb121106f8900348e29331f553a726f"}, + {file = "coverage-7.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a7a4d74cb0f5e3334f9aa26af7016ddb94fb4bfa11b4a573d8e98ecba8c34f1"}, + {file = "coverage-7.10.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d4b0aab55ad60ead26159ff12b538c85fbab731a5e3411c642b46c3525863437"}, + {file = "coverage-7.10.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dcc93488c9ebd229be6ee1f0d9aad90da97b33ad7e2912f5495804d78a3cd6b7"}, + {file = "coverage-7.10.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa309df995d020f3438407081b51ff527171cca6772b33cf8f85344b8b4b8770"}, + {file = "coverage-7.10.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cfb8b9d8855c8608f9747602a48ab525b1d320ecf0113994f6df23160af68262"}, + {file = "coverage-7.10.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:320d86da829b012982b414c7cdda65f5d358d63f764e0e4e54b33097646f39a3"}, + {file = "coverage-7.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dc60ddd483c556590da1d9482a4518292eec36dd0e1e8496966759a1f282bcd0"}, + {file = "coverage-7.10.1-cp311-cp311-win32.whl", hash = "sha256:4fcfe294f95b44e4754da5b58be750396f2b1caca8f9a0e78588e3ef85f8b8be"}, + {file = "coverage-7.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:efa23166da3fe2915f8ab452dde40319ac84dc357f635737174a08dbd912980c"}, + {file = "coverage-7.10.1-cp311-cp311-win_arm64.whl", hash = "sha256:d12b15a8c3759e2bb580ffa423ae54be4f184cf23beffcbd641f4fe6e1584293"}, + {file = "coverage-7.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6b7dc7f0a75a7eaa4584e5843c873c561b12602439d2351ee28c7478186c4da4"}, + {file = "coverage-7.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:607f82389f0ecafc565813aa201a5cade04f897603750028dd660fb01797265e"}, + {file = "coverage-7.10.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f7da31a1ba31f1c1d4d5044b7c5813878adae1f3af8f4052d679cc493c7328f4"}, + {file = "coverage-7.10.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51fe93f3fe4f5d8483d51072fddc65e717a175490804e1942c975a68e04bf97a"}, + {file = "coverage-7.10.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e59d00830da411a1feef6ac828b90bbf74c9b6a8e87b8ca37964925bba76dbe"}, + {file = "coverage-7.10.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:924563481c27941229cb4e16eefacc35da28563e80791b3ddc5597b062a5c386"}, + {file = "coverage-7.10.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ca79146ee421b259f8131f153102220b84d1a5e6fb9c8aed13b3badfd1796de6"}, + {file = "coverage-7.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2b225a06d227f23f386fdc0eab471506d9e644be699424814acc7d114595495f"}, + {file = "coverage-7.10.1-cp312-cp312-win32.whl", hash = "sha256:5ba9a8770effec5baaaab1567be916c87d8eea0c9ad11253722d86874d885eca"}, + {file = "coverage-7.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:9eb245a8d8dd0ad73b4062135a251ec55086fbc2c42e0eb9725a9b553fba18a3"}, + {file = "coverage-7.10.1-cp312-cp312-win_arm64.whl", hash = "sha256:7718060dd4434cc719803a5e526838a5d66e4efa5dc46d2b25c21965a9c6fcc4"}, + {file = "coverage-7.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ebb08d0867c5a25dffa4823377292a0ffd7aaafb218b5d4e2e106378b1061e39"}, + {file = "coverage-7.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f32a95a83c2e17422f67af922a89422cd24c6fa94041f083dd0bb4f6057d0bc7"}, + {file = "coverage-7.10.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c4c746d11c8aba4b9f58ca8bfc6fbfd0da4efe7960ae5540d1a1b13655ee8892"}, + {file = "coverage-7.10.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7f39edd52c23e5c7ed94e0e4bf088928029edf86ef10b95413e5ea670c5e92d7"}, + {file = "coverage-7.10.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab6e19b684981d0cd968906e293d5628e89faacb27977c92f3600b201926b994"}, + {file = "coverage-7.10.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5121d8cf0eacb16133501455d216bb5f99899ae2f52d394fe45d59229e6611d0"}, + {file = "coverage-7.10.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df1c742ca6f46a6f6cbcaef9ac694dc2cb1260d30a6a2f5c68c5f5bcfee1cfd7"}, + {file = "coverage-7.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:40f9a38676f9c073bf4b9194707aa1eb97dca0e22cc3766d83879d72500132c7"}, + {file = "coverage-7.10.1-cp313-cp313-win32.whl", hash = "sha256:2348631f049e884839553b9974f0821d39241c6ffb01a418efce434f7eba0fe7"}, + {file = "coverage-7.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:4072b31361b0d6d23f750c524f694e1a417c1220a30d3ef02741eed28520c48e"}, + {file = "coverage-7.10.1-cp313-cp313-win_arm64.whl", hash = "sha256:3e31dfb8271937cab9425f19259b1b1d1f556790e98eb266009e7a61d337b6d4"}, + {file = "coverage-7.10.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1c4f679c6b573a5257af6012f167a45be4c749c9925fd44d5178fd641ad8bf72"}, + {file = "coverage-7.10.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:871ebe8143da284bd77b84a9136200bd638be253618765d21a1fce71006d94af"}, + {file = "coverage-7.10.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:998c4751dabf7d29b30594af416e4bf5091f11f92a8d88eb1512c7ba136d1ed7"}, + {file = "coverage-7.10.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:780f750a25e7749d0af6b3631759c2c14f45de209f3faaa2398312d1c7a22759"}, + {file = "coverage-7.10.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:590bdba9445df4763bdbebc928d8182f094c1f3947a8dc0fc82ef014dbdd8324"}, + {file = "coverage-7.10.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b2df80cb6a2af86d300e70acb82e9b79dab2c1e6971e44b78dbfc1a1e736b53"}, + {file = "coverage-7.10.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d6a558c2725bfb6337bf57c1cd366c13798bfd3bfc9e3dd1f4a6f6fc95a4605f"}, + {file = "coverage-7.10.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e6150d167f32f2a54690e572e0a4c90296fb000a18e9b26ab81a6489e24e78dd"}, + {file = "coverage-7.10.1-cp313-cp313t-win32.whl", hash = "sha256:d946a0c067aa88be4a593aad1236493313bafaa27e2a2080bfe88db827972f3c"}, + {file = "coverage-7.10.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e37c72eaccdd5ed1130c67a92ad38f5b2af66eeff7b0abe29534225db2ef7b18"}, + {file = "coverage-7.10.1-cp313-cp313t-win_arm64.whl", hash = "sha256:89ec0ffc215c590c732918c95cd02b55c7d0f569d76b90bb1a5e78aa340618e4"}, + {file = "coverage-7.10.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:166d89c57e877e93d8827dac32cedae6b0277ca684c6511497311249f35a280c"}, + {file = "coverage-7.10.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bed4a2341b33cd1a7d9ffc47df4a78ee61d3416d43b4adc9e18b7d266650b83e"}, + {file = "coverage-7.10.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ddca1e4f5f4c67980533df01430184c19b5359900e080248bbf4ed6789584d8b"}, + {file = "coverage-7.10.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:37b69226001d8b7de7126cad7366b0778d36777e4d788c66991455ba817c5b41"}, + {file = "coverage-7.10.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2f22102197bcb1722691296f9e589f02b616f874e54a209284dd7b9294b0b7f"}, + {file = "coverage-7.10.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1e0c768b0f9ac5839dac5cf88992a4bb459e488ee8a1f8489af4cb33b1af00f1"}, + {file = "coverage-7.10.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:991196702d5e0b120a8fef2664e1b9c333a81d36d5f6bcf6b225c0cf8b0451a2"}, + {file = "coverage-7.10.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae8e59e5f4fd85d6ad34c2bb9d74037b5b11be072b8b7e9986beb11f957573d4"}, + {file = "coverage-7.10.1-cp314-cp314-win32.whl", hash = "sha256:042125c89cf74a074984002e165d61fe0e31c7bd40ebb4bbebf07939b5924613"}, + {file = "coverage-7.10.1-cp314-cp314-win_amd64.whl", hash = "sha256:a22c3bfe09f7a530e2c94c87ff7af867259c91bef87ed2089cd69b783af7b84e"}, + {file = "coverage-7.10.1-cp314-cp314-win_arm64.whl", hash = "sha256:ee6be07af68d9c4fca4027c70cea0c31a0f1bc9cb464ff3c84a1f916bf82e652"}, + {file = "coverage-7.10.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d24fb3c0c8ff0d517c5ca5de7cf3994a4cd559cde0315201511dbfa7ab528894"}, + {file = "coverage-7.10.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1217a54cfd79be20512a67ca81c7da3f2163f51bbfd188aab91054df012154f5"}, + {file = "coverage-7.10.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:51f30da7a52c009667e02f125737229d7d8044ad84b79db454308033a7808ab2"}, + {file = "coverage-7.10.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ed3718c757c82d920f1c94089066225ca2ad7f00bb904cb72b1c39ebdd906ccb"}, + {file = "coverage-7.10.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc452481e124a819ced0c25412ea2e144269ef2f2534b862d9f6a9dae4bda17b"}, + {file = "coverage-7.10.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9d6f494c307e5cb9b1e052ec1a471060f1dea092c8116e642e7a23e79d9388ea"}, + {file = "coverage-7.10.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fc0e46d86905ddd16b85991f1f4919028092b4e511689bbdaff0876bd8aab3dd"}, + {file = "coverage-7.10.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80b9ccd82e30038b61fc9a692a8dc4801504689651b281ed9109f10cc9fe8b4d"}, + {file = "coverage-7.10.1-cp314-cp314t-win32.whl", hash = "sha256:e58991a2b213417285ec866d3cd32db17a6a88061a985dbb7e8e8f13af429c47"}, + {file = "coverage-7.10.1-cp314-cp314t-win_amd64.whl", hash = "sha256:e88dd71e4ecbc49d9d57d064117462c43f40a21a1383507811cf834a4a620651"}, + {file = "coverage-7.10.1-cp314-cp314t-win_arm64.whl", hash = "sha256:1aadfb06a30c62c2eb82322171fe1f7c288c80ca4156d46af0ca039052814bab"}, + {file = "coverage-7.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:57b6e8789cbefdef0667e4a94f8ffa40f9402cee5fc3b8e4274c894737890145"}, + {file = "coverage-7.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:85b22a9cce00cb03156334da67eb86e29f22b5e93876d0dd6a98646bb8a74e53"}, + {file = "coverage-7.10.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:97b6983a2f9c76d345ca395e843a049390b39652984e4a3b45b2442fa733992d"}, + {file = "coverage-7.10.1-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ddf2a63b91399a1c2f88f40bc1705d5a7777e31c7e9eb27c602280f477b582ba"}, + {file = "coverage-7.10.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47ab6dbbc31a14c5486420c2c1077fcae692097f673cf5be9ddbec8cdaa4cdbc"}, + {file = "coverage-7.10.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:21eb7d8b45d3700e7c2936a736f732794c47615a20f739f4133d5230a6512a88"}, + {file = "coverage-7.10.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:283005bb4d98ae33e45f2861cd2cde6a21878661c9ad49697f6951b358a0379b"}, + {file = "coverage-7.10.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:fefe31d61d02a8b2c419700b1fade9784a43d726de26495f243b663cd9fe1513"}, + {file = "coverage-7.10.1-cp39-cp39-win32.whl", hash = "sha256:e8ab8e4c7ec7f8a55ac05b5b715a051d74eac62511c6d96d5bb79aaafa3b04cf"}, + {file = "coverage-7.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:c36baa0ecde742784aa76c2b816466d3ea888d5297fda0edbac1bf48fa94688a"}, + {file = "coverage-7.10.1-py3-none-any.whl", hash = "sha256:fa2a258aa6bf188eb9a8948f7102a83da7c430a0dce918dbd8b60ef8fcb772d7"}, + {file = "coverage-7.10.1.tar.gz", hash = "sha256:ae2b4856f29ddfe827106794f3589949a57da6f0d38ab01e24ec35107979ba57"}, ] [package.dependencies] @@ -689,56 +773,61 @@ files = [ [[package]] name = "cryptography" -version = "42.0.8" +version = "45.0.5" 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"] +python-versions = "!=3.9.0,!=3.9.1,>=3.7" +files = [ + {file = "cryptography-45.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:101ee65078f6dd3e5a028d4f19c07ffa4dd22cce6a20eaa160f8b5219911e7d8"}, + {file = "cryptography-45.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a264aae5f7fbb089dbc01e0242d3b67dffe3e6292e1f5182122bdf58e65215d"}, + {file = "cryptography-45.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e74d30ec9c7cb2f404af331d5b4099a9b322a8a6b25c4632755c8757345baac5"}, + {file = "cryptography-45.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3af26738f2db354aafe492fb3869e955b12b2ef2e16908c8b9cb928128d42c57"}, + {file = "cryptography-45.0.5-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e6c00130ed423201c5bc5544c23359141660b07999ad82e34e7bb8f882bb78e0"}, + {file = "cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:dd420e577921c8c2d31289536c386aaa30140b473835e97f83bc71ea9d2baf2d"}, + {file = "cryptography-45.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d05a38884db2ba215218745f0781775806bde4f32e07b135348355fe8e4991d9"}, + {file = "cryptography-45.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ad0caded895a00261a5b4aa9af828baede54638754b51955a0ac75576b831b27"}, + {file = "cryptography-45.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9024beb59aca9d31d36fcdc1604dd9bbeed0a55bface9f1908df19178e2f116e"}, + {file = "cryptography-45.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91098f02ca81579c85f66df8a588c78f331ca19089763d733e34ad359f474174"}, + {file = "cryptography-45.0.5-cp311-abi3-win32.whl", hash = "sha256:926c3ea71a6043921050eaa639137e13dbe7b4ab25800932a8498364fc1abec9"}, + {file = "cryptography-45.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:b85980d1e345fe769cfc57c57db2b59cff5464ee0c045d52c0df087e926fbe63"}, + {file = "cryptography-45.0.5-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3562c2f23c612f2e4a6964a61d942f891d29ee320edb62ff48ffb99f3de9ae8"}, + {file = "cryptography-45.0.5-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3fcfbefc4a7f332dece7272a88e410f611e79458fab97b5efe14e54fe476f4fd"}, + {file = "cryptography-45.0.5-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:460f8c39ba66af7db0545a8c6f2eabcbc5a5528fc1cf6c3fa9a1e44cec33385e"}, + {file = "cryptography-45.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9b4cf6318915dccfe218e69bbec417fdd7c7185aa7aab139a2c0beb7468c89f0"}, + {file = "cryptography-45.0.5-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2089cc8f70a6e454601525e5bf2779e665d7865af002a5dec8d14e561002e135"}, + {file = "cryptography-45.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0027d566d65a38497bc37e0dd7c2f8ceda73597d2ac9ba93810204f56f52ebc7"}, + {file = "cryptography-45.0.5-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:be97d3a19c16a9be00edf79dca949c8fa7eff621763666a145f9f9535a5d7f42"}, + {file = "cryptography-45.0.5-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:7760c1c2e1a7084153a0f68fab76e754083b126a47d0117c9ed15e69e2103492"}, + {file = "cryptography-45.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ff8728d8d890b3dda5765276d1bc6fb099252915a2cd3aff960c4c195745dd0"}, + {file = "cryptography-45.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7259038202a47fdecee7e62e0fd0b0738b6daa335354396c6ddebdbe1206af2a"}, + {file = "cryptography-45.0.5-cp37-abi3-win32.whl", hash = "sha256:1e1da5accc0c750056c556a93c3e9cb828970206c68867712ca5805e46dc806f"}, + {file = "cryptography-45.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:90cb0a7bb35959f37e23303b7eed0a32280510030daba3f7fdfbb65defde6a97"}, + {file = "cryptography-45.0.5-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:206210d03c1193f4e1ff681d22885181d47efa1ab3018766a7b32a7b3d6e6afd"}, + {file = "cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c648025b6840fe62e57107e0a25f604db740e728bd67da4f6f060f03017d5097"}, + {file = "cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b8fa8b0a35a9982a3c60ec79905ba5bb090fc0b9addcfd3dc2dd04267e45f25e"}, + {file = "cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:14d96584701a887763384f3c47f0ca7c1cce322aa1c31172680eb596b890ec30"}, + {file = "cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57c816dfbd1659a367831baca4b775b2a5b43c003daf52e9d57e1d30bc2e1b0e"}, + {file = "cryptography-45.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b9e38e0a83cd51e07f5a48ff9691cae95a79bea28fe4ded168a8e5c6c77e819d"}, + {file = "cryptography-45.0.5-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8c4a6ff8a30e9e3d38ac0539e9a9e02540ab3f827a3394f8852432f6b0ea152e"}, + {file = "cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bd4c45986472694e5121084c6ebbd112aa919a25e783b87eb95953c9573906d6"}, + {file = "cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:982518cd64c54fcada9d7e5cf28eabd3ee76bd03ab18e08a48cad7e8b6f31b18"}, + {file = "cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:12e55281d993a793b0e883066f590c1ae1e802e3acb67f8b442e721e475e6463"}, + {file = "cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:5aa1e32983d4443e310f726ee4b071ab7569f58eedfdd65e9675484a4eb67bd1"}, + {file = "cryptography-45.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e357286c1b76403dd384d938f93c46b2b058ed4dfcdce64a770f0537ed3feb6f"}, + {file = "cryptography-45.0.5.tar.gz", hash = "sha256:72e76caa004ab63accdf26023fccd1d087f6d90ec6048ff33ad0445abf7f605a"}, +] + +[package.dependencies] +cffi = {version = ">=1.14", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test = ["certifi (>=2024)", "cryptography-vectors (==45.0.5)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] @@ -862,24 +951,24 @@ cython = ["cython"] [[package]] name = "decorator" -version = "5.1.1" +version = "5.2.1" description = "Decorators for Humans" optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" files = [ - {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, - {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, + {file = "decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"}, + {file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"}, ] [[package]] name = "distlib" -version = "0.3.8" +version = "0.4.0" 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"}, + {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, + {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, ] [[package]] @@ -971,13 +1060,13 @@ pgp = ["gpg"] [[package]] name = "eth-abi" -version = "5.1.0" +version = "5.2.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"}, + {file = "eth_abi-5.2.0-py3-none-any.whl", hash = "sha256:17abe47560ad753f18054f5b3089fcb588f3e3a092136a416b6c1502cb7e8877"}, + {file = "eth_abi-5.2.0.tar.gz", hash = "sha256:178703fa98c07d8eecd5ae569e7e8d159e493ebb6eeb534a8fe973fbc4e40ef0"}, ] [package.dependencies] @@ -986,27 +1075,27 @@ 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)"] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "eth-hash[pycryptodome]", "hypothesis (>=6.22.0,<6.108.7)", "ipython", "mypy (==1.10.0)", "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-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["eth-hash[pycryptodome]", "hypothesis (>=6.22.0,<6.108.7)", "pytest (>=7.0.0)", "pytest-pythonpath (>=0.7.1)", "pytest-timeout (>=2.0.0)", "pytest-xdist (>=2.4.0)"] +tools = ["hypothesis (>=6.22.0,<6.108.7)"] [[package]] name = "eth-account" -version = "0.13.0" +version = "0.13.7" 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"}, + {file = "eth_account-0.13.7-py3-none-any.whl", hash = "sha256:39727de8c94d004ff61d10da7587509c04d2dc7eac71e04830135300bdfc6d24"}, + {file = "eth_account-0.13.7.tar.gz", hash = "sha256:5853ecbcbb22e65411176f121f5f24b8afeeaf13492359d254b16d8b18c77a46"}, ] [package.dependencies] bitarray = ">=2.4.0" -ckzg = ">=0.4.3" +ckzg = ">=2.0.0" eth-abi = ">=4.0.0-b.2" -eth-keyfile = ">=0.6.0" +eth-keyfile = ">=0.7.0,<0.9.0" eth-keys = ">=0.4.0" eth-rlp = ">=2.1.0" eth-utils = ">=2.0.0" @@ -1015,46 +1104,46 @@ 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)"] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "coverage", "hypothesis (>=6.22.0,<6.108.7)", "ipython", "mypy (==1.10.0)", "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 (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["coverage", "hypothesis (>=6.22.0,<6.108.7)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] [[package]] name = "eth-bloom" -version = "3.0.1" +version = "3.1.0" 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"}, + {file = "eth_bloom-3.1.0-py3-none-any.whl", hash = "sha256:c96b2dd6cafa407373bca1a9d74b650378ba672d5b17f2771bf7d3c3aaa7651c"}, + {file = "eth_bloom-3.1.0.tar.gz", hash = "sha256:4bc918f6fde44334e92b23cfb345db961e2e3af620535cbc872444f7a143cb88"}, ] [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)"] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "hypothesis (>=3.31.2)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["towncrier (>=24,<25)"] test = ["hypothesis (>=3.31.2)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] [[package]] name = "eth-hash" -version = "0.7.0" +version = "0.7.1" description = "eth-hash: The Ethereum hashing function, keccak256, sometimes (erroneously) called sha3" optional = false -python-versions = ">=3.8, <4" +python-versions = "<4,>=3.8" 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"}, + {file = "eth_hash-0.7.1-py3-none-any.whl", hash = "sha256:0fb1add2adf99ef28883fd6228eb447ef519ea72933535ad1a0b28c6f65f868a"}, + {file = "eth_hash-0.7.1.tar.gz", hash = "sha256:d2411a403a0b0a62e8247b4117932d900ffb4c8c64b15f92620547ca5ce46be5"}, ] [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)"] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "ipython", "mypy (==1.10.0)", "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 (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] 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)"] @@ -1082,13 +1171,13 @@ test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] [[package]] name = "eth-keys" -version = "0.5.1" +version = "0.7.0" 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"}, + {file = "eth_keys-0.7.0-py3-none-any.whl", hash = "sha256:b0cdda8ffe8e5ba69c7c5ca33f153828edcace844f67aabd4542d7de38b159cf"}, + {file = "eth_keys-0.7.0.tar.gz", hash = "sha256:79d24fd876201df67741de3e3fefb3f4dbcbb6ace66e47e6fe662851a4547814"}, ] [package.dependencies] @@ -1096,42 +1185,42 @@ 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)"] +coincurve = ["coincurve (>=17.0.0)"] +dev = ["asn1tools (>=0.146.2)", "build (>=0.9.0)", "bump_my_version (>=0.19.0)", "coincurve (>=17.0.0)", "eth-hash[pysha3]", "factory-boy (>=3.0.1)", "hypothesis (>=5.10.3)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pyasn1 (>=0.4.5)", "pytest (>=7.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["towncrier (>=24,<25)"] 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" +version = "2.2.0" description = "eth-rlp: RLP definitions for common Ethereum objects in Python" optional = false -python-versions = ">=3.8, <4" +python-versions = "<4,>=3.8" 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"}, + {file = "eth_rlp-2.2.0-py3-none-any.whl", hash = "sha256:5692d595a741fbaef1203db6a2fedffbd2506d31455a6ad378c8449ee5985c47"}, + {file = "eth_rlp-2.2.0.tar.gz", hash = "sha256:5e4b2eb1b8213e303d6a232dfe35ab8c29e2d3051b86e8d359def80cd21db83d"}, ] [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\""} +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)"] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "eth-hash[pycryptodome]", "ipython", "mypy (==1.10.0)", "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 (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] test = ["eth-hash[pycryptodome]", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] [[package]] name = "eth-stdlib" -version = "0.2.7" +version = "0.2.8" description = "Ethereum Standard Library for Python" optional = false -python-versions = ">=3.8,<4.0" +python-versions = "<4.0,>=3.8" 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"}, + {file = "eth_stdlib-0.2.8-py3-none-any.whl", hash = "sha256:77979707e01899c4626b78d20f05a1da6ba1dc3224c0a173259618b7970326e9"}, + {file = "eth_stdlib-0.2.8.tar.gz", hash = "sha256:cda115a841142d4646e3f05818cbb08e2ca3daafc740646fa5888f9b8db08f97"}, ] [package.dependencies] @@ -1142,53 +1231,60 @@ hypothesis = ["hypothesis (>=6.58.0,<7.0.0)"] [[package]] name = "eth-typing" -version = "4.2.3" +version = "5.2.1" 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"}, + {file = "eth_typing-5.2.1-py3-none-any.whl", hash = "sha256:b0c2812ff978267563b80e9d701f487dd926f1d376d674f3b535cfe28b665d3d"}, + {file = "eth_typing-5.2.1.tar.gz", hash = "sha256:7557300dbf02a93c70fa44af352b5c4a58f94e997a0fd6797fb7d1c29d9538ee"}, ] +[package.dependencies] +typing_extensions = ">=4.5.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-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)"] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "ipython", "mypy (==1.10.0)", "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 (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] [[package]] name = "eth-utils" -version = "4.1.1" +version = "5.3.0" 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"}, + {file = "eth_utils-5.3.0-py3-none-any.whl", hash = "sha256:ac184883ab299d923428bbe25dae5e356979a3993e0ef695a864db0a20bc262d"}, + {file = "eth_utils-5.3.0.tar.gz", hash = "sha256:1f096867ac6be895f456fa3acb26e9573ae66e753abad9208f316d24d6178156"}, ] [package.dependencies] cytoolz = {version = ">=0.10.1", markers = "implementation_name == \"cpython\""} eth-hash = ">=0.3.1" -eth-typing = ">=3.0.0" +eth-typing = ">=5.0.0" +pydantic = ">=2.0.0,<3" 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)"] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "eth-hash[pycryptodome]", "hypothesis (>=4.43.0)", "ipython", "mypy (==1.10.0)", "mypy (==1.10.0)", "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 (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["hypothesis (>=4.43.0)", "mypy (==1.10.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] [[package]] name = "exceptiongroup" -version = "1.2.1" +version = "1.3.0" 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"}, + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, ] +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + [package.extras] test = ["pytest (>=6)"] @@ -1208,13 +1304,13 @@ testing = ["hatch", "pre-commit", "pytest", "tox"] [[package]] name = "executing" -version = "2.0.1" +version = "2.2.0" description = "Get the currently executing AST node of a frame, and other information" optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" files = [ - {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, - {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, + {file = "executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa"}, + {file = "executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755"}, ] [package.extras] @@ -1222,28 +1318,32 @@ tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipyth [[package]] name = "fancycompleter" -version = "0.9.1" +version = "0.11.1" description = "colorful TAB completion for Python prompt" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "fancycompleter-0.9.1-py3-none-any.whl", hash = "sha256:dd076bca7d9d524cc7f25ec8f35ef95388ffef9ef46def4d3d25e9b044ad7080"}, - {file = "fancycompleter-0.9.1.tar.gz", hash = "sha256:09e0feb8ae242abdfd7ef2ba55069a46f011814a80fe5476be48f51b00247272"}, + {file = "fancycompleter-0.11.1-py3-none-any.whl", hash = "sha256:44243d7fab37087208ca5acacf8f74c0aa4d733d04d593857873af7513cdf8a6"}, + {file = "fancycompleter-0.11.1.tar.gz", hash = "sha256:5b4ad65d76b32b1259251516d0f1cb2d82832b1ff8506697a707284780757f69"}, ] [package.dependencies] -pyreadline = {version = "*", markers = "platform_system == \"Windows\""} -pyrepl = ">=0.8.2" +pyreadline3 = {version = "*", markers = "platform_system == \"Windows\" and python_version < \"3.13\""} +pyrepl = {version = ">=0.11.3", markers = "python_version < \"3.13\""} + +[package.extras] +dev = ["fancycompleter[tests]", "mypy", "ruff (==0.11.8)"] +tests = ["pytest", "pytest-cov"] [[package]] name = "fastjsonschema" -version = "2.19.1" +version = "2.21.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"}, + {file = "fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667"}, + {file = "fastjsonschema-2.21.1.tar.gz", hash = "sha256:794d4f0a58f848961ba16af7b9c85a3e88cd360df008c59aac6fc5ae9323b5d4"}, ] [package.extras] @@ -1251,19 +1351,19 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc [[package]] name = "filelock" -version = "3.14.0" +version = "3.18.0" description = "A platform independent file lock." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"}, - {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"}, + {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, + {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, ] [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)"] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] +typing = ["typing-extensions (>=4.12.2)"] [[package]] name = "ghp-import" @@ -1284,29 +1384,29 @@ dev = ["flake8", "markdown", "twine", "wheel"] [[package]] name = "hexbytes" -version = "1.2.0" +version = "1.3.1" description = "hexbytes: Python `bytes` subclass that decodes hex, with a readable console output" optional = false -python-versions = ">=3.8, <4" +python-versions = "<4,>=3.8" files = [ - {file = "hexbytes-1.2.0-py3-none-any.whl", hash = "sha256:bb243ab58b8d8390e3a753fbc9e3616f0f958df43d874e19ae0e4b746722a7e9"}, - {file = "hexbytes-1.2.0.tar.gz", hash = "sha256:965f1cc712e7b263c41fdf3fb36cf671ba6f59b895937cf33941a5c996ec3a5c"}, + {file = "hexbytes-1.3.1-py3-none-any.whl", hash = "sha256:da01ff24a1a9a2b1881c4b85f0e9f9b0f51b526b379ffa23832ae7899d29c2c7"}, + {file = "hexbytes-1.3.1.tar.gz", hash = "sha256:a657eebebdfe27254336f98d8af6e2236f3f83aed164b87466b6cf6c5f5a4765"}, ] [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)"] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "eth_utils (>=2.0.0)", "hypothesis (>=3.44.24)", "ipython", "mypy (==1.10.0)", "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 (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["eth_utils (>=2.0.0)", "hypothesis (>=3.44.24)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] [[package]] name = "hypothesis" -version = "6.103.1" +version = "6.136.6" description = "A library for property-based testing" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "hypothesis-6.103.1-py3-none-any.whl", hash = "sha256:d3c959fab6233e78867499e2117ae9db8dc40eeed936d71a2cfc7b6094972e74"}, - {file = "hypothesis-6.103.1.tar.gz", hash = "sha256:d299d5c21d6408eab3be670c94c974f3acf0b511c61fe81804b09091e393ee1f"}, + {file = "hypothesis-6.136.6-py3-none-any.whl", hash = "sha256:1d6296dde36d42263bd44a084c74e91467e78186676e410167f920aa0374a9e7"}, + {file = "hypothesis-6.136.6.tar.gz", hash = "sha256:2ad2e4f2012be4d41c6515b0628d84d48af6e6c38b4db50840bd9ac0899f5856"}, ] [package.dependencies] @@ -1315,61 +1415,130 @@ 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)"] +all = ["black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.93)", "django (>=4.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.24)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.19.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2025.2)", "watchdog (>=4.0.0)"] 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)"] +crosshair = ["crosshair-tool (>=0.0.93)", "hypothesis-crosshair (>=0.0.24)"] dateutil = ["python-dateutil (>=1.4)"] -django = ["django (>=3.2)"] +django = ["django (>=4.2)"] dpcontracts = ["dpcontracts (>=0.4)"] ghostwriter = ["black (>=19.10b0)"] lark = ["lark (>=0.10.1)"] -numpy = ["numpy (>=1.17.3)"] +numpy = ["numpy (>=1.19.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)"] +watchdog = ["watchdog (>=4.0.0)"] +zoneinfo = ["tzdata (>=2025.2)"] [[package]] name = "idna" -version = "3.7" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, -] + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "immutables" +version = "0.21" +description = "Immutable Collections" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "immutables-0.21-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:14cb09d4f4577ad9ab8770a340dc2158e0a5ab5775cb34c75960167a31104212"}, + {file = "immutables-0.21-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:22ba593f95044ac60d2af463f3dc86cd0e223f8c51df85dff65d663d93e19f51"}, + {file = "immutables-0.21-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25afc81a7bcf26c8364f85e52a14e0095344343e79493c73b0e9a765310a0bed"}, + {file = "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"}, + {file = "immutables-0.21-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ba8bca21a1d034f4577ede1e9553a681dd01199c06b563f1a8316f2623b64985"}, + {file = "immutables-0.21-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:39337bfb42f83dd787a81e2d00e90efa17c4a39a9cf1210b8a50dafe32438aae"}, + {file = "immutables-0.21-cp310-cp310-win32.whl", hash = "sha256:b24aa98f6cdae4ba15baf3aa00e84223bafcd0d3fd7f0443474527ec951845e1"}, + {file = "immutables-0.21-cp310-cp310-win_amd64.whl", hash = "sha256:715f8e5f8e1c35f036f9ac62eaf8b672eec1cdc2b4f9b73864cc64eccc76661c"}, + {file = "immutables-0.21-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5d780c38067047911a2e06a86ba063ba0055618ab5573c8198ef3f368e321303"}, + {file = "immutables-0.21-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9aab9d0f0016f6e0bfe7e4a4cb831ef20063da6468b1bbc71d06ef285781ee9e"}, + {file = "immutables-0.21-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ff83390b05d3372acb9a0c928f6cc20c78e74ca20ed88eb941f84a63b65e444"}, + {file = "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"}, + {file = "immutables-0.21-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc7844c9fbb5bece5bfdf2bf8ea74d308f42f40b0665fd25c58abf56d7db024a"}, + {file = "immutables-0.21-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:984106fa4345efd9f96de22e9949fc97bac8598bdebee03c20b2497a88bff3b7"}, + {file = "immutables-0.21-cp311-cp311-win32.whl", hash = "sha256:1bdb5200518518601377e4877d5034e7c535e9ea8a9d601ed8b0eedef0c7becd"}, + {file = "immutables-0.21-cp311-cp311-win_amd64.whl", hash = "sha256:dd00c34f431c54c95e7b84bfdbdeacb4f039a6a24eb0c1f7aa4b168bb9a6ad0a"}, + {file = "immutables-0.21-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef1ed262094b755903122c3c3a83ad0e0d5c3ab7887cda12b2fe878769d1ee0d"}, + {file = "immutables-0.21-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce604f81d9d8f26e60b52ebcb56bb5c0462c8ea50fb17868487d15f048a2f13e"}, + {file = "immutables-0.21-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b48b116aaca4500398058b5a87814857a60c4cb09417fecc12d7da0f5639b73d"}, + {file = "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"}, + {file = "immutables-0.21-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e44346e2221a5a676c880ca8e0e6429fa24d1a4ae562573f5c04d7f2e759b030"}, + {file = "immutables-0.21-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8b10139b529a460e53fe8be699ebd848c54c8a33ebe67763bcfcc809a475a26f"}, + {file = "immutables-0.21-cp312-cp312-win32.whl", hash = "sha256:fc512d808662614feb17d2d92e98f611d69669a98c7af15910acf1dc72737038"}, + {file = "immutables-0.21-cp312-cp312-win_amd64.whl", hash = "sha256:461dcb0f58a131045155e52a2c43de6ec2fe5ba19bdced6858a3abb63cee5111"}, + {file = "immutables-0.21-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:79674b51aa8dd983f9ac55f7f67b433b1df84a6b4f28ab860588389a5659485b"}, + {file = "immutables-0.21-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93c8350f8f7d0d9693f708229d9d0578e6f3b785ce6da4bced1da97137aacfad"}, + {file = "immutables-0.21-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:583d2a63e444ce1538cc2bda56ae1f4a1a11473dbc0377c82b516bc7eec3b81e"}, + {file = "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"}, + {file = "immutables-0.21-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:338bede057250b33716a3e4892e15df0bf5a5ddbf1d67ead996b3e680b49ef9e"}, + {file = "immutables-0.21-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8781c89583b68f604cf30f0978b722165824c3075888639fde771bf1a3e12dc0"}, + {file = "immutables-0.21-cp313-cp313-win32.whl", hash = "sha256:e97ea83befad873712f283c0cccd630f70cba753e207b4868af28d5b85e9dc54"}, + {file = "immutables-0.21-cp313-cp313-win_amd64.whl", hash = "sha256:cfcb23bd898f5a4ef88692b42c51f52ca7373a35ba4dcc215060a668639eb5da"}, + {file = "immutables-0.21-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07a37d8699255402a10784d4d45f2bcc00ca7dba8da763207a834b15767e6c62"}, + {file = "immutables-0.21-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9139fd80bb05501216f49c4306bb80d0c1a08c3f0f621ed2939bf52d7f762661"}, + {file = "immutables-0.21-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc6fc7e917e281361ad243be1a3cb56a7633de88ee67c94cdf5651958ead30d9"}, + {file = "immutables-0.21-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6a577f55eaaf763b685eef9710edbeb7ee95e2e5f54e7e5e0fd0f60ae2eb648"}, + {file = "immutables-0.21-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ca912c1bb35615ccbe361a6bb76e6fd43827394102467967d5599d78b50dd0f4"}, + {file = "immutables-0.21-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:586e88ca7ed44b7bb2cd7b212abd2637b51bd95bdb4856ab111b44715a62071c"}, + {file = "immutables-0.21-cp38-cp38-win32.whl", hash = "sha256:21adc6b478a58692c79c5bf316b39d3fd0552441d2b38eef1782a7555deee484"}, + {file = "immutables-0.21-cp38-cp38-win_amd64.whl", hash = "sha256:ecff5274357dc18aae053e5e10b8eee5e9b4d6cc774d34148c992cb2eb787ec3"}, + {file = "immutables-0.21-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e2aadf3bdd90daa0e8cb9c3cde4070e1021036e3b57f571a007ce24f323e47a9"}, + {file = "immutables-0.21-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5f8f507731d4d15e0c579aa77d8482471f988dc0f451e4bf3853ec36ccd42627"}, + {file = "immutables-0.21-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb9a378a4480381d7d3d63b0d201cf610eae0bf70e26a9306e3e631c9bd64010"}, + {file = "immutables-0.21-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7b5920bbfcaf038894c8ce4ed2eff0b31c3559810a61806db751be8ab4d703"}, + {file = "immutables-0.21-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8b90702d1fe313e8273ae7abb46fc0f0a87b47c1c9a83aed9a161301146e655c"}, + {file = "immutables-0.21-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:71cbbc6fbe7e7321648047ff9273f4605f8bd5ce456841a65ef151080e9d3481"}, + {file = "immutables-0.21-cp39-cp39-win32.whl", hash = "sha256:c44f286c47dc0d4d7b5bf19fbe975e6d57c56d2878cea413e1ec7a4bfffb2727"}, + {file = "immutables-0.21-cp39-cp39-win_amd64.whl", hash = "sha256:cf15314c39484b8947a4e20c3526021272510592fb2807b5136a2fcd6ab0151b"}, + {file = "immutables-0.21.tar.gz", hash = "sha256:b55ffaf0449790242feb4c56ab799ea7af92801a0a43f9e2f4f8af2ab24dfc4a"}, +] + +[package.extras] +test = ["flake8 (>=5.0,<6.0)", "mypy (>=1.4,<2.0)", "pycodestyle (>=2.9,<3.0)", "pytest (>=7.4,<8.0)"] [[package]] name = "importlib-metadata" -version = "7.1.0" +version = "8.7.0" description = "Read metadata from Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" 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"}, + {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, + {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, ] [package.dependencies] -zipp = ">=0.5" +zipp = ">=3.20" [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] 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)"] +test = ["flufl.flake8", "importlib_resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] [[package]] name = "iniconfig" -version = "2.0.0" +version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] [[package]] @@ -1385,13 +1554,13 @@ files = [ [[package]] name = "ipython" -version = "8.25.0" +version = "8.37.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"}, + {file = "ipython-8.37.0-py3-none-any.whl", hash = "sha256:ed87326596b878932dbcb171e3e698845434d8c61b8d8cd474bf663041a9dcf2"}, + {file = "ipython-8.37.0.tar.gz", hash = "sha256:ca815841e1a41a1e6b73a0b08f3038af9b2252564d01fc405356d34033012216"}, ] [package.dependencies] @@ -1401,16 +1570,16 @@ 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" +prompt_toolkit = ">=3.0.41,<3.1.0" pygments = ">=2.4.0" -stack-data = "*" +stack_data = "*" traitlets = ">=5.13.0" -typing-extensions = {version = ">=4.6", markers = "python_version < \"3.12\""} +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"] +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"] @@ -1418,8 +1587,8 @@ 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"] +test = ["packaging", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "ipython[test]", "jupyter_ai", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"] [[package]] name = "jaraco-classes" @@ -1441,37 +1610,37 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-ena [[package]] name = "jedi" -version = "0.19.1" +version = "0.19.2" 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"}, + {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"}, + {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"}, ] [package.dependencies] -parso = ">=0.8.3,<0.9.0" +parso = ">=0.8.4,<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)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"] [[package]] name = "jeepney" -version = "0.8.0" +version = "0.9.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"}, + {file = "jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683"}, + {file = "jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732"}, ] [package.extras] test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] -trio = ["async_generator", "trio"] +trio = ["trio"] [[package]] name = "jinja2" @@ -1625,13 +1794,13 @@ test = ["pytest"] [[package]] name = "markdown" -version = "3.8" +version = "3.8.2" 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"}, + {file = "markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24"}, + {file = "markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45"}, ] [package.extras] @@ -1856,89 +2025,92 @@ files = [ [[package]] name = "more-itertools" -version = "10.2.0" +version = "10.7.0" description = "More routines for operating on iterables, beyond itertools" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" 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"}, + {file = "more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e"}, + {file = "more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3"}, ] [[package]] name = "msgpack" -version = "1.0.8" +version = "1.1.1" 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"}, + {file = "msgpack-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:353b6fc0c36fde68b661a12949d7d49f8f51ff5fa019c1e47c87c4ff34b080ed"}, + {file = "msgpack-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:79c408fcf76a958491b4e3b103d1c417044544b68e96d06432a189b43d1215c8"}, + {file = "msgpack-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78426096939c2c7482bf31ef15ca219a9e24460289c00dd0b94411040bb73ad2"}, + {file = "msgpack-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b17ba27727a36cb73aabacaa44b13090feb88a01d012c0f4be70c00f75048b4"}, + {file = "msgpack-1.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a17ac1ea6ec3c7687d70201cfda3b1e8061466f28f686c24f627cae4ea8efd0"}, + {file = "msgpack-1.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:88d1e966c9235c1d4e2afac21ca83933ba59537e2e2727a999bf3f515ca2af26"}, + {file = "msgpack-1.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f6d58656842e1b2ddbe07f43f56b10a60f2ba5826164910968f5933e5178af75"}, + {file = "msgpack-1.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:96decdfc4adcbc087f5ea7ebdcfd3dee9a13358cae6e81d54be962efc38f6338"}, + {file = "msgpack-1.1.1-cp310-cp310-win32.whl", hash = "sha256:6640fd979ca9a212e4bcdf6eb74051ade2c690b862b679bfcb60ae46e6dc4bfd"}, + {file = "msgpack-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:8b65b53204fe1bd037c40c4148d00ef918eb2108d24c9aaa20bc31f9810ce0a8"}, + {file = "msgpack-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:71ef05c1726884e44f8b1d1773604ab5d4d17729d8491403a705e649116c9558"}, + {file = "msgpack-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:36043272c6aede309d29d56851f8841ba907a1a3d04435e43e8a19928e243c1d"}, + {file = "msgpack-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a32747b1b39c3ac27d0670122b57e6e57f28eefb725e0b625618d1b59bf9d1e0"}, + {file = "msgpack-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a8b10fdb84a43e50d38057b06901ec9da52baac6983d3f709d8507f3889d43f"}, + {file = "msgpack-1.1.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba0c325c3f485dc54ec298d8b024e134acf07c10d494ffa24373bea729acf704"}, + {file = "msgpack-1.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:88daaf7d146e48ec71212ce21109b66e06a98e5e44dca47d853cbfe171d6c8d2"}, + {file = "msgpack-1.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8b55ea20dc59b181d3f47103f113e6f28a5e1c89fd5b67b9140edb442ab67f2"}, + {file = "msgpack-1.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a28e8072ae9779f20427af07f53bbb8b4aa81151054e882aee333b158da8752"}, + {file = "msgpack-1.1.1-cp311-cp311-win32.whl", hash = "sha256:7da8831f9a0fdb526621ba09a281fadc58ea12701bc709e7b8cbc362feabc295"}, + {file = "msgpack-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fd1b58e1431008a57247d6e7cc4faa41c3607e8e7d4aaf81f7c29ea013cb458"}, + {file = "msgpack-1.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae497b11f4c21558d95de9f64fff7053544f4d1a17731c866143ed6bb4591238"}, + {file = "msgpack-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:33be9ab121df9b6b461ff91baac6f2731f83d9b27ed948c5b9d1978ae28bf157"}, + {file = "msgpack-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f64ae8fe7ffba251fecb8408540c34ee9df1c26674c50c4544d72dbf792e5ce"}, + {file = "msgpack-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a494554874691720ba5891c9b0b39474ba43ffb1aaf32a5dac874effb1619e1a"}, + {file = "msgpack-1.1.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb643284ab0ed26f6957d969fe0dd8bb17beb567beb8998140b5e38a90974f6c"}, + {file = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d275a9e3c81b1093c060c3837e580c37f47c51eca031f7b5fb76f7b8470f5f9b"}, + {file = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fd6b577e4541676e0cc9ddc1709d25014d3ad9a66caa19962c4f5de30fc09ef"}, + {file = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb29aaa613c0a1c40d1af111abf025f1732cab333f96f285d6a93b934738a68a"}, + {file = "msgpack-1.1.1-cp312-cp312-win32.whl", hash = "sha256:870b9a626280c86cff9c576ec0d9cbcc54a1e5ebda9cd26dab12baf41fee218c"}, + {file = "msgpack-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:5692095123007180dca3e788bb4c399cc26626da51629a31d40207cb262e67f4"}, + {file = "msgpack-1.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3765afa6bd4832fc11c3749be4ba4b69a0e8d7b728f78e68120a157a4c5d41f0"}, + {file = "msgpack-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8ddb2bcfd1a8b9e431c8d6f4f7db0773084e107730ecf3472f1dfe9ad583f3d9"}, + {file = "msgpack-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:196a736f0526a03653d829d7d4c5500a97eea3648aebfd4b6743875f28aa2af8"}, + {file = "msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d592d06e3cc2f537ceeeb23d38799c6ad83255289bb84c2e5792e5a8dea268a"}, + {file = "msgpack-1.1.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4df2311b0ce24f06ba253fda361f938dfecd7b961576f9be3f3fbd60e87130ac"}, + {file = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4141c5a32b5e37905b5940aacbc59739f036930367d7acce7a64e4dec1f5e0b"}, + {file = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b1ce7f41670c5a69e1389420436f41385b1aa2504c3b0c30620764b15dded2e7"}, + {file = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4147151acabb9caed4e474c3344181e91ff7a388b888f1e19ea04f7e73dc7ad5"}, + {file = "msgpack-1.1.1-cp313-cp313-win32.whl", hash = "sha256:500e85823a27d6d9bba1d057c871b4210c1dd6fb01fbb764e37e4e8847376323"}, + {file = "msgpack-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:6d489fba546295983abd142812bda76b57e33d0b9f5d5b71c09a583285506f69"}, + {file = "msgpack-1.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bba1be28247e68994355e028dcd668316db30c1f758d3241a7b903ac78dcd285"}, + {file = "msgpack-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8f93dcddb243159c9e4109c9750ba5b335ab8d48d9522c5308cd05d7e3ce600"}, + {file = "msgpack-1.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fbbc0b906a24038c9958a1ba7ae0918ad35b06cb449d398b76a7d08470b0ed9"}, + {file = "msgpack-1.1.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:61e35a55a546a1690d9d09effaa436c25ae6130573b6ee9829c37ef0f18d5e78"}, + {file = "msgpack-1.1.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:1abfc6e949b352dadf4bce0eb78023212ec5ac42f6abfd469ce91d783c149c2a"}, + {file = "msgpack-1.1.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:996f2609ddf0142daba4cefd767d6db26958aac8439ee41db9cc0db9f4c4c3a6"}, + {file = "msgpack-1.1.1-cp38-cp38-win32.whl", hash = "sha256:4d3237b224b930d58e9d83c81c0dba7aacc20fcc2f89c1e5423aa0529a4cd142"}, + {file = "msgpack-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:da8f41e602574ece93dbbda1fab24650d6bf2a24089f9e9dbb4f5730ec1e58ad"}, + {file = "msgpack-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5be6b6bc52fad84d010cb45433720327ce886009d862f46b26d4d154001994b"}, + {file = "msgpack-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3a89cd8c087ea67e64844287ea52888239cbd2940884eafd2dcd25754fb72232"}, + {file = "msgpack-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d75f3807a9900a7d575d8d6674a3a47e9f227e8716256f35bc6f03fc597ffbf"}, + {file = "msgpack-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d182dac0221eb8faef2e6f44701812b467c02674a322c739355c39e94730cdbf"}, + {file = "msgpack-1.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b13fe0fb4aac1aa5320cd693b297fe6fdef0e7bea5518cbc2dd5299f873ae90"}, + {file = "msgpack-1.1.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:435807eeb1bc791ceb3247d13c79868deb22184e1fc4224808750f0d7d1affc1"}, + {file = "msgpack-1.1.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4835d17af722609a45e16037bb1d4d78b7bdf19d6c0128116d178956618c4e88"}, + {file = "msgpack-1.1.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a8ef6e342c137888ebbfb233e02b8fbd689bb5b5fcc59b34711ac47ebd504478"}, + {file = "msgpack-1.1.1-cp39-cp39-win32.whl", hash = "sha256:61abccf9de335d9efd149e2fff97ed5974f2481b3353772e8e2dd3402ba2bd57"}, + {file = "msgpack-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:40eae974c873b2992fd36424a5d9407f93e97656d999f43fca9d29f820899084"}, + {file = "msgpack-1.1.1.tar.gz", hash = "sha256:77b79ce34a2bdab2594f490c8e80dd62a02d650b91a75159a63ec413b8d104cd"}, ] [[package]] name = "packaging" -version = "23.2" +version = "24.2" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] @@ -2032,13 +2204,13 @@ ptyprocess = ">=0.5" [[package]] name = "pkginfo" -version = "1.11.0" +version = "1.12.1.2" 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"}, + {file = "pkginfo-1.12.1.2-py3-none-any.whl", hash = "sha256:c783ac885519cab2c34927ccfa6bf64b5a704d7c69afaea583dd9b7afe969343"}, + {file = "pkginfo-1.12.1.2.tar.gz", hash = "sha256:5cd957824ac36f140260964eba3c6be6442a8359b8c48f4adf90210f33a04b7b"}, ] [package.extras] @@ -2046,44 +2218,44 @@ testing = ["pytest", "pytest-cov", "wheel"] [[package]] name = "platformdirs" -version = "4.2.2" +version = "4.3.8" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, + {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, + {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, ] [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)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] [[package]] name = "pluggy" -version = "1.5.0" +version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, ] [package.extras] dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] +testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "poetry" -version = "1.8.3" +version = "1.8.5" 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"}, + {file = "poetry-1.8.5-py3-none-any.whl", hash = "sha256:5505fba69bf2a792b5d7402d21839c853644337392b745109b86a23010cce5f3"}, + {file = "poetry-1.8.5.tar.gz", hash = "sha256:eb2c88d224f58f36df8f7b36d6c380c07d1001bca28bde620f68fc086e881b70"}, ] [package.dependencies] @@ -2097,9 +2269,9 @@ 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" +pkginfo = ">=1.12,<2.0" platformdirs = ">=3.0.0,<5" -poetry-core = "1.9.0" +poetry-core = "1.9.1" poetry-plugin-export = ">=1.6.0,<2.0.0" pyproject-hooks = ">=1.0.0,<2.0.0" requests = ">=2.26,<3.0" @@ -2108,18 +2280,18 @@ 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" +virtualenv = ">=20.26.6,<21.0.0" xattr = {version = ">=1.0.0,<2.0.0", markers = "sys_platform == \"darwin\""} [[package]] name = "poetry-core" -version = "1.9.0" +version = "1.9.1" description = "Poetry PEP 517 Build Backend" optional = false -python-versions = ">=3.8,<4.0" +python-versions = "<4.0,>=3.8" 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"}, + {file = "poetry_core-1.9.1-py3-none-any.whl", hash = "sha256:6f45dd3598e0de8d9b0367360253d4c5d4d0110c8f5c71120a14f0e0f116c1a0"}, + {file = "poetry_core-1.9.1.tar.gz", hash = "sha256:7a2d49214bf58b4f17f99d6891d947a9836c9899a67a5069f52d7b67217f61b8"}, ] [[package]] @@ -2139,13 +2311,13 @@ poetry-core = ">=1.7.0,<3.0.0" [[package]] name = "prompt-toolkit" -version = "3.0.46" +version = "3.0.51" description = "Library for building powerful interactive command lines in Python" optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.8" 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"}, + {file = "prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07"}, + {file = "prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed"}, ] [package.dependencies] @@ -2164,13 +2336,13 @@ files = [ [[package]] name = "pure-eval" -version = "0.2.2" +version = "0.2.3" 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"}, + {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, + {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, ] [package.extras] @@ -2189,53 +2361,52 @@ files = [ [[package]] name = "py-ecc" -version = "7.0.1" +version = "8.0.0" 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"}, + {file = "py_ecc-8.0.0-py3-none-any.whl", hash = "sha256:c0b2dfc4bde67a55122a392591a10e851a986d5128f680628c80b405f7663e13"}, + {file = "py_ecc-8.0.0.tar.gz", hash = "sha256:56aca19e5dc37294f60c1cc76666c03c2276e7666412b9a559fa0145d099933d"}, ] [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)"] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "ipython", "mypy (==1.10.0)", "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 (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] [[package]] name = "py-evm" -version = "0.10.1b1" +version = "0.12.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"}, + {file = "py_evm-0.12.1b1-py3-none-any.whl", hash = "sha256:015ebc8dd95925030be87ce4b3fd31e3c70df626c5ad8665fb06cd611c73eb68"}, + {file = "py_evm-0.12.1b1.tar.gz", hash = "sha256:7bcd9935a3ac2989c8f068b2006f136189281ebc6e279663405cb2c5397ed890"}, ] [package.dependencies] cached-property = ">=1.5.1" -ckzg = ">=0.4.3" +ckzg = ">=2.0.0" eth-bloom = ">=1.0.3" eth-keys = ">=0.4.0" -eth-typing = ">=3.3.0" +eth-typing = ">=5.2.0" eth-utils = ">=2.0.0" lru-dict = ">=1.1.6" -py-ecc = ">=1.4.7" +py-ecc = ">=8.0.0" 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)"] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "cached-property (>=1.5.1)", "ckzg (>=2.0.0)", "eth-bloom (>=1.0.3)", "eth-keys (>=0.4.0)", "eth-typing (>=5.2.0)", "eth-utils (>=2.0.0)", "factory-boy (>=3.0.0)", "hypothesis (>=6,<7)", "ipython", "lru-dict (>=1.1.6)", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "py-ecc (>=8.0.0)", "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-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "sphinxcontrib-asyncio (>=0.2.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "trie (>=2.0.0)", "twine", "wheel"] +docs = ["py-evm (>=0.8.0b1)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "sphinxcontrib-asyncio (>=0.2.0)", "towncrier (>=24,<25)"] +eth = ["cached-property (>=1.5.1)", "ckzg (>=2.0.0)", "eth-bloom (>=1.0.3)", "eth-keys (>=0.4.0)", "eth-typing (>=5.2.0)", "eth-utils (>=2.0.0)", "lru-dict (>=1.1.6)", "py-ecc (>=8.0.0)", "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)"] @@ -2252,150 +2423,181 @@ files = [ [[package]] name = "pycryptodome" -version = "3.20.0" +version = "3.23.0" description = "Cryptographic library for Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 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"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a176b79c49af27d7f6c12e4b178b0824626f40a7b9fed08f712291b6d54bf566"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:573a0b3017e06f2cffd27d92ef22e46aa3be87a2d317a5abf7cc0e84e321bd75"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:63dad881b99ca653302b2c7191998dd677226222a3f2ea79999aa51ce695f720"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-win32.whl", hash = "sha256:b34e8e11d97889df57166eda1e1ddd7676da5fcd4d71a0062a760e75060514b4"}, + {file = "pycryptodome-3.23.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:7ac1080a8da569bde76c0a104589c4f414b8ba296c0b3738cf39a466a9fb1818"}, + {file = "pycryptodome-3.23.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6fe8258e2039eceb74dfec66b3672552b6b7d2c235b2dfecc05d16b8921649a8"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39"}, + {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27"}, + {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c"}, + {file = "pycryptodome-3.23.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:350ebc1eba1da729b35ab7627a833a1a355ee4e852d8ba0447fafe7b14504d56"}, + {file = "pycryptodome-3.23.0-pp27-pypy_73-win32.whl", hash = "sha256:93837e379a3e5fd2bb00302a47aee9fdf7940d83595be3915752c74033d17ca7"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:865d83c906b0fc6a59b510deceee656b6bc1c4fa0d82176e2b77e97a420a996a"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d4d56153efc4d81defe8b65fd0821ef8b2d5ddf8ed19df31ba2f00872b8002"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3f2d0aaf8080bda0587d58fc9fe4766e012441e2eed4269a77de6aea981c8be"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64093fc334c1eccfd3933c134c4457c34eaca235eeae49d69449dc4728079339"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ce64e84a962b63a47a592690bdc16a7eaf709d2c2697ababf24a0def566899a6"}, + {file = "pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef"}, ] [[package]] name = "pydantic" -version = "2.7.3" +version = "2.11.7" description = "Data validation using Python type hints" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pydantic-2.7.3-py3-none-any.whl", hash = "sha256:ea91b002777bf643bb20dd717c028ec43216b24a6001a280f83877fd2655d0b4"}, - {file = "pydantic-2.7.3.tar.gz", hash = "sha256:c46c76a40bb1296728d7a8b99aa73dd70a48c3510111ff290034f860c99c419e"}, + {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, + {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, ] [package.dependencies] -annotated-types = ">=0.4.0" -pydantic-core = "2.18.4" -typing-extensions = ">=4.6.1" +annotated-types = ">=0.6.0" +pydantic-core = "2.33.2" +typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" [package.extras] email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.18.4" +version = "2.33.2" description = "Core functionality for Pydantic validation and serialization" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" 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"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, ] [package.dependencies] @@ -2403,13 +2605,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pygments" -version = "2.18.0" +version = "2.19.2" 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"}, + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, ] [package.extras] @@ -2417,13 +2619,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pymdown-extensions" -version = "10.14.3" +version = "10.16.1" description = "Extension pack for Python Markdown." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" 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"}, + {file = "pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d"}, + {file = "pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91"}, ] [package.dependencies] @@ -2435,71 +2637,82 @@ extra = ["pygments (>=2.19.1)"] [[package]] name = "pyproject-hooks" -version = "1.1.0" +version = "1.2.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"}, + {file = "pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913"}, + {file = "pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8"}, ] [[package]] -name = "pyreadline" -version = "2.1" -description = "A python implmementation of GNU readline." +name = "pyreadline3" +version = "3.5.4" +description = "A python implementation of GNU readline." optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "pyreadline-2.1.zip", hash = "sha256:4530592fc2e85b25b1a9f79664433da09237c1a270e4d78ea5aa3a2c7229e2d1"}, + {file = "pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"}, + {file = "pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7"}, ] +[package.extras] +dev = ["build", "flake8", "mypy", "pytest", "twine"] + [[package]] name = "pyrepl" -version = "0.9.0" +version = "0.11.4" description = "A library for building flexible command line interfaces" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "pyrepl-0.9.0.tar.gz", hash = "sha256:292570f34b5502e871bbb966d639474f2b57fbfcd3373c2d6a2f3d56e681a775"}, + {file = "pyrepl-0.11.4-py3-none-any.whl", hash = "sha256:ac30d6340267a21c39e1b1934f92bca6b8735017d14b17e40f903b2d1563541d"}, + {file = "pyrepl-0.11.4.tar.gz", hash = "sha256:efe988b4a6e5eed587e9769dc2269aeec2b6feec2f5d77995ee85b9ad7cf7063"}, ] +[package.extras] +dev = ["pyrepl[tests]", "ruff (==0.11.8)"] +tests = ["pexpect", "pytest", "pytest-coverage", "pytest-timeout"] + [[package]] name = "pytest" -version = "8.2.2" +version = "8.4.1" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, - {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, + {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, + {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, ] [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" +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" 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"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" -version = "5.0.0" +version = "6.2.1" description = "Pytest plugin for measuring coverage." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" 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"}, + {file = "pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5"}, + {file = "pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2"}, ] [package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" +coverage = {version = ">=7.5", extras = ["toml"]} +pluggy = ">=1.2" +pytest = ">=6.2.5" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] @@ -2521,13 +2734,13 @@ pytest = ">=3.10" [[package]] name = "pytest-xdist" -version = "3.6.1" +version = "3.8.0" description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" 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"}, + {file = "pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88"}, + {file = "pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1"}, ] [package.dependencies] @@ -2555,13 +2768,13 @@ six = ">=1.5" [[package]] name = "pywin32-ctypes" -version = "0.2.2" +version = "0.2.3" 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"}, + {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, + {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, ] [[package]] @@ -2628,13 +2841,13 @@ files = [ [[package]] name = "pyyaml-env-tag" -version = "0.1" -description = "A custom YAML tag for referencing environment variables in YAML files. " +version = "1.1" +description = "A custom YAML tag for referencing environment variables in YAML files." optional = false -python-versions = ">=3.6" +python-versions = ">=3.9" 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"}, + {file = "pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04"}, + {file = "pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff"}, ] [package.dependencies] @@ -2642,211 +2855,220 @@ pyyaml = "*" [[package]] name = "rapidfuzz" -version = "3.9.3" +version = "3.13.0" description = "rapid fuzzy string matching" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" 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"] + {file = "rapidfuzz-3.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aafc42a1dc5e1beeba52cd83baa41372228d6d8266f6d803c16dbabbcc156255"}, + {file = "rapidfuzz-3.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:85c9a131a44a95f9cac2eb6e65531db014e09d89c4f18c7b1fa54979cb9ff1f3"}, + {file = "rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d7cec4242d30dd521ef91c0df872e14449d1dffc2a6990ede33943b0dae56c3"}, + {file = "rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e297c09972698c95649e89121e3550cee761ca3640cd005e24aaa2619175464e"}, + {file = "rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef0f5f03f61b0e5a57b1df7beafd83df993fd5811a09871bad6038d08e526d0d"}, + {file = "rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d8cf5f7cd6e4d5eb272baf6a54e182b2c237548d048e2882258336533f3f02b7"}, + {file = "rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9256218ac8f1a957806ec2fb9a6ddfc6c32ea937c0429e88cf16362a20ed8602"}, + {file = "rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1bdd2e6d0c5f9706ef7595773a81ca2b40f3b33fd7f9840b726fb00c6c4eb2e"}, + {file = "rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5280be8fd7e2bee5822e254fe0a5763aa0ad57054b85a32a3d9970e9b09bbcbf"}, + {file = "rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd742c03885db1fce798a1cd87a20f47f144ccf26d75d52feb6f2bae3d57af05"}, + {file = "rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5435fcac94c9ecf0504bf88a8a60c55482c32e18e108d6079a0089c47f3f8cf6"}, + {file = "rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:93a755266856599be4ab6346273f192acde3102d7aa0735e2f48b456397a041f"}, + {file = "rapidfuzz-3.13.0-cp310-cp310-win32.whl", hash = "sha256:3abe6a4e8eb4cfc4cda04dd650a2dc6d2934cbdeda5def7e6fd1c20f6e7d2a0b"}, + {file = "rapidfuzz-3.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:e8ddb58961401da7d6f55f185512c0d6bd24f529a637078d41dd8ffa5a49c107"}, + {file = "rapidfuzz-3.13.0-cp310-cp310-win_arm64.whl", hash = "sha256:c523620d14ebd03a8d473c89e05fa1ae152821920c3ff78b839218ff69e19ca3"}, + {file = "rapidfuzz-3.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d395a5cad0c09c7f096433e5fd4224d83b53298d53499945a9b0e5a971a84f3a"}, + {file = "rapidfuzz-3.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7b3eda607a019169f7187328a8d1648fb9a90265087f6903d7ee3a8eee01805"}, + {file = "rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98e0bfa602e1942d542de077baf15d658bd9d5dcfe9b762aff791724c1c38b70"}, + {file = "rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bef86df6d59667d9655905b02770a0c776d2853971c0773767d5ef8077acd624"}, + {file = "rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fedd316c165beed6307bf754dee54d3faca2c47e1f3bcbd67595001dfa11e969"}, + {file = "rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5158da7f2ec02a930be13bac53bb5903527c073c90ee37804090614cab83c29e"}, + {file = "rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b6f913ee4618ddb6d6f3e387b76e8ec2fc5efee313a128809fbd44e65c2bbb2"}, + {file = "rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d25fdbce6459ccbbbf23b4b044f56fbd1158b97ac50994eaae2a1c0baae78301"}, + {file = "rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:25343ccc589a4579fbde832e6a1e27258bfdd7f2eb0f28cb836d6694ab8591fc"}, + {file = "rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a9ad1f37894e3ffb76bbab76256e8a8b789657183870be11aa64e306bb5228fd"}, + {file = "rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5dc71ef23845bb6b62d194c39a97bb30ff171389c9812d83030c1199f319098c"}, + {file = "rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b7f4c65facdb94f44be759bbd9b6dda1fa54d0d6169cdf1a209a5ab97d311a75"}, + {file = "rapidfuzz-3.13.0-cp311-cp311-win32.whl", hash = "sha256:b5104b62711565e0ff6deab2a8f5dbf1fbe333c5155abe26d2cfd6f1849b6c87"}, + {file = "rapidfuzz-3.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:9093cdeb926deb32a4887ebe6910f57fbcdbc9fbfa52252c10b56ef2efb0289f"}, + {file = "rapidfuzz-3.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:f70f646751b6aa9d05be1fb40372f006cc89d6aad54e9d79ae97bd1f5fce5203"}, + {file = "rapidfuzz-3.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a1a6a906ba62f2556372282b1ef37b26bca67e3d2ea957277cfcefc6275cca7"}, + {file = "rapidfuzz-3.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2fd0975e015b05c79a97f38883a11236f5a24cca83aa992bd2558ceaa5652b26"}, + {file = "rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d4e13593d298c50c4f94ce453f757b4b398af3fa0fd2fde693c3e51195b7f69"}, + {file = "rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed6f416bda1c9133000009d84d9409823eb2358df0950231cc936e4bf784eb97"}, + {file = "rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1dc82b6ed01acb536b94a43996a94471a218f4d89f3fdd9185ab496de4b2a981"}, + {file = "rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9d824de871daa6e443b39ff495a884931970d567eb0dfa213d234337343835f"}, + {file = "rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d18228a2390375cf45726ce1af9d36ff3dc1f11dce9775eae1f1b13ac6ec50f"}, + {file = "rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5fe634c9482ec5d4a6692afb8c45d370ae86755e5f57aa6c50bfe4ca2bdd87"}, + {file = "rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:694eb531889f71022b2be86f625a4209c4049e74be9ca836919b9e395d5e33b3"}, + {file = "rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11b47b40650e06147dee5e51a9c9ad73bb7b86968b6f7d30e503b9f8dd1292db"}, + {file = "rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:98b8107ff14f5af0243f27d236bcc6e1ef8e7e3b3c25df114e91e3a99572da73"}, + {file = "rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b836f486dba0aceb2551e838ff3f514a38ee72b015364f739e526d720fdb823a"}, + {file = "rapidfuzz-3.13.0-cp312-cp312-win32.whl", hash = "sha256:4671ee300d1818d7bdfd8fa0608580d7778ba701817216f0c17fb29e6b972514"}, + {file = "rapidfuzz-3.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e2065f68fb1d0bf65adc289c1bdc45ba7e464e406b319d67bb54441a1b9da9e"}, + {file = "rapidfuzz-3.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:65cc97c2fc2c2fe23586599686f3b1ceeedeca8e598cfcc1b7e56dc8ca7e2aa7"}, + {file = "rapidfuzz-3.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:09e908064d3684c541d312bd4c7b05acb99a2c764f6231bd507d4b4b65226c23"}, + {file = "rapidfuzz-3.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:57c390336cb50d5d3bfb0cfe1467478a15733703af61f6dffb14b1cd312a6fae"}, + {file = "rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0da54aa8547b3c2c188db3d1c7eb4d1bb6dd80baa8cdaeaec3d1da3346ec9caa"}, + {file = "rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df8e8c21e67afb9d7fbe18f42c6111fe155e801ab103c81109a61312927cc611"}, + {file = "rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:461fd13250a2adf8e90ca9a0e1e166515cbcaa5e9c3b1f37545cbbeff9e77f6b"}, + {file = "rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2b3dd5d206a12deca16870acc0d6e5036abeb70e3cad6549c294eff15591527"}, + {file = "rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1343d745fbf4688e412d8f398c6e6d6f269db99a54456873f232ba2e7aeb4939"}, + {file = "rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b1b065f370d54551dcc785c6f9eeb5bd517ae14c983d2784c064b3aa525896df"}, + {file = "rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:11b125d8edd67e767b2295eac6eb9afe0b1cdc82ea3d4b9257da4b8e06077798"}, + {file = "rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c33f9c841630b2bb7e69a3fb5c84a854075bb812c47620978bddc591f764da3d"}, + {file = "rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ae4574cb66cf1e85d32bb7e9ec45af5409c5b3970b7ceb8dea90168024127566"}, + {file = "rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e05752418b24bbd411841b256344c26f57da1148c5509e34ea39c7eb5099ab72"}, + {file = "rapidfuzz-3.13.0-cp313-cp313-win32.whl", hash = "sha256:0e1d08cb884805a543f2de1f6744069495ef527e279e05370dd7c83416af83f8"}, + {file = "rapidfuzz-3.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9a7c6232be5f809cd39da30ee5d24e6cadd919831e6020ec6c2391f4c3bc9264"}, + {file = "rapidfuzz-3.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:3f32f15bacd1838c929b35c84b43618481e1b3d7a61b5ed2db0291b70ae88b53"}, + {file = "rapidfuzz-3.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cc64da907114d7a18b5e589057e3acaf2fec723d31c49e13fedf043592a3f6a7"}, + {file = "rapidfuzz-3.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4d9d7f84c8e992a8dbe5a3fdbea73d733da39bf464e62c912ac3ceba9c0cff93"}, + {file = "rapidfuzz-3.13.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a79a2f07786a2070669b4b8e45bd96a01c788e7a3c218f531f3947878e0f956"}, + {file = "rapidfuzz-3.13.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f338e71c45b69a482de8b11bf4a029993230760120c8c6e7c9b71760b6825a1"}, + {file = "rapidfuzz-3.13.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adb40ca8ddfcd4edd07b0713a860be32bdf632687f656963bcbce84cea04b8d8"}, + {file = "rapidfuzz-3.13.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48719f7dcf62dfb181063b60ee2d0a39d327fa8ad81b05e3e510680c44e1c078"}, + {file = "rapidfuzz-3.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9327a4577f65fc3fb712e79f78233815b8a1c94433d0c2c9f6bc5953018b3565"}, + {file = "rapidfuzz-3.13.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:200030dfc0a1d5d6ac18e993c5097c870c97c41574e67f227300a1fb74457b1d"}, + {file = "rapidfuzz-3.13.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cc269e74cad6043cb8a46d0ce580031ab642b5930562c2bb79aa7fbf9c858d26"}, + {file = "rapidfuzz-3.13.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:e62779c6371bd2b21dbd1fdce89eaec2d93fd98179d36f61130b489f62294a92"}, + {file = "rapidfuzz-3.13.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f4797f821dc5d7c2b6fc818b89f8a3f37bcc900dd9e4369e6ebf1e525efce5db"}, + {file = "rapidfuzz-3.13.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d21f188f6fe4fbf422e647ae9d5a68671d00218e187f91859c963d0738ccd88c"}, + {file = "rapidfuzz-3.13.0-cp39-cp39-win32.whl", hash = "sha256:45dd4628dd9c21acc5c97627dad0bb791764feea81436fb6e0a06eef4c6dceaa"}, + {file = "rapidfuzz-3.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:624a108122039af89ddda1a2b7ab2a11abe60c1521956f142f5d11bcd42ef138"}, + {file = "rapidfuzz-3.13.0-cp39-cp39-win_arm64.whl", hash = "sha256:435071fd07a085ecbf4d28702a66fd2e676a03369ee497cc38bcb69a46bc77e2"}, + {file = "rapidfuzz-3.13.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe5790a36d33a5d0a6a1f802aa42ecae282bf29ac6f7506d8e12510847b82a45"}, + {file = "rapidfuzz-3.13.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:cdb33ee9f8a8e4742c6b268fa6bd739024f34651a06b26913381b1413ebe7590"}, + {file = "rapidfuzz-3.13.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c99b76b93f7b495eee7dcb0d6a38fb3ce91e72e99d9f78faa5664a881cb2b7d"}, + {file = "rapidfuzz-3.13.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6af42f2ede8b596a6aaf6d49fdee3066ca578f4856b85ab5c1e2145de367a12d"}, + {file = "rapidfuzz-3.13.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c0efa73afbc5b265aca0d8a467ae2a3f40d6854cbe1481cb442a62b7bf23c99"}, + {file = "rapidfuzz-3.13.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7ac21489de962a4e2fc1e8f0b0da4aa1adc6ab9512fd845563fecb4b4c52093a"}, + {file = "rapidfuzz-3.13.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1ba007f4d35a45ee68656b2eb83b8715e11d0f90e5b9f02d615a8a321ff00c27"}, + {file = "rapidfuzz-3.13.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d7a217310429b43be95b3b8ad7f8fc41aba341109dc91e978cd7c703f928c58f"}, + {file = "rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:558bf526bcd777de32b7885790a95a9548ffdcce68f704a81207be4a286c1095"}, + {file = "rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:202a87760f5145140d56153b193a797ae9338f7939eb16652dd7ff96f8faf64c"}, + {file = "rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcccc08f671646ccb1e413c773bb92e7bba789e3a1796fd49d23c12539fe2e4"}, + {file = "rapidfuzz-3.13.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1f219f1e3c3194d7a7de222f54450ce12bc907862ff9a8962d83061c1f923c86"}, + {file = "rapidfuzz-3.13.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ccbd0e7ea1a216315f63ffdc7cd09c55f57851afc8fe59a74184cb7316c0598b"}, + {file = "rapidfuzz-3.13.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a50856f49a4016ef56edd10caabdaf3608993f9faf1e05c3c7f4beeac46bd12a"}, + {file = "rapidfuzz-3.13.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fd05336db4d0b8348d7eaaf6fa3c517b11a56abaa5e89470ce1714e73e4aca7"}, + {file = "rapidfuzz-3.13.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:573ad267eb9b3f6e9b04febce5de55d8538a87c56c64bf8fd2599a48dc9d8b77"}, + {file = "rapidfuzz-3.13.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30fd1451f87ccb6c2f9d18f6caa483116bbb57b5a55d04d3ddbd7b86f5b14998"}, + {file = "rapidfuzz-3.13.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6dd36d4916cf57ddb05286ed40b09d034ca5d4bca85c17be0cb6a21290597d9"}, + {file = "rapidfuzz-3.13.0.tar.gz", hash = "sha256:d2eaf3839e52cbcc0accbe9817a67b4b0fcf70aaeb229cfddc1c28061f9ce5d8"}, +] + +[package.extras] +all = ["numpy"] [[package]] name = "regex" -version = "2024.5.15" +version = "2025.7.34" description = "Alternative regular expression module, to replace re." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" 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"}, + {file = "regex-2025.7.34-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d856164d25e2b3b07b779bfed813eb4b6b6ce73c2fd818d46f47c1eb5cd79bd6"}, + {file = "regex-2025.7.34-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d15a9da5fad793e35fb7be74eec450d968e05d2e294f3e0e77ab03fa7234a83"}, + {file = "regex-2025.7.34-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:95b4639c77d414efa93c8de14ce3f7965a94d007e068a94f9d4997bb9bd9c81f"}, + {file = "regex-2025.7.34-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7de1ceed5a5f84f342ba4a9f4ae589524adf9744b2ee61b5da884b5b659834"}, + {file = "regex-2025.7.34-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02e5860a250cd350c4933cf376c3bc9cb28948e2c96a8bc042aee7b985cfa26f"}, + {file = "regex-2025.7.34-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a5966220b9a1a88691282b7e4350e9599cf65780ca60d914a798cb791aa1177"}, + {file = "regex-2025.7.34-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48fb045bbd4aab2418dc1ba2088a5e32de4bfe64e1457b948bb328a8dc2f1c2e"}, + {file = "regex-2025.7.34-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:20ff8433fa45e131f7316594efe24d4679c5449c0ca69d91c2f9d21846fdf064"}, + {file = "regex-2025.7.34-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c436fd1e95c04c19039668cfb548450a37c13f051e8659f40aed426e36b3765f"}, + {file = "regex-2025.7.34-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0b85241d3cfb9f8a13cefdfbd58a2843f208f2ed2c88181bf84e22e0c7fc066d"}, + {file = "regex-2025.7.34-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:075641c94126b064c65ab86e7e71fc3d63e7ff1bea1fb794f0773c97cdad3a03"}, + {file = "regex-2025.7.34-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:70645cad3407d103d1dbcb4841839d2946f7d36cf38acbd40120fee1682151e5"}, + {file = "regex-2025.7.34-cp310-cp310-win32.whl", hash = "sha256:3b836eb4a95526b263c2a3359308600bd95ce7848ebd3c29af0c37c4f9627cd3"}, + {file = "regex-2025.7.34-cp310-cp310-win_amd64.whl", hash = "sha256:cbfaa401d77334613cf434f723c7e8ba585df162be76474bccc53ae4e5520b3a"}, + {file = "regex-2025.7.34-cp310-cp310-win_arm64.whl", hash = "sha256:bca11d3c38a47c621769433c47f364b44e8043e0de8e482c5968b20ab90a3986"}, + {file = "regex-2025.7.34-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da304313761b8500b8e175eb2040c4394a875837d5635f6256d6fa0377ad32c8"}, + {file = "regex-2025.7.34-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:35e43ebf5b18cd751ea81455b19acfdec402e82fe0dc6143edfae4c5c4b3909a"}, + {file = "regex-2025.7.34-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96bbae4c616726f4661fe7bcad5952e10d25d3c51ddc388189d8864fbc1b3c68"}, + {file = "regex-2025.7.34-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9feab78a1ffa4f2b1e27b1bcdaad36f48c2fed4870264ce32f52a393db093c78"}, + {file = "regex-2025.7.34-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f14b36e6d4d07f1a5060f28ef3b3561c5d95eb0651741474ce4c0a4c56ba8719"}, + {file = "regex-2025.7.34-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85c3a958ef8b3d5079c763477e1f09e89d13ad22198a37e9d7b26b4b17438b33"}, + {file = "regex-2025.7.34-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37555e4ae0b93358fa7c2d240a4291d4a4227cc7c607d8f85596cdb08ec0a083"}, + {file = "regex-2025.7.34-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee38926f31f1aa61b0232a3a11b83461f7807661c062df9eb88769d86e6195c3"}, + {file = "regex-2025.7.34-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a664291c31cae9c4a30589bd8bc2ebb56ef880c9c6264cb7643633831e606a4d"}, + {file = "regex-2025.7.34-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f3e5c1e0925e77ec46ddc736b756a6da50d4df4ee3f69536ffb2373460e2dafd"}, + {file = "regex-2025.7.34-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d428fc7731dcbb4e2ffe43aeb8f90775ad155e7db4347a639768bc6cd2df881a"}, + {file = "regex-2025.7.34-cp311-cp311-win32.whl", hash = "sha256:e154a7ee7fa18333ad90b20e16ef84daaeac61877c8ef942ec8dfa50dc38b7a1"}, + {file = "regex-2025.7.34-cp311-cp311-win_amd64.whl", hash = "sha256:24257953d5c1d6d3c129ab03414c07fc1a47833c9165d49b954190b2b7f21a1a"}, + {file = "regex-2025.7.34-cp311-cp311-win_arm64.whl", hash = "sha256:3157aa512b9e606586900888cd469a444f9b898ecb7f8931996cb715f77477f0"}, + {file = "regex-2025.7.34-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7f7211a746aced993bef487de69307a38c5ddd79257d7be83f7b202cb59ddb50"}, + {file = "regex-2025.7.34-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fb31080f2bd0681484b275461b202b5ad182f52c9ec606052020fe13eb13a72f"}, + {file = "regex-2025.7.34-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0200a5150c4cf61e407038f4b4d5cdad13e86345dac29ff9dab3d75d905cf130"}, + {file = "regex-2025.7.34-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:739a74970e736df0773788377969c9fea3876c2fc13d0563f98e5503e5185f46"}, + {file = "regex-2025.7.34-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4fef81b2f7ea6a2029161ed6dea9ae13834c28eb5a95b8771828194a026621e4"}, + {file = "regex-2025.7.34-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea74cf81fe61a7e9d77989050d0089a927ab758c29dac4e8e1b6c06fccf3ebf0"}, + {file = "regex-2025.7.34-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4636a7f3b65a5f340ed9ddf53585c42e3ff37101d383ed321bfe5660481744b"}, + {file = "regex-2025.7.34-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cef962d7834437fe8d3da6f9bfc6f93f20f218266dcefec0560ed7765f5fe01"}, + {file = "regex-2025.7.34-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:cbe1698e5b80298dbce8df4d8d1182279fbdaf1044e864cbc9d53c20e4a2be77"}, + {file = "regex-2025.7.34-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:32b9f9bcf0f605eb094b08e8da72e44badabb63dde6b83bd530580b488d1c6da"}, + {file = "regex-2025.7.34-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:524c868ba527eab4e8744a9287809579f54ae8c62fbf07d62aacd89f6026b282"}, + {file = "regex-2025.7.34-cp312-cp312-win32.whl", hash = "sha256:d600e58ee6d036081c89696d2bdd55d507498a7180df2e19945c6642fac59588"}, + {file = "regex-2025.7.34-cp312-cp312-win_amd64.whl", hash = "sha256:9a9ab52a466a9b4b91564437b36417b76033e8778e5af8f36be835d8cb370d62"}, + {file = "regex-2025.7.34-cp312-cp312-win_arm64.whl", hash = "sha256:c83aec91af9c6fbf7c743274fd952272403ad9a9db05fe9bfc9df8d12b45f176"}, + {file = "regex-2025.7.34-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3c9740a77aeef3f5e3aaab92403946a8d34437db930a0280e7e81ddcada61f5"}, + {file = "regex-2025.7.34-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:69ed3bc611540f2ea70a4080f853741ec698be556b1df404599f8724690edbcd"}, + {file = "regex-2025.7.34-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d03c6f9dcd562c56527c42b8530aad93193e0b3254a588be1f2ed378cdfdea1b"}, + {file = "regex-2025.7.34-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6164b1d99dee1dfad33f301f174d8139d4368a9fb50bf0a3603b2eaf579963ad"}, + {file = "regex-2025.7.34-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1e4f4f62599b8142362f164ce776f19d79bdd21273e86920a7b604a4275b4f59"}, + {file = "regex-2025.7.34-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:72a26dcc6a59c057b292f39d41465d8233a10fd69121fa24f8f43ec6294e5415"}, + {file = "regex-2025.7.34-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5273fddf7a3e602695c92716c420c377599ed3c853ea669c1fe26218867002f"}, + {file = "regex-2025.7.34-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c1844be23cd40135b3a5a4dd298e1e0c0cb36757364dd6cdc6025770363e06c1"}, + {file = "regex-2025.7.34-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dde35e2afbbe2272f8abee3b9fe6772d9b5a07d82607b5788e8508974059925c"}, + {file = "regex-2025.7.34-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f6e8e7af516a7549412ce57613e859c3be27d55341a894aacaa11703a4c31a"}, + {file = "regex-2025.7.34-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:469142fb94a869beb25b5f18ea87646d21def10fbacb0bcb749224f3509476f0"}, + {file = "regex-2025.7.34-cp313-cp313-win32.whl", hash = "sha256:da7507d083ee33ccea1310447410c27ca11fb9ef18c95899ca57ff60a7e4d8f1"}, + {file = "regex-2025.7.34-cp313-cp313-win_amd64.whl", hash = "sha256:9d644de5520441e5f7e2db63aec2748948cc39ed4d7a87fd5db578ea4043d997"}, + {file = "regex-2025.7.34-cp313-cp313-win_arm64.whl", hash = "sha256:7bf1c5503a9f2cbd2f52d7e260acb3131b07b6273c470abb78568174fe6bde3f"}, + {file = "regex-2025.7.34-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:8283afe7042d8270cecf27cca558873168e771183d4d593e3c5fe5f12402212a"}, + {file = "regex-2025.7.34-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6c053f9647e3421dd2f5dff8172eb7b4eec129df9d1d2f7133a4386319b47435"}, + {file = "regex-2025.7.34-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a16dd56bbcb7d10e62861c3cd000290ddff28ea142ffb5eb3470f183628011ac"}, + {file = "regex-2025.7.34-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69c593ff5a24c0d5c1112b0df9b09eae42b33c014bdca7022d6523b210b69f72"}, + {file = "regex-2025.7.34-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98d0ce170fcde1a03b5df19c5650db22ab58af375aaa6ff07978a85c9f250f0e"}, + {file = "regex-2025.7.34-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d72765a4bff8c43711d5b0f5b452991a9947853dfa471972169b3cc0ba1d0751"}, + {file = "regex-2025.7.34-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4494f8fd95a77eb434039ad8460e64d57baa0434f1395b7da44015bef650d0e4"}, + {file = "regex-2025.7.34-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4f42b522259c66e918a0121a12429b2abcf696c6f967fa37bdc7b72e61469f98"}, + {file = "regex-2025.7.34-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:aaef1f056d96a0a5d53ad47d019d5b4c66fe4be2da87016e0d43b7242599ffc7"}, + {file = "regex-2025.7.34-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:656433e5b7dccc9bc0da6312da8eb897b81f5e560321ec413500e5367fcd5d47"}, + {file = "regex-2025.7.34-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e91eb2c62c39705e17b4d42d4b86c4e86c884c0d15d9c5a47d0835f8387add8e"}, + {file = "regex-2025.7.34-cp314-cp314-win32.whl", hash = "sha256:f978ddfb6216028c8f1d6b0f7ef779949498b64117fc35a939022f67f810bdcb"}, + {file = "regex-2025.7.34-cp314-cp314-win_amd64.whl", hash = "sha256:4b7dc33b9b48fb37ead12ffc7bdb846ac72f99a80373c4da48f64b373a7abeae"}, + {file = "regex-2025.7.34-cp314-cp314-win_arm64.whl", hash = "sha256:4b8c4d39f451e64809912c82392933d80fe2e4a87eeef8859fcc5380d0173c64"}, + {file = "regex-2025.7.34-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fd5edc3f453de727af267c7909d083e19f6426fc9dd149e332b6034f2a5611e6"}, + {file = "regex-2025.7.34-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa1cdfb8db96ef20137de5587954c812821966c3e8b48ffc871e22d7ec0a4938"}, + {file = "regex-2025.7.34-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:89c9504fc96268e8e74b0283e548f53a80c421182a2007e3365805b74ceef936"}, + {file = "regex-2025.7.34-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33be70d75fa05a904ee0dc43b650844e067d14c849df7e82ad673541cd465b5f"}, + {file = "regex-2025.7.34-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57d25b6732ea93eeb1d090e8399b6235ca84a651b52d52d272ed37d3d2efa0f1"}, + {file = "regex-2025.7.34-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:baf2fe122a3db1c0b9f161aa44463d8f7e33eeeda47bb0309923deb743a18276"}, + {file = "regex-2025.7.34-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a764a83128af9c1a54be81485b34dca488cbcacefe1e1d543ef11fbace191e1"}, + {file = "regex-2025.7.34-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c7f663ccc4093877f55b51477522abd7299a14c5bb7626c5238599db6a0cb95d"}, + {file = "regex-2025.7.34-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4913f52fbc7a744aaebf53acd8d3dc1b519e46ba481d4d7596de3c862e011ada"}, + {file = "regex-2025.7.34-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:efac4db9e044d47fd3b6b0d40b6708f4dfa2d8131a5ac1d604064147c0f552fd"}, + {file = "regex-2025.7.34-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:7373afae7cfb716e3b8e15d0184510d518f9d21471f2d62918dbece85f2c588f"}, + {file = "regex-2025.7.34-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9960d162f3fecf6af252534a1ae337e9c2e20d74469fed782903b24e2cc9d3d7"}, + {file = "regex-2025.7.34-cp39-cp39-win32.whl", hash = "sha256:95d538b10eb4621350a54bf14600cc80b514211d91a019dc74b8e23d2159ace5"}, + {file = "regex-2025.7.34-cp39-cp39-win_amd64.whl", hash = "sha256:f7f3071b5faa605b0ea51ec4bb3ea7257277446b053f4fd3ad02b1dcb4e64353"}, + {file = "regex-2025.7.34-cp39-cp39-win_arm64.whl", hash = "sha256:716a47515ba1d03f8e8a61c5013041c8c90f2e21f055203498105d7571b44531"}, + {file = "regex-2025.7.34.tar.gz", hash = "sha256:9ead9765217afd04a86822dfcd4ed2747dfe426e887da413b15ff0ac2457e21a"}, ] [[package]] name = "requests" -version = "2.32.3" +version = "2.32.4" 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"}, + {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, + {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, ] [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" +charset_normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<3" @@ -2870,13 +3092,13 @@ requests = ">=2.0.1,<3.0.0" [[package]] name = "rich" -version = "13.7.1" +version = "14.1.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.8.0" files = [ - {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, - {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, + {file = "rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f"}, + {file = "rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8"}, ] [package.dependencies] @@ -2888,23 +3110,23 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rlp" -version = "4.0.1" +version = "4.1.0" 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"}, + {file = "rlp-4.1.0-py3-none-any.whl", hash = "sha256:8eca394c579bad34ee0b937aecb96a57052ff3716e19c7a578883e767bc5da6f"}, + {file = "rlp-4.1.0.tar.gz", hash = "sha256:be07564270a96f3e225e2c107db263de96b5bc1f27722d2855bd3459a08e95a9"}, ] [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)"] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "hypothesis (>=6.22.0,<6.108.7)", "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 (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] rust-backend = ["rusty-rlp (>=0.2.1)"] -test = ["hypothesis (==5.19.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] +test = ["hypothesis (>=6.22.0,<6.108.7)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] [[package]] name = "secretstorage" @@ -2934,24 +3156,24 @@ files = [ [[package]] name = "six" -version = "1.16.0" +version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] name = "snekmate" -version = "0.1.1" +version = "0.1.2" 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"}, + {file = "snekmate-0.1.2-py3-none-any.whl", hash = "sha256:df439ba25843f52e6d7e1b58000192f77cd0a5caa9a756075655ead2b66e6314"}, + {file = "snekmate-0.1.2.tar.gz", hash = "sha256:8ed2c6a29b90424543d1792599c5c0b5ccf6988931b43029d1ca7e520a045038"}, ] [[package]] @@ -3017,35 +3239,65 @@ forking-recommended = ["requests-cache (>=1.2.1)", "ujson (>=5.10.0)"] [[package]] name = "tomli" -version = "2.0.1" +version = "2.2.1" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] [[package]] name = "tomlkit" -version = "0.12.5" +version = "0.13.3" description = "Style preserving TOML library" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomlkit-0.12.5-py3-none-any.whl", hash = "sha256:af914f5a9c59ed9d0762c7b64d3b5d5df007448eb9cd2edc8a46b1eafead172f"}, - {file = "tomlkit-0.12.5.tar.gz", hash = "sha256:eef34fba39834d4d6b73c9ba7f3e4d1c417a4e56f89a7e96e090dd0d24b8fb3c"}, + {file = "tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0"}, + {file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"}, ] [[package]] name = "toolz" -version = "0.12.1" +version = "1.0.0" description = "List processing tools and functional utilities" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "toolz-0.12.1-py3-none-any.whl", hash = "sha256:d22731364c07d72eea0a0ad45bafb2c2937ab6fd38a3507bf55eae8744aa7d85"}, - {file = "toolz-0.12.1.tar.gz", hash = "sha256:ecca342664893f177a13dac0e6b41cbd8ac25a358e5f215316d43e2100224f4d"}, + {file = "toolz-1.0.0-py3-none-any.whl", hash = "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236"}, + {file = "toolz-1.0.0.tar.gz", hash = "sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02"}, ] [[package]] @@ -3065,13 +3317,13 @@ test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0, [[package]] name = "trie" -version = "3.0.1" +version = "3.1.0" 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"}, + {file = "trie-3.1.0-py3-none-any.whl", hash = "sha256:dfc3e6ac0e76f0efa900ec1bfd082f0f1ba87f95cbfd81cc12338b03f4c679c4"}, + {file = "trie-3.1.0.tar.gz", hash = "sha256:b31fd3376d6dccfe8ad13b525e233f2c268d5c48afb90a4de09672423d4b1026"}, ] [package.dependencies] @@ -3082,41 +3334,55 @@ 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)"] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "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 (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["towncrier (>=24,<25)"] test = ["hypothesis (>=6.56.4,<7)", "pycryptodome", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] [[package]] name = "trove-classifiers" -version = "2024.5.22" +version = "2025.5.9.12" 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"}, + {file = "trove_classifiers-2025.5.9.12-py3-none-any.whl", hash = "sha256:e381c05537adac78881c8fa345fd0e9970159f4e4a04fcc42cfd3129cca640ce"}, + {file = "trove_classifiers-2025.5.9.12.tar.gz", hash = "sha256:7ca7c8a7a76e2cd314468c677c69d12cc2357711fcab4a60f87994c1589e5cb5"}, ] [[package]] name = "typing-extensions" -version = "4.12.1" -description = "Backported and Experimental Type Hints for Python 3.8+" +version = "4.14.1" +description = "Backported and Experimental Type Hints for Python 3.9+" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +files = [ + {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, + {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" 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"}, + {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, + {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, ] +[package.dependencies] +typing-extensions = ">=4.12.0" + [[package]] name = "urllib3" -version = "2.2.1" +version = "2.5.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, - {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, ] [package.extras] @@ -3127,13 +3393,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.26.2" +version = "20.32.0" description = "Virtual Python Environment builder" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "virtualenv-20.26.2-py3-none-any.whl", hash = "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b"}, - {file = "virtualenv-20.26.2.tar.gz", hash = "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c"}, + {file = "virtualenv-20.32.0-py3-none-any.whl", hash = "sha256:2c310aecb62e5aa1b06103ed7c2977b81e042695de2697d01017ff0f1034af56"}, + {file = "virtualenv-20.32.0.tar.gz", hash = "sha256:886bf75cadfdc964674e6e33eb74d787dff31ca314ceace03ca5810620f4ecf0"}, ] [package.dependencies] @@ -3162,28 +3428,28 @@ requests = ">=2.32.3,<3" [[package]] name = "vyper" -version = "0.4.1" +version = "0.4.3" 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"}, + {file = "vyper-0.4.3-py3-none-any.whl", hash = "sha256:3b9671727c888363740dc678e60336759871487d0e4e9fdd973048fa9635c4fd"}, + {file = "vyper-0.4.3.tar.gz", hash = "sha256:22a7573657401d8a3bc690d65d6b7741680007180978c3a05f9892db31d5dcf5"}, ] [package.dependencies] -asttokens = ">=2.0.5,<3" +asttokens = ">=2.0.5,<4" cbor2 = ">=5.4.6,<6" -importlib-metadata = "*" +immutables = "*" lark = ">=1.0.0,<2" -packaging = ">=23.1,<24" +packaging = ">=23.1" 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"] +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.12.1b1)", "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"] +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.12.1b1)", "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" @@ -3240,13 +3506,13 @@ files = [ [[package]] name = "wheel" -version = "0.43.0" +version = "0.45.1" 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"}, + {file = "wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248"}, + {file = "wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729"}, ] [package.extras] @@ -3271,69 +3537,81 @@ test = ["pytest"] [[package]] name = "xattr" -version = "1.1.0" +version = "1.2.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"}, + {file = "xattr-1.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3df4d8d91e2996c3c72a390ec82e8544acdcb6c7df67b954f1736ff37ea4293e"}, + {file = "xattr-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f5eec248976bbfa6c23df25d4995413df57dccf4161f6cbae36f643e99dbc397"}, + {file = "xattr-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fafecfdedf7e8d455443bec2c3edab8a93d64672619cd1a4ee043a806152e19c"}, + {file = "xattr-1.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c229e245c6c9a85d2fd7d07531498f837dd34670e556b552f73350f11edf000c"}, + {file = "xattr-1.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:376631e2383918fbc3dc9bcaeb9a533e319322d2cff1c119635849edf74e1126"}, + {file = "xattr-1.2.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbae24ab22afe078d549645501ecacaa17229e0b7769c8418fad69b51ad37c9"}, + {file = "xattr-1.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a161160211081d765ac41fa056f4f9b1051f027f08188730fbc9782d0dce623e"}, + {file = "xattr-1.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a542acf6c4e8221664b51b35e0160c44bd0ed1f2fd80019476f7698f4911e560"}, + {file = "xattr-1.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:034f075fc5a9391a1597a6c9a21cb57b688680f0f18ecf73b2efc22b8d330cff"}, + {file = "xattr-1.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:00c26c14c90058338993bb2d3e1cebf562e94ec516cafba64a8f34f74b9d18b4"}, + {file = "xattr-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b4f43dc644db87d5eb9484a9518c34a864cb2e588db34cffc42139bf55302a1c"}, + {file = "xattr-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c7602583fc643ca76576498e2319c7cef0b72aef1936701678589da6371b731b"}, + {file = "xattr-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90c3ad4a9205cceb64ec54616aa90aa42d140c8ae3b9710a0aaa2843a6f1aca7"}, + {file = "xattr-1.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83d87cfe19cd606fc0709d45a4d6efc276900797deced99e239566926a5afedf"}, + {file = "xattr-1.2.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c67dabd9ddc04ead63fbc85aed459c9afcc24abfc5bb3217fff7ec9a466faacb"}, + {file = "xattr-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9a18ee82d8ba2c17f1e8414bfeb421fa763e0fb4acbc1e124988ca1584ad32d5"}, + {file = "xattr-1.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:38de598c47b85185e745986a061094d2e706e9c2d9022210d2c738066990fe91"}, + {file = "xattr-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:15e754e854bdaac366ad3f1c8fbf77f6668e8858266b4246e8c5f487eeaf1179"}, + {file = "xattr-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:daff0c1f5c5e4eaf758c56259c4f72631fa9619875e7a25554b6077dc73da964"}, + {file = "xattr-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:109b11fb3f73a0d4e199962f11230ab5f462e85a8021874f96c1732aa61148d5"}, + {file = "xattr-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c7c12968ce0bf798d8ba90194cef65de768bee9f51a684e022c74cab4218305"}, + {file = "xattr-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37989dabf25ff18773e4aaeebcb65604b9528f8645f43e02bebaa363e3ae958"}, + {file = "xattr-1.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:165de92b0f2adafb336f936931d044619b9840e35ba01079f4dd288747b73714"}, + {file = "xattr-1.2.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82191c006ae4c609b22b9aea5f38f68fff022dc6884c4c0e1dba329effd4b288"}, + {file = "xattr-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2b2e9c87dc643b09d86befad218e921f6e65b59a4668d6262b85308de5dbd1dd"}, + {file = "xattr-1.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:14edd5d47d0bb92b23222c0bb6379abbddab01fb776b2170758e666035ecf3aa"}, + {file = "xattr-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:12183d5eb104d4da787638c7dadf63b718472d92fec6dbe12994ea5d094d7863"}, + {file = "xattr-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c385ea93a18aeb6443a719eb6a6b1d7f7b143a4d1f2b08bc4fadfc429209e629"}, + {file = "xattr-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2d39d7b36842c67ab3040bead7eb6d601e35fa0d6214ed20a43df4ec30b6f9f9"}, + {file = "xattr-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:320ef856bb817f4c40213b6de956dc440d0f23cdc62da3ea02239eb5147093f8"}, + {file = "xattr-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26d306bfb3b5641726f2ee0da6f63a2656aa7fdcfd15de61c476e3ca6bc3277e"}, + {file = "xattr-1.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c67e70d5d8136d328ad13f85b887ffa97690422f1a11fb29ab2f702cf66e825a"}, + {file = "xattr-1.2.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8904d3539afe1a84fc0b7f02fa91da60d2505adf2d5951dc855bf9e75fe322b2"}, + {file = "xattr-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2520516c1d058895eae00b2b2f10833514caea6dc6802eef1e431c474b5317ad"}, + {file = "xattr-1.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:29d06abbef4024b7469fcd0d4ade6d2290582350a4df95fcc48fa48b2e83246b"}, + {file = "xattr-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:093c75f7d9190be355b8e86da3f460b9bfe3d6a176f92852d44dcc3289aa10dc"}, + {file = "xattr-1.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ee3901db48de913dcef004c5d7b477a1f4aadff997445ef62907b10fdad57de"}, + {file = "xattr-1.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b837898a5225c7f7df731783cd78bae2ed81b84bacf020821f1cd2ab2d74de58"}, + {file = "xattr-1.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cedc281811e424ecf6a14208532f7ac646866f91f88e8eadd00d8fe535e505fd"}, + {file = "xattr-1.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf60577caa248f539e4e646090b10d6ad1f54189de9a7f1854c23fdef28f574e"}, + {file = "xattr-1.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:363724f33510d2e7c7e080b389271a1241cb4929a1d9294f89721152b4410972"}, + {file = "xattr-1.2.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97db00596865845efb72f3d565a1f82b01006c5bf5a87d8854a6afac43502593"}, + {file = "xattr-1.2.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0b199ba31078f3e4181578595cd60400ee055b4399672169ceee846d33ff26de"}, + {file = "xattr-1.2.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:b19472dc38150ac09a478c71092738d86882bc9ff687a4a8f7d1a25abce20b5e"}, + {file = "xattr-1.2.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:79f7823b30ed557e0e7ffd9a6b1a821a22f485f5347e54b8d24c4a34b7545ba4"}, + {file = "xattr-1.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8eee258f5774933cb972cff5c3388166374e678980d2a1f417d7d6f61d9ae172"}, + {file = "xattr-1.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2a9de621eadf0466c391363bd6ed903b1a1bcd272422b5183fd06ef79d05347b"}, + {file = "xattr-1.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc714f236f17c57c510ae9ada9962d8e4efc9f9ea91504e2c6a09008f3918ddf"}, + {file = "xattr-1.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:545e0ad3f706724029efd23dec58fb358422ae68ab4b560b712aedeaf40446a0"}, + {file = "xattr-1.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:200bb3cdba057cb721b727607bc340a74c28274f4a628a26011f574860f5846b"}, + {file = "xattr-1.2.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b0b27c889cc9ff0dba62ac8a2eef98f4911c1621e4e8c409d5beb224c4c227c"}, + {file = "xattr-1.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ea7cf8afd717853ad78eba8ca83ff66a53484ba2bb2a4283462bc5c767518174"}, + {file = "xattr-1.2.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:02fa813db054bbb7a61c570ae025bd01c36fc20727b40f49031feb930234bc72"}, + {file = "xattr-1.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2827e23d7a1a20f31162c47ab4bd341a31e83421121978c4ab2aad5cd79ea82b"}, + {file = "xattr-1.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:29ae44247d46e63671311bf7e700826a97921278e2c0c04c2d11741888db41b8"}, + {file = "xattr-1.2.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:629c42c1dd813442d90f281f69b88ef0c9625f604989bef8411428671f70f43e"}, + {file = "xattr-1.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:549f8fbda5da48cafc81ba6ab7bb8e8e14c4b0748c37963dc504bcae505474b7"}, + {file = "xattr-1.2.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa83e677b5f92a3c5c86eaf875e9d3abbc43887ff1767178def865fa9f12a3a0"}, + {file = "xattr-1.2.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb669f01627962ce2bc556f19d421162247bc2cad0d4625d6ea5eb32af4cf29b"}, + {file = "xattr-1.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:212156aa5fb987a53211606bc09e6fea3eda3855af9f2940e40df5a2a592425a"}, + {file = "xattr-1.2.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:7dc4fa9448a513077c5ccd1ce428ff0682cdddfc71301dbbe4ee385c74517f73"}, + {file = "xattr-1.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e4b93f2e74793b61c0a7b7bdef4a3813930df9c01eda72fad706b8db7658bc2"}, + {file = "xattr-1.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dddd5f6d0bb95b099d6a3888c248bf246525647ccb8cf9e8f0fc3952e012d6fb"}, + {file = "xattr-1.2.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68fbdffebe8c398a82c84ecf5e6f6a3adde9364f891cba066e58352af404a45c"}, + {file = "xattr-1.2.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c9ee84de7cd4a6d61b0b79e2f58a6bdb13b03dbad948489ebb0b73a95caee7ae"}, + {file = "xattr-1.2.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5594fcbc38fdbb3af16a8ad18c37c81c8814955f0d636be857a67850cd556490"}, + {file = "xattr-1.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:017aac8005e1e84d5efa4b86c0896c6eb96f2331732d388600a5b999166fec1c"}, + {file = "xattr-1.2.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d27a64f695440450c119ae4bc8f54b0b726a812ebea1666fff3873236936f36"}, + {file = "xattr-1.2.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f7e7067e1a400ad4485536a9e84c3330373086b2324fafa26d07527eeb4b175"}, + {file = "xattr-1.2.0.tar.gz", hash = "sha256:a64c8e21eff1be143accf80fd3b8fde3e28a478c37da298742af647ac3e5e0a7"}, ] [package.dependencies] @@ -3344,20 +3622,24 @@ test = ["pytest"] [[package]] name = "zipp" -version = "3.19.2" +version = "3.23.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, - {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, + {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, + {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, ] [package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] 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)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<4" -content-hash = "5eafe79451ba6c6b3d2b295bf09be587b1e0f328ef51de8501bb8f772ee2ed36" +content-hash = "f9e74e34a08e706327695c531ab4017e35e1c68707de1a94bb7f80e656e81195" diff --git a/pyproject.toml b/pyproject.toml index bb3a953e..5574bb85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ package-mode = false [tool.poetry.dependencies] python = ">=3.10,<4" -vyper = "0.4.1" +vyper = "0.4.3" poetry = "^1" titanoboa = "0.2.6" hypothesis = "^6.99.0" From f9aa10b757e7823ca1b6a16a527fe83ad10ecc82 Mon Sep 17 00:00:00 2001 From: macket Date: Sun, 3 Aug 2025 14:48:59 +0400 Subject: [PATCH 066/413] fix: get price_oracle from AMM contract --- contracts/lending/LendingFactory.vy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/lending/LendingFactory.vy b/contracts/lending/LendingFactory.vy index 99048b73..bad3dfa1 100644 --- a/contracts/lending/LendingFactory.vy +++ b/contracts/lending/LendingFactory.vy @@ -17,7 +17,6 @@ interface Vault: 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: @@ -25,6 +24,7 @@ interface Controller: interface AMM: def set_admin(_admin: address): nonpayable + def price_oracle_contract() -> address: view interface Pool: def price_oracle(i: uint256 = 0) -> uint256: view # Universal method! @@ -373,7 +373,7 @@ def collateral_tokens(n: uint256) -> address: @view @external def price_oracles(n: uint256) -> address: - return self.vaults[n].price_oracle() + return AMM(self.vaults[n].amm()).price_oracle_contract() @view From 83765dc88d662bc6e7fc60c103435838418bf450 Mon Sep 17 00:00:00 2001 From: macket Date: Sun, 3 Aug 2025 14:53:03 +0400 Subject: [PATCH 067/413] test: adjust tests for new lending controller and factory --- tests/lending/conftest.py | 49 ++++++++++--------- .../test_health_calculator_stateful.py | 3 +- tests/lending/test_oracle_attack.py | 38 ++++++++------ tests/lending/test_vault.py | 4 -- 4 files changed, 50 insertions(+), 44 deletions(-) diff --git a/tests/lending/conftest.py b/tests/lending/conftest.py index 42a5eb14..011746cd 100644 --- a/tests/lending/conftest.py +++ b/tests/lending/conftest.py @@ -16,7 +16,7 @@ def amm_impl(amm_interface, admin): @pytest.fixture(scope="session") def controller_interface(): - return boa.load_partial('contracts/Controller.vy') + return boa.load_partial('contracts/lending/Controller.vy') @pytest.fixture(scope="session") @@ -30,6 +30,11 @@ def stablecoin(get_borrowed_token): return get_borrowed_token(18) +@pytest.fixture(scope="session") +def vault_interface(): + return boa.load_partial('contracts/lending/Vault.vy') + + @pytest.fixture(scope="session") def vault_impl(admin): with boa.env.prank(admin): @@ -58,25 +63,18 @@ def mpolicy_impl(mpolicy_interface, 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') + return boa.load_partial('contracts/lending/LendingFactory.vy') @pytest.fixture(scope="module") -def factory(factory_partial, stablecoin, amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, gauge_impl, admin): +def factory(factory_partial, stablecoin, amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_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) + price_oracle_impl, mpolicy_impl, admin, admin) @pytest.fixture(scope="module", params=product([2, 6, 8, 18], [True, False])) @@ -103,25 +101,32 @@ def borrowed_token(tokens_for_vault): @pytest.fixture(scope="module") -def vault(factory, vault_impl, collateral_token, borrowed_token, price_oracle, admin): +def vault_controller_amm(factory, borrowed_token, collateral_token, price_oracle, 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" - ) + return 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" ) @pytest.fixture(scope="module") -def market_controller(vault, controller_interface): - return controller_interface.at(vault.controller()) +def vault(vault_interface, vault_controller_amm): + return vault_interface.at(vault_controller_amm[0]) + + +@pytest.fixture(scope="module") +def market_controller(controller_interface, vault_controller_amm, admin): + controller = controller_interface.at(vault_controller_amm[1]) + with boa.env.prank(admin): + controller.set_borrow_cap(2**256 - 1) + + return controller @pytest.fixture(scope="module") -def market_amm(vault, amm_interface): - return amm_interface.at(vault.amm()) +def market_amm(amm_interface, vault_controller_amm): + return amm_interface.at(vault_controller_amm[2]) @pytest.fixture(scope="module") diff --git a/tests/lending/test_health_calculator_stateful.py b/tests/lending/test_health_calculator_stateful.py index 20188421..1656e722 100644 --- a/tests/lending/test_health_calculator_stateful.py +++ b/tests/lending/test_health_calculator_stateful.py @@ -26,7 +26,7 @@ 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.debt_ceiling = self.controller.borrowed_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) @@ -134,7 +134,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"): diff --git a/tests/lending/test_oracle_attack.py b/tests/lending/test_oracle_attack.py index 112a6bc8..0f8fd8de 100644 --- a/tests/lending/test_oracle_attack.py +++ b/tests/lending/test_oracle_attack.py @@ -39,13 +39,12 @@ def hacker(accounts): @pytest.fixture(scope="module") -def factory_new(factory_partial, stablecoin, amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, gauge_impl, admin): +def factory_new(factory_partial, stablecoin, amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_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) + price_oracle_impl, mpolicy_impl, admin, admin) @pytest.fixture(scope="module") @@ -54,27 +53,26 @@ def amm_old_interface(): @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): +def factory_old(factory_partial, stablecoin, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, amm_old_interface, admin): 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) + price_oracle_impl, mpolicy_impl, admin, admin) @pytest.fixture(scope='module') -def vault_new(factory_new, vault_impl, borrowed_token, collateral_token, price_oracle, admin): +def vault_new(factory_new, vault_interface, borrowed_token, collateral_token, price_oracle, admin): with boa.env.prank(admin): price_oracle.set_price(int(1e18)) - vault = vault_impl.at( + vault = vault_interface.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" - ) + )[0] ) boa.env.time_travel(120) @@ -83,16 +81,16 @@ def vault_new(factory_new, vault_impl, borrowed_token, collateral_token, price_o @pytest.fixture(scope='module') -def vault_old(factory_old, vault_impl, borrowed_token, collateral_token, price_oracle, admin): +def vault_old(factory_old, vault_interface, borrowed_token, collateral_token, price_oracle, admin): with boa.env.prank(admin): price_oracle.set_price(int(1e18)) - vault = vault_impl.at( + vault = vault_interface.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" - ) + )[0] ) boa.env.time_travel(120) @@ -101,8 +99,12 @@ def vault_old(factory_old, vault_impl, borrowed_token, collateral_token, price_o @pytest.fixture(scope='module') -def controller_new(vault_new, controller_interface): - return controller_interface.at(vault_new.controller()) +def controller_new(vault_new, controller_interface, admin): + controller = controller_interface.at(vault_new.controller()) + with boa.env.prank(admin): + controller.set_borrow_cap(2 ** 256 - 1) + + return controller @pytest.fixture(scope='module') @@ -111,8 +113,12 @@ def amm_new(vault_new, amm_interface): @pytest.fixture(scope='module') -def controller_old(vault_old, controller_interface): - return controller_interface.at(vault_old.controller()) +def controller_old(vault_old, controller_interface, admin): + controller = controller_interface.at(vault_old.controller()) + with boa.env.prank(admin): + controller.set_borrow_cap(2 ** 256 - 1) + + return controller @pytest.fixture(scope='module') diff --git a/tests/lending/test_vault.py b/tests/lending/test_vault.py index cffadab0..1b16caa4 100644 --- a/tests/lending/test_vault.py +++ b/tests/lending/test_vault.py @@ -33,10 +33,6 @@ def test_vault_creation(vault, market_controller, market_amm, market_mpolicy, fa 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]) def test_deposit_and_withdraw(vault, borrowed_token, accounts, admin, supply_limit): From 317a8040321b8f5ec399b4a91fd31af5ac41c9dc Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 5 Aug 2025 16:01:17 +0400 Subject: [PATCH 068/413] test: adjust tests for new lending controller --- tests/lending/test_bigfuzz.py | 38 ++++++++++--------- .../lending/test_st_interest_conservation.py | 2 + 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/tests/lending/test_bigfuzz.py b/tests/lending/test_bigfuzz.py index bfe79709..5a8c2c13 100644 --- a/tests/lending/test_bigfuzz.py +++ b/tests/lending/test_bigfuzz.py @@ -132,6 +132,8 @@ def repay(self, ratio, uid): user = self.accounts[uid] debt = self.market_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): @@ -289,8 +291,8 @@ def self_liquidate_and_health(self, emode, frac): self.borrowed_token._mint_for_testing(user, diff) if emode == USE_FRACTION: try: - self.market_controller.liquidate_extended( - user, 0, frac, ZERO_ADDRESS, []) + self.market_controller.liquidate( + user, 0, frac, ZERO_ADDRESS, b'') except Exception: if self.market_controller.debt(user) * frac // 10**18 == 0: return @@ -298,9 +300,9 @@ def self_liquidate_and_health(self, emode, frac): elif emode == USE_CALLBACKS: self.borrowed_token.transfer(self.fake_leverage.address, self.borrowed_token.balanceOf(user)) try: - self.market_controller.liquidate_extended( + self.market_controller.liquidate( user, 0, frac, - self.fake_leverage.address, []) + self.fake_leverage.address, b'') except Exception: if self.market_controller.debt(user) * frac // 10**18 == 0: return @@ -331,13 +333,13 @@ 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, 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( + self.market_controller.liquidate( user, 0, frac, - self.fake_leverage.address, []) + self.fake_leverage.address, b'') else: self.market_controller.liquidate(user, 0) if emode == USE_CALLBACKS: @@ -353,13 +355,13 @@ def liquidate(self, uid, luid, emode, frac): 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, 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( + self.market_controller.liquidate( user, 0, frac, - self.fake_leverage.address, []) + self.fake_leverage.address, b'') else: self.market_controller.liquidate(user, 0) if emode == USE_CALLBACKS: @@ -368,8 +370,8 @@ def liquidate(self, uid, luid, emode, frac): else: if emode == USE_FRACTION: try: - self.market_controller.liquidate_extended( - user, 0, frac, ZERO_ADDRESS, []) + self.market_controller.liquidate( + user, 0, frac, ZERO_ADDRESS, b'') except Exception: if self.market_controller.debt(user) * frac // 10**18 == 0: return @@ -377,9 +379,9 @@ def liquidate(self, uid, luid, emode, frac): elif emode == USE_CALLBACKS: self.borrowed_token.transfer(self.fake_leverage.address, self.borrowed_token.balanceOf(user)) try: - self.market_controller.liquidate_extended( + self.market_controller.liquidate( user, 0, frac, - self.fake_leverage.address, []) + self.fake_leverage.address, b'') except Exception: if self.market_controller.debt(user) * frac // 10**18 == 0: return @@ -419,12 +421,12 @@ def time_travel(self, dt): def debt_supply(self): total_debt = self.market_controller.total_debt() if total_debt == 0: - assert self.market_controller.minted() <= self.market_controller.redeemed() # Paid back more than lent out + assert self.market_controller.lent() <= self.market_controller.repaid() # Paid back more than lent out 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() + assert self.market_controller.repaid() + self.market_controller.total_debt() >= self.market_controller.lent() def test_big_fuzz( diff --git a/tests/lending/test_st_interest_conservation.py b/tests/lending/test_st_interest_conservation.py index 51253805..7a94013d 100644 --- a/tests/lending/test_st_interest_conservation.py +++ b/tests/lending/test_st_interest_conservation.py @@ -97,6 +97,8 @@ def create_loan(self, c_amount, amount, n, user_id): @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) From 7d4987b059b944c55d2b7781f05620d22200fe32 Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 5 Aug 2025 19:31:19 +0200 Subject: [PATCH 069/413] refactor: built llamalend controller on top --- contracts/AMM.vy | 1 + contracts/Controller.vy | 811 --------- contracts/MintController.vy | 1592 +++++++++++++++++ contracts/controller_core.vy | 811 --------- contracts/interfaces/IController.vyi | 327 ---- contracts/interfaces/ILlamalendController.vyi | 35 +- contracts/interfaces/IMintController.vyi | 346 +++- contracts/lending/Controller.vy | 869 --------- contracts/lending/LLController.vy | 260 +++ 9 files changed, 2232 insertions(+), 2820 deletions(-) delete mode 100644 contracts/Controller.vy create mode 100644 contracts/MintController.vy delete mode 100644 contracts/controller_core.vy delete mode 100644 contracts/interfaces/IController.vyi delete mode 100644 contracts/lending/Controller.vy create mode 100644 contracts/lending/LLController.vy diff --git a/contracts/AMM.vy b/contracts/AMM.vy index eaf3adae..9a554c3a 100644 --- a/contracts/AMM.vy +++ b/contracts/AMM.vy @@ -77,6 +77,7 @@ active_band: public(int256) min_band: public(int256) max_band: public(int256) +# TODO setter _price_oracle_contract: immutable(IPriceOracle) # TODO This is a workaround for a compiler bug diff --git a/contracts/Controller.vy b/contracts/Controller.vy deleted file mode 100644 index 2258ce65..00000000 --- a/contracts/Controller.vy +++ /dev/null @@ -1,811 +0,0 @@ -# pragma version 0.4.3 -# pragma nonreentrancy on -# pragma optimize codesize -""" -@title crvUSD Controller -@author Curve.Fi -@license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved -""" - -from ethereum.ercs import IERC20 -from ethereum.ercs import IERC20Detailed -from contracts.interfaces import IAMM -from contracts.interfaces import ILMGauge -from contracts.interfaces import IMonetaryPolicy -from contracts.interfaces import IFactory - -from contracts.interfaces import IController - -implements: IController -from contracts.interfaces import IMintController - -from snekmate.utils import math -from contracts import controller_core as ctrl - -initializes: ctrl - -exports: ( - ctrl.amm, - ctrl.amm_price, - ctrl.approval, - ctrl.approve, - ctrl.borrowed_token, - ctrl.calculate_debt_n1, - ctrl.collateral_token, - ctrl.debt, - ctrl.extra_health, - ctrl.health, - ctrl.health_calculator, - ctrl.liquidation_discount, - ctrl.liquidation_discounts, - ctrl.loan_discount, - ctrl.loan_exists, - ctrl.loan_ix, - ctrl.loans, - ctrl.min_collateral, - ctrl.monetary_policy, - ctrl.n_loans, - ctrl.save_rate, - ctrl.set_extra_health, - ctrl.tokens_to_liquidate, - ctrl.total_debt, - ctrl.user_prices, - ctrl.user_state, - ctrl.users_to_liquidate, -) - -FACTORY: immutable(IFactory) - -minted: public(uint256) -redeemed: public(uint256) - - -@deploy -def __init__( - collateral_token: IERC20, - monetary_policy: IMonetaryPolicy, - loan_discount: uint256, - liquidation_discount: uint256, - amm: IAMM, -): - """ - @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 = IFactory(msg.sender) - - borrowed_token: IERC20 = staticcall FACTORY.stablecoin() - - ctrl.__init__( - amm, - collateral_token, - borrowed_token, - monetary_policy, - loan_discount, - liquidation_discount, - ) - - -@external -@view -def factory() -> address: - """ - @notice Address of the factory - """ - return FACTORY.address - - -@external -@view -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 >= ctrl.MIN_TICKS_UINT and N <= ctrl.MAX_TICKS_UINT - - y_effective: uint256 = ctrl.get_y_effective( - collateral * ctrl.COLLATERAL_PRECISION, - N, - ctrl.loan_discount + ctrl.extra_health[user], - ) - - x: uint256 = unsafe_sub( - max(unsafe_div(y_effective * ctrl.max_p_base(), 10**18), 1), 1 - ) - x = unsafe_div( - x * (10**18 - 10**14), unsafe_mul(10**18, ctrl.BORROWED_PRECISION) - ) # Make it a bit smaller - # TODO need to port cap? - return min( - x, staticcall ctrl.BORROWED_TOKEN.balanceOf(self) + current_debt - ) # Cannot borrow beyond the amount of coins Controller has - - -@internal -def _create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address): - assert ctrl.loan[_for].initial_debt == 0, "Loan already created" - assert N > ctrl.MIN_TICKS_UINT - 1, "Need more ticks" - assert N < ctrl.MAX_TICKS_UINT + 1, "Need less ticks" - - n1: int256 = ctrl._calculate_debt_n1(collateral, debt, N, _for) - n2: int256 = n1 + convert(unsafe_sub(N, 1), int256) - - rate_mul: uint256 = staticcall ctrl.AMM.get_rate_mul() - ctrl.loan[_for] = IController.Loan(initial_debt=debt, rate_mul=rate_mul) - liquidation_discount: uint256 = ctrl.liquidation_discount - ctrl.liquidation_discounts[_for] = liquidation_discount - - n_loans: uint256 = ctrl.n_loans - ctrl.loans[n_loans] = _for - ctrl.loan_ix[_for] = n_loans - ctrl.n_loans = unsafe_add(n_loans, 1) - - ctrl._total_debt.initial_debt = ( - ctrl._total_debt.initial_debt * rate_mul // ctrl._total_debt.rate_mul - + debt - ) - ctrl._total_debt.rate_mul = rate_mul - - extcall ctrl.AMM.deposit_range(_for, collateral, n1, n2) - self.minted += debt - - ctrl._save_rate() - - log IController.UserState( - user=_for, - collateral=collateral, - debt=debt, - n1=n1, - n2=n2, - liquidation_discount=liquidation_discount, - ) - log IController.Borrow( - user=_for, collateral_increase=collateral, loan_increase=debt - ) - - -@external -def create_loan( - collateral: uint256, - debt: uint256, - N: uint256, - _for: address = msg.sender, - callbacker: address = empty(address), - calldata: Bytes[10**4] = b"", -): - """ - @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 _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 ctrl._check_approval(_for) - - more_collateral: uint256 = 0 - if callbacker != empty(address): - ctrl.transfer(ctrl.BORROWED_TOKEN, callbacker, debt) - # If there is any unused debt, callbacker can send it to the user - more_collateral = ctrl.execute_callback( - callbacker, - ctrl.CALLBACK_DEPOSIT, - _for, - 0, - collateral, - debt, - calldata, - ).collateral - - self._create_loan(collateral + more_collateral, debt, N, _for) - - ctrl.transferFrom( - ctrl.COLLATERAL_TOKEN, msg.sender, ctrl.AMM.address, collateral - ) - if more_collateral > 0: - ctrl.transferFrom( - ctrl.COLLATERAL_TOKEN, callbacker, ctrl.AMM.address, more_collateral - ) - if callbacker == empty(address): - ctrl.transfer(ctrl.BORROWED_TOKEN, _for, debt) - - -@internal -def _add_collateral_borrow( - d_collateral: uint256, - d_debt: uint256, - _for: address, - remove_collateral: bool, - check_rounding: bool, -): - """ - @notice Internal method to borrow and add or remove collateral - @param d_collateral Amount of collateral to add - @param d_debt Amount of debt increase - @param _for Address to transfer tokens to - @param remove_collateral Remove collateral instead of adding - @param check_rounding Check that amount added is no less than the rounding error on the loan - """ - debt: uint256 = 0 - rate_mul: uint256 = 0 - debt, rate_mul = ctrl._debt(_for) - assert debt > 0, "Loan doesn't exist" - debt += d_debt - - xy: uint256[2] = extcall ctrl.AMM.withdraw(_for, 10**18) - assert xy[0] == 0, "Already in underwater mode" - if remove_collateral: - xy[1] -= d_collateral - else: - xy[1] += d_collateral - if check_rounding: - # We need d(x + p*y) > 1 wei. For that, we do an equivalent check (but with x2 for safety) - # 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 * staticcall ctrl.AMM.price_oracle() - > 2 - * 10**18 - * ctrl.BORROWED_PRECISION // ctrl.COLLATERAL_PRECISION - ) - ns: int256[2] = staticcall ctrl.AMM.read_user_tick_numbers(_for) - size: uint256 = convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256) - n1: int256 = ctrl._calculate_debt_n1(xy[1], debt, size, _for) - n2: int256 = n1 + unsafe_sub(ns[1], ns[0]) - - extcall ctrl.AMM.deposit_range(_for, xy[1], n1, n2) - ctrl.loan[_for] = IController.Loan(initial_debt=debt, rate_mul=rate_mul) - - liquidation_discount: uint256 = 0 - if _for == msg.sender: - liquidation_discount = ctrl.liquidation_discount - ctrl.liquidation_discounts[_for] = liquidation_discount - else: - liquidation_discount = ctrl.liquidation_discounts[_for] - - if d_debt != 0: - ctrl._total_debt.initial_debt = ( - ctrl._total_debt.initial_debt - * rate_mul // ctrl._total_debt.rate_mul - + d_debt - ) - ctrl._total_debt.rate_mul = rate_mul - - if remove_collateral: - log IController.RemoveCollateral( - user=_for, collateral_decrease=d_collateral - ) - else: - 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, - ) - - -@external -def add_collateral(collateral: uint256, _for: address = msg.sender): - """ - @notice Add extra collateral to avoid bad liqidations - @param collateral Amount of collateral to add - @param _for Address to add collateral for - """ - if collateral == 0: - return - self._add_collateral_borrow(collateral, 0, _for, False, _for != msg.sender) - ctrl.transferFrom( - ctrl.COLLATERAL_TOKEN, msg.sender, ctrl.AMM.address, collateral - ) - ctrl._save_rate() - - -@external -def remove_collateral(collateral: uint256, _for: address = msg.sender): - """ - @notice Remove some collateral without repaying the debt - @param collateral Amount of collateral to remove - @param _for Address to remove collateral for - """ - if collateral == 0: - return - assert ctrl._check_approval(_for) - self._add_collateral_borrow(collateral, 0, _for, True, False) - ctrl.transferFrom(ctrl.COLLATERAL_TOKEN, ctrl.AMM.address, _for, collateral) - ctrl._save_rate() - - -@external -def borrow_more( - collateral: uint256, - debt: uint256, - _for: address = msg.sender, - callbacker: address = empty(address), - calldata: Bytes[10**4] = b"", -): - """ - @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 _for Address to borrow for - @param callbacker Address of the callback contract - @param calldata Any data for callbacker - """ - if debt == 0: - return - assert ctrl._check_approval(_for) - - more_collateral: uint256 = 0 - if callbacker != empty(address): - ctrl.transfer(ctrl.BORROWED_TOKEN, callbacker, debt) - # If there is any unused debt, callbacker can send it to the user - more_collateral = ctrl.execute_callback( - callbacker, - ctrl.CALLBACK_DEPOSIT, - _for, - 0, - collateral, - debt, - calldata, - ).collateral - - self._add_collateral_borrow( - collateral + more_collateral, debt, _for, False, False - ) - self.minted += debt - - ctrl.transferFrom( - ctrl.COLLATERAL_TOKEN, msg.sender, ctrl.AMM.address, collateral - ) - if more_collateral > 0: - ctrl.transferFrom( - ctrl.COLLATERAL_TOKEN, callbacker, ctrl.AMM.address, more_collateral - ) - if callbacker == empty(address): - ctrl.transfer(ctrl.BORROWED_TOKEN, _for, debt) - ctrl._save_rate() - - -@external -def repay( - _d_debt: uint256, - _for: address = msg.sender, - max_active_band: int256 = max_value(int256), - callbacker: address = empty(address), - calldata: Bytes[10**4] = b"", -): - """ - @notice Repay debt (partially or fully) - @param _d_debt The amount of debt to repay from user's wallet. 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) - @param callbacker Address of the callback contract - @param calldata Any data for callbacker - """ - debt: uint256 = 0 - rate_mul: uint256 = 0 - debt, rate_mul = ctrl._debt(_for) - assert debt > 0, "Loan doesn't exist" - approval: bool = ctrl._check_approval(_for) - xy: uint256[2] = empty(uint256[2]) - - cb: IController.CallbackData = empty(IController.CallbackData) - if callbacker != empty(address): - assert approval - xy = extcall ctrl.AMM.withdraw(_for, 10**18) - ctrl.transferFrom( - ctrl.COLLATERAL_TOKEN, ctrl.AMM.address, callbacker, xy[1] - ) - cb = ctrl.execute_callback( - callbacker, ctrl.CALLBACK_REPAY, _for, xy[0], xy[1], debt, calldata - ) - - total_stablecoins: uint256 = _d_debt + xy[0] + cb.stablecoins - assert total_stablecoins > 0 # dev: no coins to repay - 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 - if callbacker == empty(address): - xy = extcall ctrl.AMM.withdraw(_for, 10**18) - - if xy[0] > 0: - # Only allow full repayment when underwater for the sender to do - assert approval - ctrl.transferFrom( - ctrl.BORROWED_TOKEN, ctrl.AMM.address, self, xy[0] - ) - if cb.stablecoins > 0: - ctrl.transferFrom( - ctrl.BORROWED_TOKEN, callbacker, self, cb.stablecoins - ) - if _d_debt > 0: - ctrl.transferFrom(ctrl.BORROWED_TOKEN, msg.sender, self, _d_debt) - - if total_stablecoins > d_debt: - ctrl.transfer( - ctrl.BORROWED_TOKEN, _for, unsafe_sub(total_stablecoins, d_debt) - ) - # Transfer collateral to _for - if callbacker == empty(address): - if xy[1] > 0: - ctrl.transferFrom( - ctrl.COLLATERAL_TOKEN, ctrl.AMM.address, _for, xy[1] - ) - else: - if cb.collateral > 0: - ctrl.transferFrom( - ctrl.COLLATERAL_TOKEN, callbacker, _for, cb.collateral - ) - ctrl._remove_from_list(_for) - 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=d_debt - ) - # Else - partial repayment - else: - active_band: int256 = staticcall ctrl.AMM.active_band_with_skip() - assert active_band <= max_active_band - - d_debt = total_stablecoins - debt = unsafe_sub(debt, d_debt) - ns: int256[2] = staticcall ctrl.AMM.read_user_tick_numbers(_for) - size: int256 = unsafe_sub(ns[1], ns[0]) - liquidation_discount: uint256 = ctrl.liquidation_discounts[_for] - - if ns[0] > active_band: - # Not in soft-liquidation - can use callback and move bands - new_collateral: uint256 = cb.collateral - if callbacker == empty(address): - xy = extcall ctrl.AMM.withdraw(_for, 10**18) - new_collateral = xy[1] - ns[0] = ctrl._calculate_debt_n1( - new_collateral, - debt, - convert(unsafe_add(size, 1), uint256), - _for, - ) - ns[1] = ns[0] + size - extcall ctrl.AMM.deposit_range(_for, new_collateral, ns[0], ns[1]) - else: - # Underwater - cannot use callback or move bands but can avoid a bad liquidation - xy = staticcall ctrl.AMM.get_sum_xy(_for) - assert callbacker == empty(address) - - if approval: - # Update liquidation discount only if we are that same user. No rugs - liquidation_discount = ctrl.liquidation_discount - ctrl.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 ctrl._health(_for, debt, False, liquidation_discount) > 0 - - if cb.stablecoins > 0: - ctrl.transferFrom( - ctrl.BORROWED_TOKEN, callbacker, self, cb.stablecoins - ) - if _d_debt > 0: - ctrl.transferFrom(ctrl.BORROWED_TOKEN, msg.sender, self, _d_debt) - - log IController.UserState( - user=_for, - collateral=xy[1], - debt=debt, - n1=ns[0], - n2=ns[1], - liquidation_discount=liquidation_discount, - ) - log IController.Repay( - user=_for, collateral_decrease=0, loan_decrease=d_debt - ) - - self.redeemed += d_debt - - ctrl.loan[_for] = IController.Loan(initial_debt=debt, rate_mul=rate_mul) - total_debt: uint256 = ( - ctrl._total_debt.initial_debt * rate_mul // ctrl._total_debt.rate_mul - ) - ctrl._total_debt.initial_debt = unsafe_sub(max(total_debt, d_debt), d_debt) - ctrl._total_debt.rate_mul = rate_mul - - ctrl._save_rate() - - -@internal -def _liquidate( - user: address, - min_x: uint256, - health_limit: uint256, - frac: uint256, - callbacker: address, - calldata: Bytes[10**4], -): - """ - @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 - @param callbacker Address of the callback contract - @param calldata Any data for callbacker - """ - debt: uint256 = 0 - rate_mul: uint256 = 0 - debt, rate_mul = ctrl._debt(user) - - if health_limit != 0: - assert ( - ctrl._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 debt > 0 - final_debt = unsafe_sub(final_debt, debt) - - # Withdraw sender's stablecoin 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] = extcall ctrl.AMM.withdraw( - user, ctrl._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) - ctrl.transferFrom(ctrl.BORROWED_TOKEN, ctrl.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 - ctrl.transferFrom( - ctrl.COLLATERAL_TOKEN, ctrl.AMM.address, msg.sender, xy[1] - ) - # Request what's left from user - ctrl.transferFrom(ctrl.BORROWED_TOKEN, msg.sender, self, to_repay) - - else: - # Move collateral to callbacker, call it and remove everything from it back in - ctrl.transferFrom( - ctrl.COLLATERAL_TOKEN, ctrl.AMM.address, callbacker, xy[1] - ) - # Callback - cb: IController.CallbackData = ctrl.execute_callback( - callbacker, - ctrl.CALLBACK_LIQUIDATE, - user, - xy[0], - xy[1], - debt, - calldata, - ) - assert cb.stablecoins >= to_repay, "not enough proceeds" - if cb.stablecoins > to_repay: - ctrl.transferFrom( - ctrl.BORROWED_TOKEN, - callbacker, - msg.sender, - unsafe_sub(cb.stablecoins, to_repay), - ) - ctrl.transferFrom(ctrl.BORROWED_TOKEN, callbacker, self, to_repay) - ctrl.transferFrom( - ctrl.COLLATERAL_TOKEN, callbacker, msg.sender, cb.collateral - ) - else: - # Withdraw collateral - ctrl.transferFrom( - ctrl.COLLATERAL_TOKEN, ctrl.AMM.address, msg.sender, xy[1] - ) - # Return what's left to user - if xy[0] > debt: - ctrl.transferFrom( - ctrl.BORROWED_TOKEN, - ctrl.AMM.address, - msg.sender, - unsafe_sub(xy[0], debt), - ) - self.redeemed += debt - ctrl.loan[user] = IController.Loan( - initial_debt=final_debt, rate_mul=rate_mul - ) - log IController.Repay( - user=user, collateral_decrease=xy[1], loan_decrease=debt - ) - log IController.Liquidate( - liquidator=msg.sender, - user=user, - collateral_received=xy[1], - stablecoin_received=xy[0], - debt=debt, - ) - if final_debt == 0: - 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 - ctrl._remove_from_list(user) - - d: uint256 = ( - ctrl._total_debt.initial_debt * rate_mul // ctrl._total_debt.rate_mul - ) - ctrl._total_debt.initial_debt = unsafe_sub(max(d, debt), debt) - ctrl._total_debt.rate_mul = rate_mul - - ctrl._save_rate() - - -@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 (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 calldata Any data for callbacker - """ - discount: uint256 = 0 - if not ctrl._check_approval(user): - discount = ctrl.liquidation_discounts[user] - self._liquidate( - user, min_x, discount, min(frac, 10**18), callbacker, calldata - ) - - -@external -@reentrant -def set_amm_fee(fee: uint256): - """ - @notice Set the AMM fee (factory admin only) - @dev Reentrant because AMM is nonreentrant TODO check this one - @param fee The fee which should be no higher than MAX_AMM_FEE - """ - assert msg.sender == staticcall FACTORY.admin() - assert fee <= ctrl.MAX_AMM_FEE and fee >= ctrl.MIN_AMM_FEE, "Fee" - extcall ctrl.AMM.set_fee(fee) - - -@external -def set_monetary_policy(monetary_policy: address): - """ - @notice Set monetary policy contract - @param monetary_policy Address of the monetary policy contract - """ - assert msg.sender == staticcall FACTORY.admin() - ctrl._monetary_policy = IMonetaryPolicy(monetary_policy) - extcall IMonetaryPolicy(monetary_policy).rate_write() - log IController.SetMonetaryPolicy(monetary_policy=monetary_policy) - - -@external -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 == staticcall FACTORY.admin() - assert loan_discount > liquidation_discount - assert liquidation_discount >= ctrl.MIN_LIQUIDATION_DISCOUNT - assert loan_discount <= ctrl.MAX_LOAN_DISCOUNT - ctrl.liquidation_discount = liquidation_discount - ctrl.loan_discount = loan_discount - log IController.SetBorrowingDiscounts( - loan_discount=loan_discount, liquidation_discount=liquidation_discount - ) - - -@external -def set_callback(cb: ILMGauge): - """ - @notice Set liquidity mining callback - """ - assert msg.sender == staticcall FACTORY.admin() - extcall ctrl.AMM.set_callback(cb) - log IController.SetLMCallback(callback=cb) - - -@external -@view -def admin_fees() -> uint256: - """ - @notice Calculate the amount of fees obtained from the interest - """ - minted: uint256 = self.minted - return unsafe_sub( - max(ctrl._get_total_debt() + self.redeemed, minted), minted - ) - - -@external -def collect_fees() -> uint256: - """ - @notice Collect the fees charged as interest. - """ - _to: address = staticcall FACTORY.fee_receiver() - - # Borrowing-based fees - rate_mul: uint256 = staticcall ctrl.AMM.get_rate_mul() - loan: IController.Loan = ctrl._total_debt - loan.initial_debt = loan.initial_debt * rate_mul // loan.rate_mul - loan.rate_mul = rate_mul - ctrl._total_debt = loan - - ctrl._save_rate() - - # 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 - ctrl.transfer(ctrl.BORROWED_TOKEN, _to, to_be_redeemed) - log IController.CollectFees( - amount=to_be_redeemed, new_supply=loan.initial_debt - ) - return to_be_redeemed - else: - log IController.CollectFees(amount=0, new_supply=loan.initial_debt) - return 0 diff --git a/contracts/MintController.vy b/contracts/MintController.vy new file mode 100644 index 00000000..ea642953 --- /dev/null +++ b/contracts/MintController.vy @@ -0,0 +1,1592 @@ +# pragma version 0.4.3 +# pragma nonreentrancy on +# pragma optimize codesize +""" +@title crvUSD Controller +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved +""" + +from contracts.interfaces import IAMM +from contracts.interfaces import IMonetaryPolicy +from contracts.interfaces import ILMGauge +from contracts.interfaces import IFactory +from ethereum.ercs import IERC20 +from ethereum.ercs import IERC20Detailed + +from contracts.interfaces import IMintController as IController + +implements: IController + +from snekmate.utils import math + +################################################################ +# IMMUTABLES # +################################################################ + +AMM: immutable(IAMM) +MAX_AMM_FEE: immutable( + uint256 +) # let's set to MIN_TICKS / A: for example, 4% max fee for A=100 +A: immutable(uint256) +Aminus1: immutable(uint256) +LOGN_A_RATIO: immutable(int256) # log(A / (A - 1)) +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 # +################################################################ + +# TODO add version + + +from contracts import constants as c + +WAD: constant(uint256) = c.WAD +DEAD_SHARES: constant(uint256) = c.DEAD_SHARES + +MIN_AMM_FEE: constant(uint256) = 10**6 # 1e-12, still needs to be above 0 +MIN_TICKS_UINT: constant(uint256) = 4 + +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, +) + +MAX_LOAN_DISCOUNT: constant(uint256) = 5 * 10**17 +MIN_LIQUIDATION_DISCOUNT: constant(uint256) = ( + 10**16 +) # Start liquidating when threshold reached +MAX_TICKS: constant(int256) = 50 +MAX_TICKS_UINT: constant(uint256) = c.MAX_TICKS_UINT +MIN_TICKS: constant(int256) = 4 +MAX_SKIP_TICKS: constant(uint256) = 1024 +MAX_P_BASE_BANDS: constant(int256) = 5 + +MAX_RATE: constant(uint256) = 43959106799 # 300% APY + +################################################################ +# STORAGE # +################################################################ + +liquidation_discount: public(uint256) +loan_discount: public(uint256) +# TODO make settable +_monetary_policy: IMonetaryPolicy +# TODO can't mark it as public, likely a compiler bug +# TODO make an issue +@external +@view +def monetary_policy() -> IMonetaryPolicy: + """ + @notice Address of the monetary policy + """ + return self._monetary_policy + + +approval: public(HashMap[address, HashMap[address, bool]]) +extra_health: public(HashMap[address, uint256]) + +loan: HashMap[address, IController.Loan] +liquidation_discounts: public(HashMap[address, uint256]) +_total_debt: IController.Loan + +# TODO uniform comment style + +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 + +# cumulative amount of assets ever repaid (including admin fees) +repaid: public(uint256) +# cumulative amount of assets admin fees have been taken from +processed: public(uint256) + +# unused for mint controller as it overlaps with debt ceiling +borrow_cap: uint256 +# left uninitialized at zero in mint markets +admin_fee: public(uint256) + + +@deploy +def __init__( + _AMM: IAMM, + _collateral_token: IERC20, + _borrowed_token: IERC20, + monetary_policy: IMonetaryPolicy, + loan_discount: uint256, + liquidation_discount: uint256, +): + # In MintController the correct way to limit borrowing + # is through the debt ceiling. + self.borrow_cap = max_value(uint256) + + FACTORY = IFactory(msg.sender) + AMM = _AMM + + A = staticcall AMM.A() + Aminus1 = A - 1 + + # TODO check math (removed unsafe) + LOGN_A_RATIO = math._wad_ln(convert(A * WAD // A - 1, int256)) + # TODO check math + SQRT_BAND_RATIO = isqrt(10**36 * A // (A - 1)) + + MAX_AMM_FEE = min(WAD * MIN_TICKS_UINT // A, 10**17) + + COLLATERAL_TOKEN = _collateral_token + collateral_decimals: uint256 = convert( + staticcall IERC20Detailed(COLLATERAL_TOKEN.address).decimals(), uint256 + ) + COLLATERAL_PRECISION = pow_mod256(10, 18 - collateral_decimals) + + BORROWED_TOKEN = _borrowed_token + borrowed_decimals: uint256 = convert( + staticcall IERC20Detailed(BORROWED_TOKEN.address).decimals(), uint256 + ) + BORROWED_PRECISION = pow_mod256(10, 18 - borrowed_decimals) + + self._monetary_policy = monetary_policy + self.liquidation_discount = liquidation_discount + self.loan_discount = loan_discount + self._total_debt.rate_mul = 10**18 + + # TODO check what this is needed for + assert extcall BORROWED_TOKEN.approve( + msg.sender, max_value(uint256), default_return_value=True + ) + + +@view +@external +def minted() -> uint256: + return self.processed + + +@view +@external +def redeemed() -> uint256: + return self.repaid + + +@external +@view +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 + """ + # Cannot borrow beyond the amount of coins Controller has + cap: uint256 = staticcall BORROWED_TOKEN.balanceOf(self) + current_debt + + return self._max_borrowable( + collateral, + N, + cap, + current_debt, + user, + ) + + +################################################################ +# BUILDING BLOCKS # +################################################################ + + +@internal +@view +def _debt(user: address) -> (uint256, uint256): + """ + @notice Get the value of debt and rate_mul and update the rate_mul counter + @param user User address + @return (debt, rate_mul) + """ + rate_mul: uint256 = staticcall AMM.get_rate_mul() + loan: IController.Loan = self.loan[user] + if loan.initial_debt == 0: + return (0, rate_mul) + else: + # Let user repay 1 smallest decimal more so that the system doesn't lose on precision + # Use ceil div + debt: uint256 = loan.initial_debt * rate_mul + 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 + return (debt, rate_mul) + + +@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 +@view +def get_y_effective( + collateral: uint256, N: uint256, discount: uint256 +) -> uint256: + """ + @notice Intermediary method which calculates y_effective defined as x_effective / p_base, + however discounted by loan_discount. + x_effective is an amount which can be obtained from collateral when liquidating + @param collateral Amount of collateral to get the value for + @param N Number of bands the deposit is made into + @param discount Loan discount at 1e18 base (e.g. 1e18 == 100%) + @return y_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_effective * p_oracle_up(n1) + # d_y_effective = y / N / sqrt(A / (A - 1)) + # 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, + ), + ), + unsafe_mul(SQRT_BAND_RATIO, N), + ) + y_effective: uint256 = d_y_effective + for i: uint256 in range(1, MAX_TICKS_UINT): + if i == N: + break + d_y_effective = unsafe_div(d_y_effective * Aminus1, 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: + """ + @notice Calculate the upper band number for the deposit to sit in to support + the given debt. 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 + @return Upper band n1 (n1 <= n2) to deposit into. Signed integer + """ + assert debt > 0, "No loan" + 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], + ) + # p_oracle_up(n1) = base_price * ((A - 1) / A)**n1 + + # We borrow up until min band touches p_oracle, + # or it touches non-empty bands which cannot be skipped. + # We calculate required n1 for given (collateral, debt), + # and if n1 corresponds to price_oracle being too high, or unreachable band + # - 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 + + # 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 = 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_div(n1, LOGN_A_RATIO) + + n1 = min(n1, 1024 - convert(N, int256)) + n0 + if n1 <= n0: + assert staticcall AMM.can_skip_bands(n1 - 1), "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 = 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() * 10**18 // 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 = unsafe_div(p_base * A, Aminus1) + if p_base > p_oracle: + return p_base_prev + return p_base + + +@internal +@view +def _check_approval(_for: address) -> bool: + return msg.sender == _for or self.approval[_for][msg.sender] + + +@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 + ) + + return f_remove + + +@internal +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 + self.loan_ix[_for] = 0 + if loan_ix < last_loan_ix: # Need to replace + last_loan: address = self.loans[last_loan_ix] + self.loans[loan_ix] = last_loan + self.loan_ix[last_loan] = loan_ix + self.n_loans = last_loan_ix + + +@internal +def transferFrom(token: IERC20, _from: address, _to: address, amount: uint256): + if amount > 0: + assert extcall token.transferFrom( + _from, _to, amount, default_return_value=True + ) + + +@internal +def transfer(token: IERC20, _to: address, amount: uint256): + if amount > 0: + assert extcall token.transfer(_to, amount, default_return_value=True) + + +@internal +@view +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 + @param user User address to calculate health for + @param debt The amount of debt to calculate health for + @param full Whether to take into account the price difference above the highest user's band + @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(staticcall AMM.get_x_down(user), int256) * health, + convert(debt, int256), + ) + - 10**18 + ) + + if full: + 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) + * (staticcall AMM.get_sum_xy(user))[1] + * COLLATERAL_PRECISION, + debt * BORROWED_PRECISION, + ), + int256, + ) + return health + + +@internal +def _save_rate(): + """ + @notice Save current rate + """ + rate: uint256 = min(extcall self._monetary_policy.rate_write(), MAX_RATE) + extcall AMM.set_rate(rate) + + +@internal +def execute_callback( + callbacker: address, + callback_sig: bytes4, + user: address, + stablecoins: uint256, + collateral: uint256, + debt: uint256, + calldata: Bytes[10**4], +) -> IController.CallbackData: + assert callbacker != COLLATERAL_TOKEN.address + assert callbacker != BORROWED_TOKEN.address + + 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, calldata), + ), + max_outsize=64, + ) + data.stablecoins = convert(slice(response, 0, 32), uint256) + data.collateral = convert(slice(response, 32, 32), uint256) + + # Checks after callback + 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 +@view +def _check_admin(): + assert msg.sender == staticcall FACTORY.admin(), "only admin" + + +@external +def liquidate( + user: address, + min_x: uint256, + _frac: uint256, + callbacker: address, + calldata: Bytes[10**4], +): + """ + @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 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" + + final_debt: uint256 = debt + # TODO shouldn't clamp max + frac: uint256 = min(_frac, 10**18) + # TODO use wads + debt = unsafe_div(debt * frac + (10**18 - 1), 10**18) + assert debt > 0 + final_debt = unsafe_sub(final_debt, debt) + + # Withdraw sender's stablecoin 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] = 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) + + 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]) + # Request what's left from user + self.transferFrom(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]) + # Callback + cb: IController.CallbackData = self.execute_callback( + callbacker, + CALLBACK_LIQUIDATE, + user, + xy[0], + xy[1], + debt, + calldata, + ) + 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 + ) + else: + # Withdraw collateral + self.transferFrom(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), + ) + self.loan[user] = IController.Loan( + initial_debt=final_debt, rate_mul=rate_mul + ) + log IController.Repay( + user=user, collateral_decrease=xy[1], loan_decrease=debt + ) + log IController.Liquidate( + liquidator=msg.sender, + user=user, + collateral_received=xy[1], + stablecoin_received=xy[0], + debt=debt, + ) + if final_debt == 0: + 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) + + self._update_total_debt(debt, rate_mul, False) + + self.repaid += debt + self._save_rate() + + +@internal +def _add_collateral_borrow( + d_collateral: uint256, + d_debt: uint256, + _for: address, + remove_collateral: bool, + check_rounding: bool, +): + """ + @notice Internal method to borrow and add or remove collateral + @param d_collateral Amount of collateral to add + @param d_debt Amount of debt increase + @param _for Address to transfer tokens to + @param remove_collateral Remove collateral instead of adding + @param check_rounding Check that amount added is no less than the rounding error on the loan + """ + debt: uint256 = 0 + rate_mul: uint256 = 0 + debt, rate_mul = self._debt(_for) + assert debt > 0, "Loan doesn't exist" + debt += d_debt + + xy: uint256[2] = extcall AMM.withdraw(_for, 10**18) + assert xy[0] == 0, "Already in underwater mode" + if remove_collateral: + xy[1] -= d_collateral + else: + xy[1] += d_collateral + if check_rounding: + # We need d(x + p*y) > 1 wei. For that, we do an equivalent check (but with x2 for safety) + # 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 * staticcall AMM.price_oracle() + > 2 * 10**18 * 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]) + + 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: + liquidation_discount = self.liquidation_discount + self.liquidation_discounts[_for] = liquidation_discount + else: + liquidation_discount = self.liquidation_discounts[_for] + + if d_debt != 0: + self._update_total_debt(d_debt, rate_mul, True) + + if remove_collateral: + log IController.RemoveCollateral( + user=_for, collateral_decrease=d_collateral + ) + else: + 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, + ) + + +@external +def borrow_more( + collateral: uint256, + debt: uint256, + _for: address = msg.sender, + callbacker: address = empty(address), + calldata: Bytes[10**4] = b"", +): + """ + @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 _for Address to borrow for + @param callbacker Address of the callback contract + @param calldata Any data for callbacker + """ + _debt: uint256 = self._borrow_more( + collateral, + debt, + _for, + callbacker, + calldata, + ) + + +@internal +def _borrow_more( + collateral: uint256, + debt: uint256, + _for: address = msg.sender, + callbacker: address = empty(address), + calldata: Bytes[10**4] = b"", +) -> uint256: + if debt == 0: + return 0 + assert self._check_approval(_for) + + more_collateral: uint256 = 0 + if callbacker != empty(address): + self.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 + + self._add_collateral_borrow( + collateral + more_collateral, debt, _for, False, False + ) + + self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) + if more_collateral > 0: + self.transferFrom( + COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral + ) + if callbacker == empty(address): + self.transfer(BORROWED_TOKEN, _for, debt) + + self.processed += debt + self._save_rate() + + return debt + + +@internal +def _update_total_debt( + d_debt: uint256, rate_mul: uint256, is_increase: bool +) -> IController.Loan: + """ + @param d_debt Change in debt amount (unsigned) + @param rate_mul New rate_mul + @param is_increase Whether debt increases or decreases + @notice Update total debt of this controller + """ + loan: IController.Loan = self._total_debt + loan.initial_debt = loan.initial_debt * rate_mul // loan.rate_mul + if is_increase: + loan.initial_debt += d_debt + assert loan.initial_debt <= self.borrow_cap, "Borrow cap exceeded" + else: + loan.initial_debt = unsafe_sub(max(loan.initial_debt, d_debt), d_debt) + loan.rate_mul = rate_mul + self._total_debt = loan + + return loan + + +@internal +@view +def _max_borrowable( + collateral: uint256, + N: uint256, + cap: uint256, + current_debt: uint256 = 0, + user: address = empty(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(), 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, cap) + + +@external +def set_callback(cb: ILMGauge): + """ + @notice Set liquidity mining callback + """ + self._check_admin() + extcall AMM.set_callback(cb) + log IController.SetLMCallback(callback=cb) + + +@external +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 + """ + 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 IController.SetBorrowingDiscounts( + loan_discount=loan_discount, liquidation_discount=liquidation_discount + ) + + +@external +def set_monetary_policy(monetary_policy: IMonetaryPolicy): + """ + @notice Set monetary policy contract + @param monetary_policy Address of the monetary policy contract + """ + self._check_admin() + self._monetary_policy = monetary_policy + extcall monetary_policy.rate_write() + log IController.SetMonetaryPolicy(monetary_policy=monetary_policy) + + +@external +def create_loan( + collateral: uint256, + debt: uint256, + N: uint256, + _for: address = msg.sender, + callbacker: address = empty(address), + calldata: Bytes[10**4] = b"", +): + """ + @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 _for Address to create the loan for + @param callbacker Address of the callback contract + @param calldata Any data for callbacker + """ + _debt: uint256 = self._create_loan( + collateral, debt, N, _for, callbacker, calldata + ) + + +@internal +def _create_loan( + collateral: uint256, + debt: uint256, + N: uint256, + _for: address, + callbacker: address = empty(address), + calldata: Bytes[10**4] = b"", +) -> uint256: + 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) + + more_collateral: uint256 = 0 + if callbacker != empty(address): + self.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 + + collateral = collateral + more_collateral + + 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" + + n1: int256 = self._calculate_debt_n1(collateral, debt, N, _for) + n2: int256 = n1 + convert(unsafe_sub(N, 1), int256) + + 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 + + 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._update_total_debt(debt, rate_mul, True) + + extcall AMM.deposit_range(_for, collateral, n1, n2) + + self.processed += debt + self._save_rate() + + log IController.UserState( + user=_for, + collateral=collateral, + debt=debt, + n1=n1, + n2=n2, + liquidation_discount=liquidation_discount, + ) + log IController.Borrow( + user=_for, collateral_increase=collateral, loan_increase=debt + ) + + self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) + if more_collateral > 0: + self.transferFrom( + COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral + ) + if callbacker == empty(address): + self.transfer(BORROWED_TOKEN, _for, debt) + + return debt + + +@external +@reentrant +def set_amm_fee(fee: uint256): + """ + @notice Set the AMM fee (factory admin only) + @dev Reentrant because AMM is nonreentrant TODO check this one + @param fee The fee which should be no higher than MAX_AMM_FEE + """ + self._check_admin() + assert fee <= MAX_AMM_FEE and fee >= MIN_AMM_FEE, "Fee" + extcall AMM.set_fee(fee) + + +@external +def repay( + _d_debt: uint256, + _for: address = msg.sender, + max_active_band: int256 = max_value(int256), + callbacker: address = empty(address), + calldata: Bytes[10**4] = b"", +): + """ + @notice Repay debt (partially or fully) + @param _d_debt The amount of debt to repay from user's wallet. 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) + @param callbacker Address of the callback contract + @param calldata Any data for callbacker + """ + debt: uint256 = 0 + rate_mul: uint256 = 0 + debt, rate_mul = self._debt(_for) + assert debt > 0, "Loan doesn't exist" + approval: bool = self._check_approval(_for) + xy: uint256[2] = empty(uint256[2]) + + cb: IController.CallbackData = empty(IController.CallbackData) + if callbacker != empty(address): + assert approval + xy = extcall AMM.withdraw(_for, 10**18) + self.transferFrom(COLLATERAL_TOKEN, AMM.address, callbacker, xy[1]) + cb = self.execute_callback( + callbacker, CALLBACK_REPAY, _for, xy[0], xy[1], debt, calldata + ) + + total_stablecoins: uint256 = _d_debt + xy[0] + cb.stablecoins + assert total_stablecoins > 0 # dev: no coins to repay + 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 + if callbacker == empty(address): + xy = extcall 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, self, xy[0]) + if cb.stablecoins > 0: + self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins) + if _d_debt > 0: + self.transferFrom(BORROWED_TOKEN, msg.sender, self, _d_debt) + + if total_stablecoins > d_debt: + self.transfer( + BORROWED_TOKEN, _for, unsafe_sub(total_stablecoins, d_debt) + ) + # Transfer collateral to _for + if callbacker == empty(address): + if xy[1] > 0: + self.transferFrom(COLLATERAL_TOKEN, AMM.address, _for, xy[1]) + else: + if cb.collateral > 0: + self.transferFrom( + COLLATERAL_TOKEN, callbacker, _for, cb.collateral + ) + self._remove_from_list(_for) + 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=d_debt + ) + # Else - partial repayment + else: + active_band: int256 = staticcall AMM.active_band_with_skip() + assert active_band <= max_active_band + + d_debt = total_stablecoins + debt = unsafe_sub(debt, d_debt) + ns: int256[2] = staticcall 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 soft-liquidation - can use callback and move bands + new_collateral: uint256 = cb.collateral + if callbacker == empty(address): + xy = extcall AMM.withdraw(_for, 10**18) + new_collateral = xy[1] + ns[0] = self._calculate_debt_n1( + new_collateral, + 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 - cannot use callback or move bands but can avoid a bad liquidation + xy = staticcall AMM.get_sum_xy(_for) + assert callbacker == empty(address) + + 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, debt, False, liquidation_discount) > 0 + + if cb.stablecoins > 0: + self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins) + if _d_debt > 0: + self.transferFrom(BORROWED_TOKEN, msg.sender, self, _d_debt) + + log IController.UserState( + user=_for, + collateral=xy[1], + debt=debt, + n1=ns[0], + n2=ns[1], + liquidation_discount=liquidation_discount, + ) + log IController.Repay( + user=_for, collateral_decrease=0, loan_decrease=d_debt + ) + + self.loan[_for] = IController.Loan(initial_debt=debt, rate_mul=rate_mul) + self._update_total_debt(d_debt, rate_mul, False) + # TODO unify naming between debt and d_debt + self.repaid += d_debt + + self._save_rate() + + +@internal +def _collect_fees() -> uint256: + """ + @notice Collect the fees charged as a fraction of interest. + """ + # TODO add early termination condition for admin fee == 0 + _to: address = staticcall FACTORY.fee_receiver() + + # Borrowing-based fees + rate_mul: uint256 = staticcall AMM.get_rate_mul() + loan: IController.Loan = self._update_total_debt(0, rate_mul, False) + self._save_rate() + + # Cumulative amount which would have been repaid if all the debt was repaid now + to_be_repaid: uint256 = loan.initial_debt + self.repaid + # Cumulative amount which was processed (admin fees have been taken from) + processed: uint256 = self.processed + # Difference between to_be_repaid and processed amount is exactly due to interest charged + if to_be_repaid > processed: + self.processed = to_be_repaid + fees: uint256 = ( + unsafe_sub(to_be_repaid, processed) * self.admin_fee // 10**18 + ) + self.transfer(BORROWED_TOKEN, _to, fees) + log IController.CollectFees(amount=fees, new_supply=loan.initial_debt) + return fees + else: + log IController.CollectFees(amount=0, new_supply=loan.initial_debt) + return 0 + + +################################################################ +# FIGURE OUT A SECTION NAME # +################################################################ + +@external +def approve(_spender: address, _allow: bool): + """ + @notice Allow another address to borrow and repay for the user + @param _spender Address to whitelist for the action + @param _allow Whether to turn the approval on or off (no amounts) + """ + self.approval[msg.sender][_spender] = _allow + log IController.Approval(owner=msg.sender, spender=_spender, allow=_allow) + + +@external +def set_extra_health(_value: uint256): + """ + @notice Add a little bit more to loan_discount to start SL with health higher than usual + @param _value 1e18-based addition to loan_discount + """ + self.extra_health[msg.sender] = _value + log IController.SetExtraHealth(user=msg.sender, health=_value) + + +@external +def save_rate(): + """ + @notice Save current rate + """ + self._save_rate() + + +@external +def collect_fees() -> uint256: + fees: uint256 = self._collect_fees() + return fees + + +@external +def add_collateral(collateral: uint256, _for: address = msg.sender): + """ + @notice Add extra collateral to avoid bad liqidations + @param collateral Amount of collateral to add + @param _for Address to add collateral for + """ + if collateral == 0: + return + self._add_collateral_borrow(collateral, 0, _for, False, _for != msg.sender) + self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) + self._save_rate() + + +@external +def remove_collateral(collateral: uint256, _for: address = msg.sender): + """ + @notice Remove some collateral without repaying the debt + @param collateral Amount of collateral to remove + @param _for Address to remove collateral for + """ + if collateral == 0: + return + assert self._check_approval(_for) + self._add_collateral_borrow(collateral, 0, _for, True, False) + self.transferFrom(COLLATERAL_TOKEN, AMM.address, _for, collateral) + self._save_rate() + + +################################################################ +# VIEW METHODS # +################################################################ + +@external +@view +@reentrant +def amm() -> IAMM: + """ + @notice Address of the AMM + """ + return AMM + + +@external +@view +@reentrant +def collateral_token() -> IERC20: + """ + @notice Address of the collateral token + """ + return COLLATERAL_TOKEN + + +@external +@view +@reentrant +def borrowed_token() -> IERC20: + """ + @notice Address of the borrowed token + """ + return BORROWED_TOKEN + + +@external +@view +def debt(user: address) -> uint256: + """ + @notice Get the value of debt without changing the state + @param user User address + @return Value of debt + """ + return self._debt(user)[0] + + +@external +@view +def loan_exists(user: address) -> bool: + """ + @notice Check whether there is a loan of `user` in existence + """ + return self.loan[user].initial_debt > 0 + + +@external +@view +@reentrant +def total_debt() -> uint256: + """ + @notice Total debt of this controller + @dev Marked as reentrant because used by monetary policy + # TODO check if @reentrant is actually needed + """ + return self._get_total_debt() + + +@external +@view +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 + @param user User to calculate the value for (only necessary for nonzero extra_health) + @return Minimal collateral required + """ + # 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, + ) + + +@external +@view +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. + @param collateral Amount of collateral (at its native precision) + @param debt Amount of requested debt + @param N Number of bands to deposit into + @param user User to calculate n1 for (only necessary for nonzero extra_health) + @return Upper band n1 (n1 <= n2) to deposit into. Signed integer + """ + return self._calculate_debt_n1(collateral, debt, N, user) + + +@view +@external +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 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 +@reentrant +def amm_price() -> uint256: + """ + @notice Current price from the AMM + @dev Marked as reentrant because AMM has a nonreentrant decorator + # TODO check if @reentrant is actually needed + """ + return staticcall AMM.get_p() + + +@view +@external +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) + """ + 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)[0], + convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256), + ] + + +@external +@view +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 + @param d_collateral Change in collateral amount (signed) + @param d_debt Change in debt amount (signed) + @param full Whether it's a 'full' health or not + @param N Number of bands in case loan doesn't yet exist + @return Signed health value + """ + ns: int256[2] = staticcall 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 = 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(10**18, 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, 10**18) - 10**18 + + 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 tokens_to_liquidate(user: address, frac: uint256 = 10**18) -> uint256: + """ + @notice Calculate the amount of stablecoins 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 + """ + health_limit: uint256 = 0 + if not self._check_approval(user): + health_limit = self.liquidation_discounts[user] + stablecoins: uint256 = unsafe_div( + (staticcall 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) + + return unsafe_sub(max(debt, stablecoins), stablecoins) + + +@view +@external +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] + ) + + +@view +@external +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. + @param _from Loan index to start iteration from + @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[IController.Position, 1000] = [] + for i: uint256 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] = staticcall AMM.get_sum_xy(user) + out.append( + IController.Position( + user=user, x=xy[0], y=xy[1], debt=debt, health=health + ) + ) + ix += 1 + return out + + +@external +@view +def admin_fees() -> uint256: + """ + @notice Calculate the amount of fees obtained from the interest + """ + processed: uint256 = self.processed + return unsafe_sub( + max(self._get_total_debt() + self.repaid, processed), processed + ) + + +@external +@view +def factory() -> IFactory: + """ + @notice Address of the factory + """ + return FACTORY diff --git a/contracts/controller_core.vy b/contracts/controller_core.vy deleted file mode 100644 index e2625725..00000000 --- a/contracts/controller_core.vy +++ /dev/null @@ -1,811 +0,0 @@ -# pragma version 0.4.3 -# pragma nonreentrancy on - -from contracts.interfaces import IAMM -from contracts.interfaces import IMonetaryPolicy -from contracts.interfaces import IController -from ethereum.ercs import IERC20 -from ethereum.ercs import IERC20Detailed - -from snekmate.utils import math - -################################################################ -# IMMUTABLES # -################################################################ - -AMM: immutable(IAMM) -MAX_AMM_FEE: immutable( - uint256 -) # let's set to MIN_TICKS / A: for example, 4% max fee for A=100 -A: immutable(uint256) -Aminus1: immutable(uint256) -LOGN_A_RATIO: immutable(int256) # log(A / (A - 1)) -SQRT_BAND_RATIO: immutable(uint256) - -COLLATERAL_TOKEN: immutable(IERC20) -COLLATERAL_PRECISION: immutable(uint256) -BORROWED_TOKEN: immutable(IERC20) -BORROWED_PRECISION: immutable(uint256) - -################################################################ -# CONSTANTS # -################################################################ - - -from contracts import constants as c - -WAD: constant(uint256) = c.WAD -DEAD_SHARES: constant(uint256) = c.DEAD_SHARES - - -MIN_AMM_FEE: constant(uint256) = 10**6 # 1e-12, still needs to be above 0 -MIN_TICKS_UINT: constant(uint256) = 4 - -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, -) - -MAX_LOAN_DISCOUNT: constant(uint256) = 5 * 10**17 -MIN_LIQUIDATION_DISCOUNT: constant(uint256) = ( - 10**16 -) # Start liquidating when threshold reached -MAX_TICKS: constant(int256) = 50 -MAX_TICKS_UINT: constant(uint256) = c.MAX_TICKS_UINT -MIN_TICKS: constant(int256) = 4 -MAX_SKIP_TICKS: constant(uint256) = 1024 -MAX_P_BASE_BANDS: constant(int256) = 5 - -MAX_RATE: constant(uint256) = 43959106799 # 300% APY - -################################################################ -# STORAGE # -################################################################ - -liquidation_discount: public(uint256) -loan_discount: public(uint256) -# TODO make settable -_monetary_policy: IMonetaryPolicy -# TODO can't mark it as public, likely a compiler bug -# TODO make an issue -@external -@view -def monetary_policy() -> IMonetaryPolicy: - """ - @notice Address of the monetary policy - """ - return self._monetary_policy - - -approval: public(HashMap[address, HashMap[address, bool]]) -extra_health: public(HashMap[address, uint256]) - -loan: HashMap[address, IController.Loan] -liquidation_discounts: public(HashMap[address, uint256]) -_total_debt: IController.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 - - -@deploy -def __init__( - _AMM: IAMM, - _collateral_token: IERC20, - _borrowed_token: IERC20, - monetary_policy: IMonetaryPolicy, - loan_discount: uint256, - liquidation_discount: uint256, -): - AMM = _AMM - - A = staticcall AMM.A() - Aminus1 = A - 1 - - # TODO check math (removed unsafe) - LOGN_A_RATIO = math._wad_ln(convert(A * WAD // A - 1, int256)) - # TODO check math - SQRT_BAND_RATIO = isqrt(10**36 * A // (A - 1)) - - MAX_AMM_FEE = min(WAD * MIN_TICKS_UINT // A, 10**17) - - COLLATERAL_TOKEN = _collateral_token - collateral_decimals: uint256 = convert( - staticcall IERC20Detailed(COLLATERAL_TOKEN.address).decimals(), uint256 - ) - COLLATERAL_PRECISION = pow_mod256(10, 18 - collateral_decimals) - - BORROWED_TOKEN = _borrowed_token - borrowed_decimals: uint256 = convert( - staticcall IERC20Detailed(BORROWED_TOKEN.address).decimals(), uint256 - ) - BORROWED_PRECISION = pow_mod256(10, 18 - borrowed_decimals) - - self._monetary_policy = monetary_policy - self.liquidation_discount = liquidation_discount - self.loan_discount = loan_discount - self._total_debt.rate_mul = 10**18 - - # TODO check what this is needed for - assert extcall BORROWED_TOKEN.approve( - msg.sender, max_value(uint256), default_return_value=True - ) - - -################################################################ -# BUILDING BLOCKS # -################################################################ - - -@internal -@view -def _debt(user: address) -> (uint256, uint256): - """ - @notice Get the value of debt and rate_mul and update the rate_mul counter - @param user User address - @return (debt, rate_mul) - """ - rate_mul: uint256 = staticcall AMM.get_rate_mul() - loan: IController.Loan = self.loan[user] - if loan.initial_debt == 0: - return (0, rate_mul) - else: - # Let user repay 1 smallest decimal more so that the system doesn't lose on precision - # Use ceil div - debt: uint256 = loan.initial_debt * rate_mul - 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 - return (debt, rate_mul) - - -@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 -@view -def get_y_effective( - collateral: uint256, N: uint256, discount: uint256 -) -> uint256: - """ - @notice Intermediary method which calculates y_effective defined as x_effective / p_base, - however discounted by loan_discount. - x_effective is an amount which can be obtained from collateral when liquidating - @param collateral Amount of collateral to get the value for - @param N Number of bands the deposit is made into - @param discount Loan discount at 1e18 base (e.g. 1e18 == 100%) - @return y_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_effective * p_oracle_up(n1) - # d_y_effective = y / N / sqrt(A / (A - 1)) - # 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, - ), - ), - unsafe_mul(SQRT_BAND_RATIO, N), - ) - y_effective: uint256 = d_y_effective - for i: uint256 in range(1, MAX_TICKS_UINT): - if i == N: - break - d_y_effective = unsafe_div(d_y_effective * Aminus1, 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: - """ - @notice Calculate the upper band number for the deposit to sit in to support - the given debt. 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 - @return Upper band n1 (n1 <= n2) to deposit into. Signed integer - """ - assert debt > 0, "No loan" - 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], - ) - # p_oracle_up(n1) = base_price * ((A - 1) / A)**n1 - - # We borrow up until min band touches p_oracle, - # or it touches non-empty bands which cannot be skipped. - # We calculate required n1 for given (collateral, debt), - # and if n1 corresponds to price_oracle being too high, or unreachable band - # - 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 - - # 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 = 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_div(n1, LOGN_A_RATIO) - - n1 = min(n1, 1024 - convert(N, int256)) + n0 - if n1 <= n0: - assert staticcall AMM.can_skip_bands(n1 - 1), "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 = 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() * 10**18 // 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 = unsafe_div(p_base * A, Aminus1) - if p_base > p_oracle: - return p_base_prev - return p_base - - -@internal -@view -def _check_approval(_for: address) -> bool: - return msg.sender == _for or self.approval[_for][msg.sender] - - -@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 - ) - - return f_remove - - -@internal -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 - self.loan_ix[_for] = 0 - if loan_ix < last_loan_ix: # Need to replace - last_loan: address = self.loans[last_loan_ix] - self.loans[loan_ix] = last_loan - self.loan_ix[last_loan] = loan_ix - self.n_loans = last_loan_ix - - -@internal -def transferFrom(token: IERC20, _from: address, _to: address, amount: uint256): - if amount > 0: - assert extcall token.transferFrom( - _from, _to, amount, default_return_value=True - ) - - -@internal -def transfer(token: IERC20, _to: address, amount: uint256): - if amount > 0: - assert extcall token.transfer(_to, amount, default_return_value=True) - - -@internal -@view -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 - @param user User address to calculate health for - @param debt The amount of debt to calculate health for - @param full Whether to take into account the price difference above the highest user's band - @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(staticcall AMM.get_x_down(user), int256) * health, - convert(debt, int256), - ) - - 10**18 - ) - - if full: - 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) - * (staticcall AMM.get_sum_xy(user))[1] - * COLLATERAL_PRECISION, - debt * BORROWED_PRECISION, - ), - int256, - ) - return health - - -@internal -def _save_rate(): - """ - @notice Save current rate - """ - rate: uint256 = min(extcall self._monetary_policy.rate_write(), MAX_RATE) - extcall AMM.set_rate(rate) - - -@internal -def execute_callback( - callbacker: address, - callback_sig: bytes4, - user: address, - stablecoins: uint256, - collateral: uint256, - debt: uint256, - calldata: Bytes[10**4], -) -> IController.CallbackData: - assert callbacker != COLLATERAL_TOKEN.address - assert callbacker != BORROWED_TOKEN.address - - 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, calldata), - ), - max_outsize=64, - ) - data.stablecoins = convert(slice(response, 0, 32), uint256) - data.collateral = convert(slice(response, 32, 32), uint256) - - # Checks after callback - 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 - - -################################################################ -# FIGURE OUT A SECTION NAME # -################################################################ - -@external -def approve(_spender: address, _allow: bool): - """ - @notice Allow another address to borrow and repay for the user - @param _spender Address to whitelist for the action - @param _allow Whether to turn the approval on or off (no amounts) - """ - self.approval[msg.sender][_spender] = _allow - log IController.Approval(owner=msg.sender, spender=_spender, allow=_allow) - - -@external -def set_extra_health(_value: uint256): - """ - @notice Add a little bit more to loan_discount to start SL with health higher than usual - @param _value 1e18-based addition to loan_discount - """ - self.extra_health[msg.sender] = _value - log IController.SetExtraHealth(user=msg.sender, health=_value) - - -@external -def save_rate(): - """ - @notice Save current rate - """ - self._save_rate() - - -################################################################ -# VIEW METHODS # -################################################################ - -@external -@view -@reentrant -def amm() -> IAMM: - """ - @notice Address of the AMM - """ - return AMM - - -@external -@view -@reentrant -def collateral_token() -> IERC20: - """ - @notice Address of the collateral token - """ - return COLLATERAL_TOKEN - - -@external -@view -@reentrant -def borrowed_token() -> IERC20: - """ - @notice Address of the borrowed token - """ - return BORROWED_TOKEN - - -@external -@view -def debt(user: address) -> uint256: - """ - @notice Get the value of debt without changing the state - @param user User address - @return Value of debt - """ - return self._debt(user)[0] - - -@external -@view -def loan_exists(user: address) -> bool: - """ - @notice Check whether there is a loan of `user` in existence - """ - return self.loan[user].initial_debt > 0 - - -@external -@view -@reentrant -def total_debt() -> uint256: - """ - @notice Total debt of this controller - @dev Marked as reentrant because used by monetary policy - # TODO check if @reentrant is actually needed - """ - return self._get_total_debt() - - - - - -@external -@view -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 - @param user User to calculate the value for (only necessary for nonzero extra_health) - @return Minimal collateral required - """ - # 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, - ) - - -@external -@view -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. - @param collateral Amount of collateral (at its native precision) - @param debt Amount of requested debt - @param N Number of bands to deposit into - @param user User to calculate n1 for (only necessary for nonzero extra_health) - @return Upper band n1 (n1 <= n2) to deposit into. Signed integer - """ - return self._calculate_debt_n1(collateral, debt, N, user) - - -@view -@external -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 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 -@reentrant -def amm_price() -> uint256: - """ - @notice Current price from the AMM - @dev Marked as reentrant because AMM has a nonreentrant decorator - # TODO check if @reentrant is actually needed - """ - return staticcall AMM.get_p() - - -@view -@external -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) - """ - 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)[0], - convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256), - ] - - -@external -@view -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 - @param d_collateral Change in collateral amount (signed) - @param d_debt Change in debt amount (signed) - @param full Whether it's a 'full' health or not - @param N Number of bands in case loan doesn't yet exist - @return Signed health value - """ - ns: int256[2] = staticcall 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 = 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(10**18, 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, 10**18) - 10**18 - - 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 tokens_to_liquidate(user: address, frac: uint256 = 10**18) -> uint256: - """ - @notice Calculate the amount of stablecoins 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 - """ - health_limit: uint256 = 0 - if not self._check_approval(user): - health_limit = self.liquidation_discounts[user] - stablecoins: uint256 = unsafe_div( - (staticcall 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) - - return unsafe_sub(max(debt, stablecoins), stablecoins) - - -@view -@external -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] - ) - - -@view -@external -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. - @param _from Loan index to start iteration from - @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[IController.Position, 1000] = [] - for i: uint256 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] = staticcall AMM.get_sum_xy(user) - out.append( - IController.Position( - user=user, x=xy[0], y=xy[1], debt=debt, health=health - ) - ) - ix += 1 - return out diff --git a/contracts/interfaces/IController.vyi b/contracts/interfaces/IController.vyi deleted file mode 100644 index f34d0bbe..00000000 --- a/contracts/interfaces/IController.vyi +++ /dev/null @@ -1,327 +0,0 @@ -from ethereum.ercs import IERC20 -from contracts.interfaces import ILMGauge -from contracts.interfaces import IAMM -from contracts.interfaces import IMonetaryPolicy - -# Structs - -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 - - -# Events - -event UserState: - user: address - collateral: uint256 - debt: uint256 - n1: int256 - n2: int256 - liquidation_discount: uint256 - - -event Borrow: - user: address - collateral_increase: uint256 - loan_increase: uint256 - - -event Repay: - user: address - collateral_decrease: uint256 - loan_decrease: uint256 - - -event RemoveCollateral: - user: address - collateral_decrease: uint256 - - -event Liquidate: - liquidator: address - user: 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: address - health: uint256 - - -event CollectFees: - amount: uint256 - new_supply: uint256 - - -event SetLMCallback: - callback: ILMGauge - - -event Approval: - owner: address - spender: address - allow: bool - - -# Functions - -@view -@external -def factory() -> address: - ... - - -@view -@external -def amm() -> IAMM: - ... - - -@view -@external -def collateral_token() -> IERC20: - ... - - -@view -@external -def borrowed_token() -> IERC20: - ... - - -@external -def save_rate(): - ... - - -@view -@external -def debt(user: address) -> uint256: - ... - - -@view -@external -def loan_exists(user: address) -> bool: - ... - - -@view -@external -def total_debt() -> uint256: - ... - - -@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: - ... - - -@view -@external -def calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256, user: address) -> int256: - ... - - -@external -def create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address, callbacker: address, calldata: Bytes[10000]): - ... - - -@external -def add_collateral(collateral: uint256, _for: address): - ... - - -@external -def remove_collateral(collateral: uint256, _for: address): - ... - - -@external -def borrow_more(collateral: uint256, debt: uint256, _for: address, callbacker: address, calldata: Bytes[10000]): - ... - - -@external -def repay(_d_debt: uint256, _for: address, max_active_band: int256, callbacker: address, calldata: Bytes[10000]): - ... - - -@view -@external -def health_calculator(user: address, d_collateral: int256, d_debt: int256, full: bool, N: uint256) -> int256: - ... - - -@external -def liquidate(user: address, min_x: uint256, frac: uint256, callbacker: address, calldata: Bytes[10000]): - ... - - -@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 amm_price() -> uint256: - ... - - -@view -@external -def user_prices(user: address) -> uint256[2]: - ... - - -@view -@external -def user_state(user: address) -> uint256[4]: - ... - - -@external -def set_amm_fee(fee: uint256): - ... - - -@external -def set_monetary_policy(monetary_policy: address): - ... - - -@external -def set_borrowing_discounts(loan_discount: uint256, liquidation_discount: uint256): - ... - - -@external -def set_callback(cb: ILMGauge): - ... - - -@view -@external -def admin_fees() -> uint256: - ... - - -@external -def collect_fees() -> uint256: - ... - - -@external -def approve(_spender: address, _allow: bool): - ... - - -@external -def set_extra_health(_value: 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 monetary_policy() -> IMonetaryPolicy: - ... - - -@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: - ... - diff --git a/contracts/interfaces/ILlamalendController.vyi b/contracts/interfaces/ILlamalendController.vyi index d8411338..00528069 100644 --- a/contracts/interfaces/ILlamalendController.vyi +++ b/contracts/interfaces/ILlamalendController.vyi @@ -1,2 +1,35 @@ +from contracts.interfaces import IVault + +# Events + event SetBorrowCap: - borrow_cap: uint256 \ No newline at end of file + borrow_cap: uint256 + + +@view +@external +def vault() -> IVault: + ... + + +@external +def set_borrow_cap(_borrow_cap: uint256): + ... + + +@external +def set_admin_fee(admin_fee: uint256): + ... + + +@view +@external +def lent() -> uint256: + ... + + +@view +@external +def collected() -> uint256: + ... + diff --git a/contracts/interfaces/IMintController.vyi b/contracts/interfaces/IMintController.vyi index 168c1cf7..4491b20a 100644 --- a/contracts/interfaces/IMintController.vyi +++ b/contracts/interfaces/IMintController.vyi @@ -1,3 +1,102 @@ +from ethereum.ercs 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 + stablecoin_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 + + +# 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 + stablecoins: uint256 + collateral: uint256 + +# Functions + +@view +@external +def monetary_policy() -> IMonetaryPolicy: + ... + + @view @external def minted() -> uint256: @@ -7,4 +106,249 @@ def minted() -> uint256: @view @external def redeemed() -> uint256: - ... \ No newline at end of file + ... + + +@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, max_active_band: int256, callbacker: address, calldata: Bytes[10000]): + ... + + +@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 repaid() -> uint256: + ... + + +@view +@external +def processed() -> uint256: + ... + + +@view +@external +def admin_fee() -> uint256: + ... + diff --git a/contracts/lending/Controller.vy b/contracts/lending/Controller.vy deleted file mode 100644 index 64a3a953..00000000 --- a/contracts/lending/Controller.vy +++ /dev/null @@ -1,869 +0,0 @@ -# pragma version 0.4.3 -# pragma nonreentrancy on -# pragma optimize codesize -""" -@title LlamaLend Controller -@author Curve.Fi -@license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved -""" - -from ethereum.ercs import IERC20 -from ethereum.ercs import IERC20Detailed -from contracts.interfaces import IAMM -from contracts.interfaces import ILMGauge -from contracts.interfaces import IMonetaryPolicy -from contracts.interfaces import IVault -from contracts.interfaces import IController - -implements: IController -from contracts.interfaces import ILlamalendController - -from snekmate.utils import math -from contracts import controller_core as ctrl - -initializes: ctrl - -exports: ( - ctrl.amm, - ctrl.amm_price, - ctrl.approval, - ctrl.approve, - ctrl.borrowed_token, - ctrl.calculate_debt_n1, - ctrl.collateral_token, - ctrl.debt, - ctrl.extra_health, - ctrl.health, - ctrl.health_calculator, - ctrl.liquidation_discount, - ctrl.liquidation_discounts, - ctrl.loan_discount, - ctrl.loan_exists, - ctrl.loan_ix, - ctrl.loans, - # ctrl.max_borrowable, # TODO check this one for diffs - ctrl.min_collateral, - ctrl.monetary_policy, - ctrl.n_loans, - ctrl.save_rate, - ctrl.set_extra_health, - ctrl.tokens_to_liquidate, - ctrl.total_debt, - ctrl.user_prices, - ctrl.user_state, - ctrl.users_to_liquidate, -) - -VAULT: immutable(IVault) - -# cumulative amount of assets ever lent -lent: public(uint256) -# cumulative amount of assets ever repaid -repaid: public(uint256) -# cumulative amount of assets admin fees have been taken from -processed: public(uint256) -# cumulative amount of assets collected by admin -collected: public(uint256) - -borrow_cap: public(uint256) -admin_fee: public(uint256) - -MAX_ADMIN_FEE: constant(uint256) = 2 * 10**17 # 20% - - -@deploy -def __init__( - monetary_policy: IMonetaryPolicy, - loan_discount: uint256, - liquidation_discount: uint256, - amm: IAMM, -): - """ - @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) - """ - VAULT = IVault(msg.sender) - - collateral_token: IERC20 = staticcall VAULT.collateral_token() - borrowed_token: IERC20 = staticcall VAULT.borrowed_token() - - ctrl.__init__( - amm, - collateral_token, - borrowed_token, - monetary_policy, - loan_discount, - liquidation_discount, - ) - - -@external -@view -def factory() -> address: - """ - @notice Address of the factory - """ - return VAULT.address - - -@internal -def _update_total_debt( - d_debt: uint256, rate_mul: uint256, is_increase: bool -) -> IController.Loan: - """ - @param d_debt Change in debt amount (unsigned) - @param rate_mul New rate_mul - @param is_increase Whether debt increases or decreases - @notice Update total debt of this controller - """ - loan: IController.Loan = ctrl._total_debt - loan.initial_debt = loan.initial_debt * rate_mul // loan.rate_mul - if is_increase: - loan.initial_debt += d_debt - assert loan.initial_debt <= self.borrow_cap, "Borrow cap exceeded" - else: - loan.initial_debt = unsafe_sub(max(loan.initial_debt, d_debt), d_debt) - loan.rate_mul = rate_mul - ctrl._total_debt = loan - - return loan - - -@internal -@view -def _borrowed_balance() -> uint256: - # (VAULT.deposited() - VAULT.withdrawn()) - (self.lent - self.repaid) - self.collected - return ( - staticcall VAULT.deposited() - + self.repaid - - staticcall VAULT.withdrawn() - - self.lent - - self.collected - ) - - -@external -@view -def borrowed_balance() -> uint256: - return self._borrowed_balance() - - -@external -@view -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 >= ctrl.MIN_TICKS_UINT and N <= ctrl.MAX_TICKS_UINT - - y_effective: uint256 = ctrl.get_y_effective( - collateral * ctrl.COLLATERAL_PRECISION, - N, - ctrl.loan_discount + ctrl.extra_health[user], - ) - - x: uint256 = unsafe_sub( - max(unsafe_div(y_effective * ctrl.max_p_base(), 10**18), 1), 1 - ) - x = unsafe_div( - x * (10**18 - 10**14), unsafe_mul(10**18, ctrl.BORROWED_PRECISION) - ) # Make it a bit smaller - - _total_debt: uint256 = ctrl._get_total_debt() - _cap: uint256 = unsafe_sub(max(self.borrow_cap, _total_debt), _total_debt) - _cap = min(self._borrowed_balance() + current_debt, _cap) - return min( - x, _cap - ) # Cannot borrow beyond the amount of coins Controller has or beyond borrow_cap - - -@internal -def _create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address): - assert ctrl.loan[_for].initial_debt == 0, "Loan already created" - assert N > ctrl.MIN_TICKS_UINT - 1, "Need more ticks" - assert N < ctrl.MAX_TICKS_UINT + 1, "Need less ticks" - - n1: int256 = ctrl._calculate_debt_n1(collateral, debt, N, _for) - n2: int256 = n1 + convert(unsafe_sub(N, 1), int256) - - rate_mul: uint256 = staticcall ctrl.AMM.get_rate_mul() - ctrl.loan[_for] = IController.Loan(initial_debt=debt, rate_mul=rate_mul) - liquidation_discount: uint256 = ctrl.liquidation_discount - ctrl.liquidation_discounts[_for] = liquidation_discount - - n_loans: uint256 = ctrl.n_loans - ctrl.loans[n_loans] = _for - ctrl.loan_ix[_for] = n_loans - ctrl.n_loans = unsafe_add(n_loans, 1) - - self._update_total_debt(debt, rate_mul, True) - - extcall ctrl.AMM.deposit_range(_for, collateral, n1, n2) - self.lent += debt - self.processed += debt - - ctrl._save_rate() - - log IController.UserState( - user=_for, - collateral=collateral, - debt=debt, - n1=n1, - n2=n2, - liquidation_discount=liquidation_discount, - ) - log IController.Borrow( - user=_for, collateral_increase=collateral, loan_increase=debt - ) - - -@external -def create_loan( - collateral: uint256, - debt: uint256, - N: uint256, - _for: address = msg.sender, - callbacker: address = empty(address), - calldata: Bytes[10**4] = b"", -): - """ - @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 _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 ctrl._check_approval(_for) - - more_collateral: uint256 = 0 - if callbacker != empty(address): - ctrl.transfer(ctrl.BORROWED_TOKEN, callbacker, debt) - # If there is any unused debt, callbacker can send it to the user - more_collateral = ctrl.execute_callback( - callbacker, - ctrl.CALLBACK_DEPOSIT, - _for, - 0, - collateral, - debt, - calldata, - ).collateral - - self._create_loan(collateral + more_collateral, debt, N, _for) - - ctrl.transferFrom( - ctrl.COLLATERAL_TOKEN, msg.sender, ctrl.AMM.address, collateral - ) - if more_collateral > 0: - ctrl.transferFrom( - ctrl.COLLATERAL_TOKEN, callbacker, ctrl.AMM.address, more_collateral - ) - if callbacker == empty(address): - ctrl.transfer(ctrl.BORROWED_TOKEN, _for, debt) - - -@internal -def _add_collateral_borrow( - d_collateral: uint256, - d_debt: uint256, - _for: address, - remove_collateral: bool, - check_rounding: bool, -): - """ - @notice Internal method to borrow and add or remove collateral - @param d_collateral Amount of collateral to add - @param d_debt Amount of debt increase - @param _for Address to transfer tokens to - @param remove_collateral Remove collateral instead of adding - @param check_rounding Check that amount added is no less than the rounding error on the loan - """ - debt: uint256 = 0 - rate_mul: uint256 = 0 - debt, rate_mul = ctrl._debt(_for) - assert debt > 0, "Loan doesn't exist" - debt += d_debt - - xy: uint256[2] = extcall ctrl.AMM.withdraw(_for, 10**18) - assert xy[0] == 0, "Already in underwater mode" - if remove_collateral: - xy[1] -= d_collateral - else: - xy[1] += d_collateral - if check_rounding: - # We need d(x + p*y) > 1 wei. For that, we do an equivalent check (but with x2 for safety) - # 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 * staticcall ctrl.AMM.price_oracle() - > 2 - * 10**18 - * ctrl.BORROWED_PRECISION // ctrl.COLLATERAL_PRECISION - ) - ns: int256[2] = staticcall ctrl.AMM.read_user_tick_numbers(_for) - size: uint256 = convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256) - n1: int256 = ctrl._calculate_debt_n1(xy[1], debt, size, _for) - n2: int256 = n1 + unsafe_sub(ns[1], ns[0]) - - extcall ctrl.AMM.deposit_range(_for, xy[1], n1, n2) - ctrl.loan[_for] = IController.Loan(initial_debt=debt, rate_mul=rate_mul) - - liquidation_discount: uint256 = 0 - if _for == msg.sender: - liquidation_discount = ctrl.liquidation_discount - ctrl.liquidation_discounts[_for] = liquidation_discount - else: - liquidation_discount = ctrl.liquidation_discounts[_for] - - if d_debt != 0: - self._update_total_debt(d_debt, rate_mul, True) - - if remove_collateral: - log IController.RemoveCollateral( - user=_for, collateral_decrease=d_collateral - ) - else: - 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, - ) - - -@external -def add_collateral(collateral: uint256, _for: address = msg.sender): - """ - @notice Add extra collateral to avoid bad liqidations - @param collateral Amount of collateral to add - @param _for Address to add collateral for - """ - if collateral == 0: - return - self._add_collateral_borrow(collateral, 0, _for, False, _for != msg.sender) - ctrl.transferFrom( - ctrl.COLLATERAL_TOKEN, msg.sender, ctrl.AMM.address, collateral - ) - ctrl._save_rate() - - -@external -def remove_collateral(collateral: uint256, _for: address = msg.sender): - """ - @notice Remove some collateral without repaying the debt - @param collateral Amount of collateral to remove - @param _for Address to remove collateral for - """ - if collateral == 0: - return - assert ctrl._check_approval(_for) - self._add_collateral_borrow(collateral, 0, _for, True, False) - ctrl.transferFrom(ctrl.COLLATERAL_TOKEN, ctrl.AMM.address, _for, collateral) - ctrl._save_rate() - - -@external -def borrow_more( - collateral: uint256, - debt: uint256, - _for: address = msg.sender, - callbacker: address = empty(address), - calldata: Bytes[10**4] = b"", -): - """ - @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 _for Address to borrow for - @param callbacker Address of the callback contract - @param calldata Any data for callbacker - """ - if debt == 0: - return - assert ctrl._check_approval(_for) - - more_collateral: uint256 = 0 - if callbacker != empty(address): - ctrl.transfer(ctrl.BORROWED_TOKEN, callbacker, debt) - # If there is any unused debt, callbacker can send it to the user - more_collateral = ctrl.execute_callback( - callbacker, - ctrl.CALLBACK_DEPOSIT, - _for, - 0, - collateral, - debt, - calldata, - ).collateral - - self._add_collateral_borrow( - collateral + more_collateral, debt, _for, False, False - ) - self.lent += debt - self.processed += debt - - ctrl.transferFrom( - ctrl.COLLATERAL_TOKEN, msg.sender, ctrl.AMM.address, collateral - ) - if more_collateral > 0: - ctrl.transferFrom( - ctrl.COLLATERAL_TOKEN, callbacker, ctrl.AMM.address, more_collateral - ) - if callbacker == empty(address): - ctrl.transfer(ctrl.BORROWED_TOKEN, _for, debt) - ctrl._save_rate() - - -@external -def repay( - _d_debt: uint256, - _for: address = msg.sender, - max_active_band: int256 = max_value(int256), - callbacker: address = empty(address), - calldata: Bytes[10**4] = b"", -): - """ - @notice Repay debt (partially or fully) - @param _d_debt The amount of debt to repay from user's wallet. 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) - @param callbacker Address of the callback contract - @param calldata Any data for callbacker - """ - debt: uint256 = 0 - rate_mul: uint256 = 0 - debt, rate_mul = ctrl._debt(_for) - assert debt > 0, "Loan doesn't exist" - approval: bool = ctrl._check_approval(_for) - xy: uint256[2] = empty(uint256[2]) - - cb: IController.CallbackData = empty(IController.CallbackData) - if callbacker != empty(address): - assert approval - xy = extcall ctrl.AMM.withdraw(_for, 10**18) - ctrl.transferFrom( - ctrl.COLLATERAL_TOKEN, ctrl.AMM.address, callbacker, xy[1] - ) - cb = ctrl.execute_callback( - callbacker, ctrl.CALLBACK_REPAY, _for, xy[0], xy[1], debt, calldata - ) - - total_stablecoins: uint256 = _d_debt + xy[0] + cb.stablecoins - assert total_stablecoins > 0 # dev: no coins to repay - 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 - if callbacker == empty(address): - xy = extcall ctrl.AMM.withdraw(_for, 10**18) - - if xy[0] > 0: - # Only allow full repayment when underwater for the sender to do - assert approval - ctrl.transferFrom( - ctrl.BORROWED_TOKEN, ctrl.AMM.address, self, xy[0] - ) - if cb.stablecoins > 0: - ctrl.transferFrom( - ctrl.BORROWED_TOKEN, callbacker, self, cb.stablecoins - ) - if _d_debt > 0: - ctrl.transferFrom(ctrl.BORROWED_TOKEN, msg.sender, self, _d_debt) - - if total_stablecoins > d_debt: - ctrl.transfer( - ctrl.BORROWED_TOKEN, _for, unsafe_sub(total_stablecoins, d_debt) - ) - # Transfer collateral to _for - if callbacker == empty(address): - if xy[1] > 0: - ctrl.transferFrom( - ctrl.COLLATERAL_TOKEN, ctrl.AMM.address, _for, xy[1] - ) - else: - if cb.collateral > 0: - ctrl.transferFrom( - ctrl.COLLATERAL_TOKEN, callbacker, _for, cb.collateral - ) - ctrl._remove_from_list(_for) - 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=d_debt - ) - # Else - partial repayment - else: - active_band: int256 = staticcall ctrl.AMM.active_band_with_skip() - assert active_band <= max_active_band - - d_debt = total_stablecoins - debt = unsafe_sub(debt, d_debt) - ns: int256[2] = staticcall ctrl.AMM.read_user_tick_numbers(_for) - size: int256 = unsafe_sub(ns[1], ns[0]) - liquidation_discount: uint256 = ctrl.liquidation_discounts[_for] - - if ns[0] > active_band: - # Not in soft-liquidation - can use callback and move bands - new_collateral: uint256 = cb.collateral - if callbacker == empty(address): - xy = extcall ctrl.AMM.withdraw(_for, 10**18) - new_collateral = xy[1] - ns[0] = ctrl._calculate_debt_n1( - new_collateral, - debt, - convert(unsafe_add(size, 1), uint256), - _for, - ) - ns[1] = ns[0] + size - extcall ctrl.AMM.deposit_range(_for, new_collateral, ns[0], ns[1]) - else: - # Underwater - cannot use callback or move bands but can avoid a bad liquidation - xy = staticcall ctrl.AMM.get_sum_xy(_for) - assert callbacker == empty(address) - - if approval: - # Update liquidation discount only if we are that same user. No rugs - liquidation_discount = ctrl.liquidation_discount - ctrl.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 ctrl._health(_for, debt, False, liquidation_discount) > 0 - - if cb.stablecoins > 0: - ctrl.transferFrom( - ctrl.BORROWED_TOKEN, callbacker, self, cb.stablecoins - ) - if _d_debt > 0: - ctrl.transferFrom(ctrl.BORROWED_TOKEN, msg.sender, self, _d_debt) - - log IController.UserState( - user=_for, - collateral=xy[1], - debt=debt, - n1=ns[0], - n2=ns[1], - liquidation_discount=liquidation_discount, - ) - log IController.Repay( - user=_for, collateral_decrease=0, loan_decrease=d_debt - ) - - self.repaid += d_debt - - ctrl.loan[_for] = IController.Loan(initial_debt=debt, rate_mul=rate_mul) - self._update_total_debt(d_debt, rate_mul, False) - - ctrl._save_rate() - - -@internal -def _liquidate( - user: address, - min_x: uint256, - health_limit: uint256, - frac: uint256, - callbacker: address, - calldata: Bytes[10**4], -): - """ - @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 - @param callbacker Address of the callback contract - @param calldata Any data for callbacker - """ - debt: uint256 = 0 - rate_mul: uint256 = 0 - debt, rate_mul = ctrl._debt(user) - - if health_limit != 0: - assert ( - ctrl._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 debt > 0 - final_debt = unsafe_sub(final_debt, debt) - - # Withdraw sender's stablecoin 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] = extcall ctrl.AMM.withdraw( - user, ctrl._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) - ctrl.transferFrom(ctrl.BORROWED_TOKEN, ctrl.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 - ctrl.transferFrom( - ctrl.COLLATERAL_TOKEN, ctrl.AMM.address, msg.sender, xy[1] - ) - # Request what's left from user - ctrl.transferFrom(ctrl.BORROWED_TOKEN, msg.sender, self, to_repay) - - else: - # Move collateral to callbacker, call it and remove everything from it back in - ctrl.transferFrom( - ctrl.COLLATERAL_TOKEN, ctrl.AMM.address, callbacker, xy[1] - ) - # Callback - cb: IController.CallbackData = ctrl.execute_callback( - callbacker, - ctrl.CALLBACK_LIQUIDATE, - user, - xy[0], - xy[1], - debt, - calldata, - ) - assert cb.stablecoins >= to_repay, "not enough proceeds" - if cb.stablecoins > to_repay: - ctrl.transferFrom( - ctrl.BORROWED_TOKEN, - callbacker, - msg.sender, - unsafe_sub(cb.stablecoins, to_repay), - ) - ctrl.transferFrom(ctrl.BORROWED_TOKEN, callbacker, self, to_repay) - ctrl.transferFrom( - ctrl.COLLATERAL_TOKEN, callbacker, msg.sender, cb.collateral - ) - else: - # Withdraw collateral - ctrl.transferFrom( - ctrl.COLLATERAL_TOKEN, ctrl.AMM.address, msg.sender, xy[1] - ) - # Return what's left to user - if xy[0] > debt: - ctrl.transferFrom( - ctrl.BORROWED_TOKEN, - ctrl.AMM.address, - msg.sender, - unsafe_sub(xy[0], debt), - ) - self.repaid += debt - ctrl.loan[user] = IController.Loan( - initial_debt=final_debt, rate_mul=rate_mul - ) - log IController.Repay( - user=user, collateral_decrease=xy[1], loan_decrease=debt - ) - log IController.Liquidate( - liquidator=msg.sender, - user=user, - collateral_received=xy[1], - stablecoin_received=xy[0], - debt=debt, - ) - if final_debt == 0: - 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 - ctrl._remove_from_list(user) - - self._update_total_debt(debt, rate_mul, False) - - ctrl._save_rate() - - -@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 (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 calldata Any data for callbacker - """ - discount: uint256 = 0 - if not ctrl._check_approval(user): - discount = ctrl.liquidation_discounts[user] - self._liquidate( - user, min_x, discount, min(frac, 10**18), callbacker, calldata - ) - - -@external -@reentrant -def set_amm_fee(fee: uint256): - """ - @notice Set the AMM fee (factory admin only) - @dev Reentrant because AMM is nonreentrant TODO check this one - @param fee The fee which should be no higher than MAX_AMM_FEE - """ - assert msg.sender == staticcall VAULT.admin() - assert fee <= ctrl.MAX_AMM_FEE and fee >= ctrl.MIN_AMM_FEE, "Fee" - extcall ctrl.AMM.set_fee(fee) - - -@external -def set_monetary_policy(monetary_policy: address): - """ - @notice Set monetary policy contract - @param monetary_policy Address of the monetary policy contract - """ - assert msg.sender == staticcall VAULT.admin() - ctrl._monetary_policy = IMonetaryPolicy(monetary_policy) - extcall IMonetaryPolicy(monetary_policy).rate_write() - log IController.SetMonetaryPolicy(monetary_policy=monetary_policy) - - -@external -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 == staticcall VAULT.admin() - assert loan_discount > liquidation_discount - assert liquidation_discount >= ctrl.MIN_LIQUIDATION_DISCOUNT - assert loan_discount <= ctrl.MAX_LOAN_DISCOUNT - ctrl.liquidation_discount = liquidation_discount - ctrl.loan_discount = loan_discount - log IController.SetBorrowingDiscounts( - loan_discount=loan_discount, liquidation_discount=liquidation_discount - ) - - -@external -def set_callback(cb: ILMGauge): - """ - @notice Set liquidity mining callback - """ - assert msg.sender == staticcall VAULT.admin() - extcall ctrl.AMM.set_callback(cb) - log IController.SetLMCallback(callback=cb) - - -@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 - """ - assert msg.sender == staticcall VAULT.admin() - self.borrow_cap = _borrow_cap - log ILlamalendController.SetBorrowCap(borrow_cap=_borrow_cap) - - -@external -def set_admin_fee(admin_fee: uint256): - """ - @param admin_fee The fee which should be no higher than MAX_ADMIN_FEE - """ - assert msg.sender == staticcall VAULT.admin() - assert admin_fee <= MAX_ADMIN_FEE, "admin_fee is higher than MAX_ADMIN_FEE" - self.admin_fee = admin_fee - - -@external -@view -def admin_fees() -> uint256: - """ - @notice Calculate the amount of fees obtained from the interest - """ - processed: uint256 = self.processed - return unsafe_sub( - max(ctrl._get_total_debt() + self.repaid, processed), processed - ) - - -@external -def collect_fees() -> uint256: - """ - @notice Collect the fees charged as a fraction of interest. - """ - _to: address = staticcall VAULT.fee_receiver() - - # Borrowing-based fees - rate_mul: uint256 = staticcall ctrl.AMM.get_rate_mul() - loan: IController.Loan = self._update_total_debt(0, rate_mul, False) - ctrl._save_rate() - - # Cumulative amount which would have been repaid if all the debt was repaid now - to_be_repaid: uint256 = loan.initial_debt + self.repaid - # Cumulative amount which was processed (admin fees have been taken from) - processed: uint256 = self.processed - # Difference between to_be_redeemed and minted amount is exactly due to interest charged - if to_be_repaid > processed: - self.processed = to_be_repaid - fees: uint256 = ( - unsafe_sub(to_be_repaid, processed) * self.admin_fee // 10**18 - ) - self.collected += fees - ctrl.transfer(ctrl.BORROWED_TOKEN, _to, fees) - log IController.CollectFees(amount=fees, new_supply=loan.initial_debt) - return fees - else: - log IController.CollectFees(amount=0, new_supply=loan.initial_debt) - return 0 diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy new file mode 100644 index 00000000..7e57d2df --- /dev/null +++ b/contracts/lending/LLController.vy @@ -0,0 +1,260 @@ +# pragma version 0.4.3 +# pragma nonreentrancy on +# pragma optimize codesize +""" +@title LlamaLend Controller +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved +""" + +from ethereum.ercs import IERC20 +from ethereum.ercs import IERC20Detailed +from contracts.interfaces import IAMM +from contracts.interfaces import ILMGauge +from contracts.interfaces import IMonetaryPolicy +from contracts.interfaces import IVault + +from contracts.interfaces import IMintController as IController + +implements: IController + +from contracts.interfaces import ILlamalendController + +implements: ILlamalendController + +from snekmate.utils import math + +# TODO rename to core +from contracts import MintController as ctrl + +initializes: ctrl + +exports: ( + ctrl.add_collateral, + ctrl.amm, + ctrl.amm_price, + ctrl.approval, + ctrl.approve, + ctrl.borrowed_token, + ctrl.calculate_debt_n1, + ctrl.collateral_token, + ctrl.debt, + ctrl.extra_health, + ctrl.health, + ctrl.health_calculator, + ctrl.liquidation_discount, + ctrl.liquidation_discounts, + ctrl.loan_discount, + ctrl.loan_exists, + ctrl.loan_ix, + ctrl.loans, + ctrl.min_collateral, + ctrl.monetary_policy, + ctrl.n_loans, + ctrl.remove_collateral, + ctrl.save_rate, + ctrl.set_extra_health, + ctrl.tokens_to_liquidate, + ctrl.total_debt, + ctrl.user_prices, + ctrl.user_state, + ctrl.users_to_liquidate, + ctrl.admin_fees, + ctrl.factory, + ctrl.liquidate, + ctrl.repay, + ctrl.set_amm_fee, + ctrl.set_borrowing_discounts, + ctrl.set_callback, + ctrl.set_monetary_policy, + ctrl.admin_fee, + # For backward compatibility + ctrl.minted, + ctrl.redeemed, + ctrl.processed, + ctrl.repaid, +) +# TODO reorder exports in a way that make sense + +VAULT: immutable(IVault) + +# TODO interface diff check + +# cumulative amount of assets ever lent +lent: public(uint256) +# cumulative amount of assets collected by admin +collected: public(uint256) + + +# TODO check this +MAX_ADMIN_FEE: constant(uint256) = 2 * 10**17 # 20% + + +@deploy +def __init__( + vault: IVault, + amm: IAMM, + collateral_token: IERC20, + borrowed_token: IERC20, + monetary_policy: IMonetaryPolicy, + loan_discount: uint256, + liquidation_discount: uint256, + borrow_cap: uint256, +): + """ + @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) + """ + # TODO set to zero + self._set_borrow_cap(borrow_cap) + VAULT = vault + + ctrl.__init__( + amm, + collateral_token, + borrowed_token, + monetary_policy, + loan_discount, + liquidation_discount, + ) + + +@external +@view +def vault() -> IVault: + """ + @notice Address of the vault + """ + return VAULT + + +@internal +@view +def _borrowed_balance() -> uint256: + # (VAULT.deposited() - VAULT.withdrawn()) - (self.lent - self.repaid) - self.collected + return ( + staticcall VAULT.deposited() + + ctrl.repaid + - staticcall VAULT.withdrawn() + - self.lent + - self.collected + ) + + +@external +@view +def borrowed_balance() -> uint256: + return self._borrowed_balance() + + +@external +@view +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 + """ + # Cannot borrow beyond the amount of coins Controller has or beyond borrow_cap + _total_debt: uint256 = ctrl._get_total_debt() + cap: uint256 = unsafe_sub(max(ctrl.borrow_cap, _total_debt), _total_debt) + cap = min(self._borrowed_balance() + current_debt, cap) + + return ctrl._max_borrowable( + collateral, + N, + cap, + current_debt, + user, + ) + + +@external +def create_loan( + collateral: uint256, + debt: uint256, + N: uint256, + _for: address = msg.sender, + callbacker: address = empty(address), + calldata: Bytes[10**4] = b"", +): + """ + @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 _for Address to create the loan for + @param callbacker Address of the callback contract + @param calldata Any data for callbacker + """ + _debt: uint256 = ctrl._create_loan( + collateral, debt, N, _for, callbacker, calldata + ) + self.lent += _debt + + +@external +def borrow_more( + collateral: uint256, + debt: uint256, + _for: address = msg.sender, + callbacker: address = empty(address), + calldata: Bytes[10**4] = b"", +): + _debt: uint256 = ctrl._borrow_more( + collateral, + debt, + _for, + callbacker, + calldata, + ) + + self.lent += _debt + + +@external +def collect_fees() -> uint256: + fees: uint256 = ctrl._collect_fees() + self.collected += fees + return fees + + +@internal +def _set_borrow_cap(_borrow_cap: uint256): + ctrl.borrow_cap = _borrow_cap + log ILlamalendController.SetBorrowCap(borrow_cap=_borrow_cap) + + +@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 + """ + ctrl._check_admin() + self._set_borrow_cap(_borrow_cap) + + +@external +def set_admin_fee(admin_fee: uint256): + """ + @param admin_fee The fee which should be no higher than MAX_ADMIN_FEE + """ + ctrl._check_admin() + assert admin_fee <= MAX_ADMIN_FEE, "admin_fee is higher than MAX_ADMIN_FEE" + ctrl.admin_fee = admin_fee From dba26582e336ad84bc7e97734da1f6ad7dc672de Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 6 Aug 2025 10:20:32 +0200 Subject: [PATCH 070/413] fix: admin fee in storage for LLController --- contracts/MintController.vy | 18 +++++++++--------- contracts/interfaces/ILlamalendController.vyi | 7 +++++++ contracts/interfaces/IMintController.vyi | 6 ------ contracts/lending/LLController.vy | 8 ++++---- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/contracts/MintController.vy b/contracts/MintController.vy index ea642953..a8361043 100644 --- a/contracts/MintController.vy +++ b/contracts/MintController.vy @@ -117,8 +117,6 @@ processed: public(uint256) # unused for mint controller as it overlaps with debt ceiling borrow_cap: uint256 -# left uninitialized at zero in mint markets -admin_fee: public(uint256) @deploy @@ -1159,10 +1157,8 @@ def repay( @internal -def _collect_fees() -> uint256: - """ - @notice Collect the fees charged as a fraction of interest. - """ +def _collect_fees(admin_fee: uint256) -> uint256: + # TODO add early termination condition for admin fee == 0 _to: address = staticcall FACTORY.fee_receiver() @@ -1179,7 +1175,7 @@ def _collect_fees() -> uint256: if to_be_repaid > processed: self.processed = to_be_repaid fees: uint256 = ( - unsafe_sub(to_be_repaid, processed) * self.admin_fee // 10**18 + unsafe_sub(to_be_repaid, processed) * admin_fee // 10**18 ) self.transfer(BORROWED_TOKEN, _to, fees) log IController.CollectFees(amount=fees, new_supply=loan.initial_debt) @@ -1224,8 +1220,12 @@ def save_rate(): @external def collect_fees() -> uint256: - fees: uint256 = self._collect_fees() - return fees + """ + @notice Collect the fees charged as interest. + """ + # In mint controller, 100% (WAD) fees are + # collected as admin fees. + return self._collect_fees(WAD) @external diff --git a/contracts/interfaces/ILlamalendController.vyi b/contracts/interfaces/ILlamalendController.vyi index 00528069..958322cb 100644 --- a/contracts/interfaces/ILlamalendController.vyi +++ b/contracts/interfaces/ILlamalendController.vyi @@ -33,3 +33,10 @@ def lent() -> uint256: def collected() -> uint256: ... + +@view +@external +def admin_fee_percentage() -> uint256: + ... + + diff --git a/contracts/interfaces/IMintController.vyi b/contracts/interfaces/IMintController.vyi index 4491b20a..a98a2514 100644 --- a/contracts/interfaces/IMintController.vyi +++ b/contracts/interfaces/IMintController.vyi @@ -346,9 +346,3 @@ def repaid() -> uint256: def processed() -> uint256: ... - -@view -@external -def admin_fee() -> uint256: - ... - diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy index 7e57d2df..31c8c2f0 100644 --- a/contracts/lending/LLController.vy +++ b/contracts/lending/LLController.vy @@ -67,7 +67,6 @@ exports: ( ctrl.set_borrowing_discounts, ctrl.set_callback, ctrl.set_monetary_policy, - ctrl.admin_fee, # For backward compatibility ctrl.minted, ctrl.redeemed, @@ -78,7 +77,6 @@ exports: ( VAULT: immutable(IVault) -# TODO interface diff check # cumulative amount of assets ever lent lent: public(uint256) @@ -86,6 +84,8 @@ lent: public(uint256) collected: public(uint256) +# Unlike mint markets admin fee here is can be less than 100% +admin_fee_percentage: public(uint256) # TODO check this MAX_ADMIN_FEE: constant(uint256) = 2 * 10**17 # 20% @@ -228,7 +228,7 @@ def borrow_more( @external def collect_fees() -> uint256: - fees: uint256 = ctrl._collect_fees() + fees: uint256 = ctrl._collect_fees(self.admin_fee_percentage) self.collected += fees return fees @@ -257,4 +257,4 @@ def set_admin_fee(admin_fee: uint256): """ ctrl._check_admin() assert admin_fee <= MAX_ADMIN_FEE, "admin_fee is higher than MAX_ADMIN_FEE" - ctrl.admin_fee = admin_fee + self.admin_fee_percentage = admin_fee From 9e2229698f7ce8a05d90fa785671792641b0cc43 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 6 Aug 2025 10:25:06 +0200 Subject: [PATCH 071/413] fix: borrow cap left uninitialized --- contracts/lending/LLController.vy | 3 --- 1 file changed, 3 deletions(-) diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy index 31c8c2f0..0e6e7110 100644 --- a/contracts/lending/LLController.vy +++ b/contracts/lending/LLController.vy @@ -99,7 +99,6 @@ def __init__( monetary_policy: IMonetaryPolicy, loan_discount: uint256, liquidation_discount: uint256, - borrow_cap: uint256, ): """ @notice Controller constructor deployed by the factory from blueprint @@ -110,8 +109,6 @@ def __init__( get_x_down() for "bad liquidation" purposes @param amm AMM address (Already deployed from blueprint) """ - # TODO set to zero - self._set_borrow_cap(borrow_cap) VAULT = vault ctrl.__init__( From 6b6da7d8f30ed08d69bbd15646541612f2d20d08 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 6 Aug 2025 10:41:47 +0200 Subject: [PATCH 072/413] fix: adjust args order to match factory --- contracts/lending/LLController.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy index 0e6e7110..bcf8c107 100644 --- a/contracts/lending/LLController.vy +++ b/contracts/lending/LLController.vy @@ -94,8 +94,8 @@ MAX_ADMIN_FEE: constant(uint256) = 2 * 10**17 # 20% def __init__( vault: IVault, amm: IAMM, - collateral_token: IERC20, borrowed_token: IERC20, + collateral_token: IERC20, monetary_policy: IMonetaryPolicy, loan_discount: uint256, liquidation_discount: uint256, From c142712fd62d3c8be86fc9619d5b19cf76fc3036 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 6 Aug 2025 11:13:46 +0200 Subject: [PATCH 073/413] fix: preserve compatibility with mint factory --- contracts/Controller.vy | 1592 ++++++++++++++++++++++++++++ contracts/MintController.vy | 1596 +---------------------------- contracts/lending/LLController.vy | 117 +-- 3 files changed, 1671 insertions(+), 1634 deletions(-) create mode 100644 contracts/Controller.vy diff --git a/contracts/Controller.vy b/contracts/Controller.vy new file mode 100644 index 00000000..a5352b89 --- /dev/null +++ b/contracts/Controller.vy @@ -0,0 +1,1592 @@ +# pragma version 0.4.3 +# pragma nonreentrancy on +# pragma optimize codesize +""" +@title crvUSD Controller +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2025 - all rights reserved +""" + +from contracts.interfaces import IAMM +from contracts.interfaces import IMonetaryPolicy +from contracts.interfaces import ILMGauge +from contracts.interfaces import IFactory +from ethereum.ercs import IERC20 +from ethereum.ercs import IERC20Detailed + +from contracts.interfaces import IMintController as IController + +implements: IController + +from snekmate.utils import math + +################################################################ +# IMMUTABLES # +################################################################ + +AMM: immutable(IAMM) +MAX_AMM_FEE: immutable( + uint256 +) # let's set to MIN_TICKS / A: for example, 4% max fee for A=100 +A: immutable(uint256) +Aminus1: immutable(uint256) +LOGN_A_RATIO: immutable(int256) # log(A / (A - 1)) +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 # +################################################################ + +# TODO add version + + +from contracts import constants as c + +WAD: constant(uint256) = c.WAD +DEAD_SHARES: constant(uint256) = c.DEAD_SHARES + +MIN_AMM_FEE: constant(uint256) = 10**6 # 1e-12, still needs to be above 0 +MIN_TICKS_UINT: constant(uint256) = 4 + +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, +) + +MAX_LOAN_DISCOUNT: constant(uint256) = 5 * 10**17 +MIN_LIQUIDATION_DISCOUNT: constant(uint256) = ( + 10**16 +) # Start liquidating when threshold reached +MAX_TICKS: constant(int256) = 50 +MAX_TICKS_UINT: constant(uint256) = c.MAX_TICKS_UINT +MIN_TICKS: constant(int256) = 4 +MAX_SKIP_TICKS: constant(uint256) = 1024 +MAX_P_BASE_BANDS: constant(int256) = 5 + +MAX_RATE: constant(uint256) = 43959106799 # 300% APY + +################################################################ +# STORAGE # +################################################################ + +liquidation_discount: public(uint256) +loan_discount: public(uint256) +# TODO make settable +_monetary_policy: IMonetaryPolicy +# TODO can't mark it as public, likely a compiler bug +# TODO make an issue +@external +@view +def monetary_policy() -> IMonetaryPolicy: + """ + @notice Address of the monetary policy + """ + return self._monetary_policy + + +approval: public(HashMap[address, HashMap[address, bool]]) +extra_health: public(HashMap[address, uint256]) + +loan: HashMap[address, IController.Loan] +liquidation_discounts: public(HashMap[address, uint256]) +_total_debt: IController.Loan + +# TODO uniform comment style + +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 + +# cumulative amount of assets ever repaid (including admin fees) +repaid: public(uint256) +# cumulative amount of assets admin fees have been taken from +processed: public(uint256) + +# unused for mint controller as it overlaps with debt ceiling +borrow_cap: uint256 + + +@deploy +def __init__( + _collateral_token: IERC20, + _borrowed_token: IERC20, + monetary_policy: IMonetaryPolicy, + loan_discount: uint256, + liquidation_discount: uint256, + _AMM: IAMM, +): + # In MintController the correct way to limit borrowing + # is through the debt ceiling. + self.borrow_cap = max_value(uint256) + + FACTORY = IFactory(msg.sender) + AMM = _AMM + + A = staticcall AMM.A() + Aminus1 = A - 1 + + # TODO check math (removed unsafe) + LOGN_A_RATIO = math._wad_ln(convert(A * WAD // A - 1, int256)) + # TODO check math + SQRT_BAND_RATIO = isqrt(10**36 * A // (A - 1)) + + MAX_AMM_FEE = min(WAD * MIN_TICKS_UINT // A, 10**17) + + COLLATERAL_TOKEN = _collateral_token + collateral_decimals: uint256 = convert( + staticcall IERC20Detailed(COLLATERAL_TOKEN.address).decimals(), uint256 + ) + COLLATERAL_PRECISION = pow_mod256(10, 18 - collateral_decimals) + + BORROWED_TOKEN = _borrowed_token + borrowed_decimals: uint256 = convert( + staticcall IERC20Detailed(BORROWED_TOKEN.address).decimals(), uint256 + ) + BORROWED_PRECISION = pow_mod256(10, 18 - borrowed_decimals) + + self._monetary_policy = monetary_policy + self.liquidation_discount = liquidation_discount + self.loan_discount = loan_discount + self._total_debt.rate_mul = 10**18 + + # TODO check what this is needed for + assert extcall BORROWED_TOKEN.approve( + msg.sender, max_value(uint256), default_return_value=True + ) + + +@view +@external +def minted() -> uint256: + return self.processed + + +@view +@external +def redeemed() -> uint256: + return self.repaid + + +@external +@view +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 + """ + # Cannot borrow beyond the amount of coins Controller has + cap: uint256 = staticcall BORROWED_TOKEN.balanceOf(self) + current_debt + + return self._max_borrowable( + collateral, + N, + cap, + current_debt, + user, + ) + + +################################################################ +# BUILDING BLOCKS # +################################################################ + + +@internal +@view +def _debt(user: address) -> (uint256, uint256): + """ + @notice Get the value of debt and rate_mul and update the rate_mul counter + @param user User address + @return (debt, rate_mul) + """ + rate_mul: uint256 = staticcall AMM.get_rate_mul() + loan: IController.Loan = self.loan[user] + if loan.initial_debt == 0: + return (0, rate_mul) + else: + # Let user repay 1 smallest decimal more so that the system doesn't lose on precision + # Use ceil div + debt: uint256 = loan.initial_debt * rate_mul + 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 + return (debt, rate_mul) + + +@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 +@view +def get_y_effective( + collateral: uint256, N: uint256, discount: uint256 +) -> uint256: + """ + @notice Intermediary method which calculates y_effective defined as x_effective / p_base, + however discounted by loan_discount. + x_effective is an amount which can be obtained from collateral when liquidating + @param collateral Amount of collateral to get the value for + @param N Number of bands the deposit is made into + @param discount Loan discount at 1e18 base (e.g. 1e18 == 100%) + @return y_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_effective * p_oracle_up(n1) + # d_y_effective = y / N / sqrt(A / (A - 1)) + # 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, + ), + ), + unsafe_mul(SQRT_BAND_RATIO, N), + ) + y_effective: uint256 = d_y_effective + for i: uint256 in range(1, MAX_TICKS_UINT): + if i == N: + break + d_y_effective = unsafe_div(d_y_effective * Aminus1, 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: + """ + @notice Calculate the upper band number for the deposit to sit in to support + the given debt. 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 + @return Upper band n1 (n1 <= n2) to deposit into. Signed integer + """ + assert debt > 0, "No loan" + 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], + ) + # p_oracle_up(n1) = base_price * ((A - 1) / A)**n1 + + # We borrow up until min band touches p_oracle, + # or it touches non-empty bands which cannot be skipped. + # We calculate required n1 for given (collateral, debt), + # and if n1 corresponds to price_oracle being too high, or unreachable band + # - 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 + + # 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 = 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_div(n1, LOGN_A_RATIO) + + n1 = min(n1, 1024 - convert(N, int256)) + n0 + if n1 <= n0: + assert staticcall AMM.can_skip_bands(n1 - 1), "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 = 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() * 10**18 // 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 = unsafe_div(p_base * A, Aminus1) + if p_base > p_oracle: + return p_base_prev + return p_base + + +@internal +@view +def _check_approval(_for: address) -> bool: + return msg.sender == _for or self.approval[_for][msg.sender] + + +@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 + ) + + return f_remove + + +@internal +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 + self.loan_ix[_for] = 0 + if loan_ix < last_loan_ix: # Need to replace + last_loan: address = self.loans[last_loan_ix] + self.loans[loan_ix] = last_loan + self.loan_ix[last_loan] = loan_ix + self.n_loans = last_loan_ix + + +@internal +def transferFrom(token: IERC20, _from: address, _to: address, amount: uint256): + if amount > 0: + assert extcall token.transferFrom( + _from, _to, amount, default_return_value=True + ) + + +@internal +def transfer(token: IERC20, _to: address, amount: uint256): + if amount > 0: + assert extcall token.transfer(_to, amount, default_return_value=True) + + +@internal +@view +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 + @param user User address to calculate health for + @param debt The amount of debt to calculate health for + @param full Whether to take into account the price difference above the highest user's band + @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(staticcall AMM.get_x_down(user), int256) * health, + convert(debt, int256), + ) + - 10**18 + ) + + if full: + 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) + * (staticcall AMM.get_sum_xy(user))[1] + * COLLATERAL_PRECISION, + debt * BORROWED_PRECISION, + ), + int256, + ) + return health + + +@internal +def _save_rate(): + """ + @notice Save current rate + """ + rate: uint256 = min(extcall self._monetary_policy.rate_write(), MAX_RATE) + extcall AMM.set_rate(rate) + + +@internal +def execute_callback( + callbacker: address, + callback_sig: bytes4, + user: address, + stablecoins: uint256, + collateral: uint256, + debt: uint256, + calldata: Bytes[10**4], +) -> IController.CallbackData: + assert callbacker != COLLATERAL_TOKEN.address + assert callbacker != BORROWED_TOKEN.address + + 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, calldata), + ), + max_outsize=64, + ) + data.stablecoins = convert(slice(response, 0, 32), uint256) + data.collateral = convert(slice(response, 32, 32), uint256) + + # Checks after callback + 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 +@view +def _check_admin(): + assert msg.sender == staticcall FACTORY.admin(), "only admin" + + +@external +def liquidate( + user: address, + min_x: uint256, + _frac: uint256, + callbacker: address, + calldata: Bytes[10**4], +): + """ + @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 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" + + final_debt: uint256 = debt + # TODO shouldn't clamp max + frac: uint256 = min(_frac, 10**18) + # TODO use wads + debt = unsafe_div(debt * frac + (10**18 - 1), 10**18) + assert debt > 0 + final_debt = unsafe_sub(final_debt, debt) + + # Withdraw sender's stablecoin 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] = 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) + + 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]) + # Request what's left from user + self.transferFrom(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]) + # Callback + cb: IController.CallbackData = self.execute_callback( + callbacker, + CALLBACK_LIQUIDATE, + user, + xy[0], + xy[1], + debt, + calldata, + ) + 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 + ) + else: + # Withdraw collateral + self.transferFrom(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), + ) + self.loan[user] = IController.Loan( + initial_debt=final_debt, rate_mul=rate_mul + ) + log IController.Repay( + user=user, collateral_decrease=xy[1], loan_decrease=debt + ) + log IController.Liquidate( + liquidator=msg.sender, + user=user, + collateral_received=xy[1], + stablecoin_received=xy[0], + debt=debt, + ) + if final_debt == 0: + 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) + + self._update_total_debt(debt, rate_mul, False) + + self.repaid += debt + self._save_rate() + + +@internal +def _add_collateral_borrow( + d_collateral: uint256, + d_debt: uint256, + _for: address, + remove_collateral: bool, + check_rounding: bool, +): + """ + @notice Internal method to borrow and add or remove collateral + @param d_collateral Amount of collateral to add + @param d_debt Amount of debt increase + @param _for Address to transfer tokens to + @param remove_collateral Remove collateral instead of adding + @param check_rounding Check that amount added is no less than the rounding error on the loan + """ + debt: uint256 = 0 + rate_mul: uint256 = 0 + debt, rate_mul = self._debt(_for) + assert debt > 0, "Loan doesn't exist" + debt += d_debt + + xy: uint256[2] = extcall AMM.withdraw(_for, 10**18) + assert xy[0] == 0, "Already in underwater mode" + if remove_collateral: + xy[1] -= d_collateral + else: + xy[1] += d_collateral + if check_rounding: + # We need d(x + p*y) > 1 wei. For that, we do an equivalent check (but with x2 for safety) + # 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 * staticcall AMM.price_oracle() + > 2 * 10**18 * 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]) + + 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: + liquidation_discount = self.liquidation_discount + self.liquidation_discounts[_for] = liquidation_discount + else: + liquidation_discount = self.liquidation_discounts[_for] + + if d_debt != 0: + self._update_total_debt(d_debt, rate_mul, True) + + if remove_collateral: + log IController.RemoveCollateral( + user=_for, collateral_decrease=d_collateral + ) + else: + 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, + ) + + +@external +def borrow_more( + collateral: uint256, + debt: uint256, + _for: address = msg.sender, + callbacker: address = empty(address), + calldata: Bytes[10**4] = b"", +): + """ + @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 _for Address to borrow for + @param callbacker Address of the callback contract + @param calldata Any data for callbacker + """ + _debt: uint256 = self._borrow_more( + collateral, + debt, + _for, + callbacker, + calldata, + ) + + +@internal +def _borrow_more( + collateral: uint256, + debt: uint256, + _for: address = msg.sender, + callbacker: address = empty(address), + calldata: Bytes[10**4] = b"", +) -> uint256: + if debt == 0: + return 0 + assert self._check_approval(_for) + + more_collateral: uint256 = 0 + if callbacker != empty(address): + self.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 + + self._add_collateral_borrow( + collateral + more_collateral, debt, _for, False, False + ) + + self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) + if more_collateral > 0: + self.transferFrom( + COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral + ) + if callbacker == empty(address): + self.transfer(BORROWED_TOKEN, _for, debt) + + self.processed += debt + self._save_rate() + + return debt + + +@internal +def _update_total_debt( + d_debt: uint256, rate_mul: uint256, is_increase: bool +) -> IController.Loan: + """ + @param d_debt Change in debt amount (unsigned) + @param rate_mul New rate_mul + @param is_increase Whether debt increases or decreases + @notice Update total debt of this controller + """ + loan: IController.Loan = self._total_debt + loan.initial_debt = loan.initial_debt * rate_mul // loan.rate_mul + if is_increase: + loan.initial_debt += d_debt + assert loan.initial_debt <= self.borrow_cap, "Borrow cap exceeded" + else: + loan.initial_debt = unsafe_sub(max(loan.initial_debt, d_debt), d_debt) + loan.rate_mul = rate_mul + self._total_debt = loan + + return loan + + +@internal +@view +def _max_borrowable( + collateral: uint256, + N: uint256, + cap: uint256, + current_debt: uint256 = 0, + user: address = empty(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(), 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, cap) + + +@external +def set_callback(cb: ILMGauge): + """ + @notice Set liquidity mining callback + """ + self._check_admin() + extcall AMM.set_callback(cb) + log IController.SetLMCallback(callback=cb) + + +@external +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 + """ + 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 IController.SetBorrowingDiscounts( + loan_discount=loan_discount, liquidation_discount=liquidation_discount + ) + + +@external +def set_monetary_policy(monetary_policy: IMonetaryPolicy): + """ + @notice Set monetary policy contract + @param monetary_policy Address of the monetary policy contract + """ + self._check_admin() + self._monetary_policy = monetary_policy + extcall monetary_policy.rate_write() + log IController.SetMonetaryPolicy(monetary_policy=monetary_policy) + + +@external +def create_loan( + collateral: uint256, + debt: uint256, + N: uint256, + _for: address = msg.sender, + callbacker: address = empty(address), + calldata: Bytes[10**4] = b"", +): + """ + @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 _for Address to create the loan for + @param callbacker Address of the callback contract + @param calldata Any data for callbacker + """ + _debt: uint256 = self._create_loan( + collateral, debt, N, _for, callbacker, calldata + ) + + +@internal +def _create_loan( + collateral: uint256, + debt: uint256, + N: uint256, + _for: address, + callbacker: address = empty(address), + calldata: Bytes[10**4] = b"", +) -> uint256: + 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) + + more_collateral: uint256 = 0 + if callbacker != empty(address): + self.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 + + collateral = collateral + more_collateral + + 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" + + n1: int256 = self._calculate_debt_n1(collateral, debt, N, _for) + n2: int256 = n1 + convert(unsafe_sub(N, 1), int256) + + 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 + + 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._update_total_debt(debt, rate_mul, True) + + extcall AMM.deposit_range(_for, collateral, n1, n2) + + self.processed += debt + self._save_rate() + + log IController.UserState( + user=_for, + collateral=collateral, + debt=debt, + n1=n1, + n2=n2, + liquidation_discount=liquidation_discount, + ) + log IController.Borrow( + user=_for, collateral_increase=collateral, loan_increase=debt + ) + + self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) + if more_collateral > 0: + self.transferFrom( + COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral + ) + if callbacker == empty(address): + self.transfer(BORROWED_TOKEN, _for, debt) + + return debt + + +@external +@reentrant +def set_amm_fee(fee: uint256): + """ + @notice Set the AMM fee (factory admin only) + @dev Reentrant because AMM is nonreentrant TODO check this one + @param fee The fee which should be no higher than MAX_AMM_FEE + """ + self._check_admin() + assert fee <= MAX_AMM_FEE and fee >= MIN_AMM_FEE, "Fee" + extcall AMM.set_fee(fee) + + +@external +def repay( + _d_debt: uint256, + _for: address = msg.sender, + max_active_band: int256 = max_value(int256), + callbacker: address = empty(address), + calldata: Bytes[10**4] = b"", +): + """ + @notice Repay debt (partially or fully) + @param _d_debt The amount of debt to repay from user's wallet. 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) + @param callbacker Address of the callback contract + @param calldata Any data for callbacker + """ + debt: uint256 = 0 + rate_mul: uint256 = 0 + debt, rate_mul = self._debt(_for) + assert debt > 0, "Loan doesn't exist" + approval: bool = self._check_approval(_for) + xy: uint256[2] = empty(uint256[2]) + + cb: IController.CallbackData = empty(IController.CallbackData) + if callbacker != empty(address): + assert approval + xy = extcall AMM.withdraw(_for, 10**18) + self.transferFrom(COLLATERAL_TOKEN, AMM.address, callbacker, xy[1]) + cb = self.execute_callback( + callbacker, CALLBACK_REPAY, _for, xy[0], xy[1], debt, calldata + ) + + total_stablecoins: uint256 = _d_debt + xy[0] + cb.stablecoins + assert total_stablecoins > 0 # dev: no coins to repay + 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 + if callbacker == empty(address): + xy = extcall 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, self, xy[0]) + if cb.stablecoins > 0: + self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins) + if _d_debt > 0: + self.transferFrom(BORROWED_TOKEN, msg.sender, self, _d_debt) + + if total_stablecoins > d_debt: + self.transfer( + BORROWED_TOKEN, _for, unsafe_sub(total_stablecoins, d_debt) + ) + # Transfer collateral to _for + if callbacker == empty(address): + if xy[1] > 0: + self.transferFrom(COLLATERAL_TOKEN, AMM.address, _for, xy[1]) + else: + if cb.collateral > 0: + self.transferFrom( + COLLATERAL_TOKEN, callbacker, _for, cb.collateral + ) + self._remove_from_list(_for) + 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=d_debt + ) + # Else - partial repayment + else: + active_band: int256 = staticcall AMM.active_band_with_skip() + assert active_band <= max_active_band + + d_debt = total_stablecoins + debt = unsafe_sub(debt, d_debt) + ns: int256[2] = staticcall 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 soft-liquidation - can use callback and move bands + new_collateral: uint256 = cb.collateral + if callbacker == empty(address): + xy = extcall AMM.withdraw(_for, 10**18) + new_collateral = xy[1] + ns[0] = self._calculate_debt_n1( + new_collateral, + 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 - cannot use callback or move bands but can avoid a bad liquidation + xy = staticcall AMM.get_sum_xy(_for) + assert callbacker == empty(address) + + 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, debt, False, liquidation_discount) > 0 + + if cb.stablecoins > 0: + self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins) + if _d_debt > 0: + self.transferFrom(BORROWED_TOKEN, msg.sender, self, _d_debt) + + log IController.UserState( + user=_for, + collateral=xy[1], + debt=debt, + n1=ns[0], + n2=ns[1], + liquidation_discount=liquidation_discount, + ) + log IController.Repay( + user=_for, collateral_decrease=0, loan_decrease=d_debt + ) + + self.loan[_for] = IController.Loan(initial_debt=debt, rate_mul=rate_mul) + self._update_total_debt(d_debt, rate_mul, False) + # TODO unify naming between debt and d_debt + self.repaid += d_debt + + self._save_rate() + + +@internal +def _collect_fees(admin_fee: uint256) -> uint256: + + # TODO add early termination condition for admin fee == 0 + _to: address = staticcall FACTORY.fee_receiver() + + # Borrowing-based fees + rate_mul: uint256 = staticcall AMM.get_rate_mul() + loan: IController.Loan = self._update_total_debt(0, rate_mul, False) + self._save_rate() + + # Cumulative amount which would have been repaid if all the debt was repaid now + to_be_repaid: uint256 = loan.initial_debt + self.repaid + # Cumulative amount which was processed (admin fees have been taken from) + processed: uint256 = self.processed + # Difference between to_be_repaid and processed amount is exactly due to interest charged + if to_be_repaid > processed: + self.processed = to_be_repaid + fees: uint256 = ( + unsafe_sub(to_be_repaid, processed) * admin_fee // 10**18 + ) + self.transfer(BORROWED_TOKEN, _to, fees) + log IController.CollectFees(amount=fees, new_supply=loan.initial_debt) + return fees + else: + log IController.CollectFees(amount=0, new_supply=loan.initial_debt) + return 0 + + +################################################################ +# FIGURE OUT A SECTION NAME # +################################################################ + +@external +def approve(_spender: address, _allow: bool): + """ + @notice Allow another address to borrow and repay for the user + @param _spender Address to whitelist for the action + @param _allow Whether to turn the approval on or off (no amounts) + """ + self.approval[msg.sender][_spender] = _allow + log IController.Approval(owner=msg.sender, spender=_spender, allow=_allow) + + +@external +def set_extra_health(_value: uint256): + """ + @notice Add a little bit more to loan_discount to start SL with health higher than usual + @param _value 1e18-based addition to loan_discount + """ + self.extra_health[msg.sender] = _value + log IController.SetExtraHealth(user=msg.sender, health=_value) + + +@external +def save_rate(): + """ + @notice Save current rate + """ + self._save_rate() + + +@external +def collect_fees() -> uint256: + """ + @notice Collect the fees charged as interest. + """ + # In mint controller, 100% (WAD) fees are + # collected as admin fees. + return self._collect_fees(WAD) + + +@external +def add_collateral(collateral: uint256, _for: address = msg.sender): + """ + @notice Add extra collateral to avoid bad liqidations + @param collateral Amount of collateral to add + @param _for Address to add collateral for + """ + if collateral == 0: + return + self._add_collateral_borrow(collateral, 0, _for, False, _for != msg.sender) + self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) + self._save_rate() + + +@external +def remove_collateral(collateral: uint256, _for: address = msg.sender): + """ + @notice Remove some collateral without repaying the debt + @param collateral Amount of collateral to remove + @param _for Address to remove collateral for + """ + if collateral == 0: + return + assert self._check_approval(_for) + self._add_collateral_borrow(collateral, 0, _for, True, False) + self.transferFrom(COLLATERAL_TOKEN, AMM.address, _for, collateral) + self._save_rate() + + +################################################################ +# VIEW METHODS # +################################################################ + +@external +@view +@reentrant +def amm() -> IAMM: + """ + @notice Address of the AMM + """ + return AMM + + +@external +@view +@reentrant +def collateral_token() -> IERC20: + """ + @notice Address of the collateral token + """ + return COLLATERAL_TOKEN + + +@external +@view +@reentrant +def borrowed_token() -> IERC20: + """ + @notice Address of the borrowed token + """ + return BORROWED_TOKEN + + +@external +@view +def debt(user: address) -> uint256: + """ + @notice Get the value of debt without changing the state + @param user User address + @return Value of debt + """ + return self._debt(user)[0] + + +@external +@view +def loan_exists(user: address) -> bool: + """ + @notice Check whether there is a loan of `user` in existence + """ + return self.loan[user].initial_debt > 0 + + +@external +@view +@reentrant +def total_debt() -> uint256: + """ + @notice Total debt of this controller + @dev Marked as reentrant because used by monetary policy + # TODO check if @reentrant is actually needed + """ + return self._get_total_debt() + + +@external +@view +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 + @param user User to calculate the value for (only necessary for nonzero extra_health) + @return Minimal collateral required + """ + # 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, + ) + + +@external +@view +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. + @param collateral Amount of collateral (at its native precision) + @param debt Amount of requested debt + @param N Number of bands to deposit into + @param user User to calculate n1 for (only necessary for nonzero extra_health) + @return Upper band n1 (n1 <= n2) to deposit into. Signed integer + """ + return self._calculate_debt_n1(collateral, debt, N, user) + + +@view +@external +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 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 +@reentrant +def amm_price() -> uint256: + """ + @notice Current price from the AMM + @dev Marked as reentrant because AMM has a nonreentrant decorator + # TODO check if @reentrant is actually needed + """ + return staticcall AMM.get_p() + + +@view +@external +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) + """ + 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)[0], + convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256), + ] + + +@external +@view +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 + @param d_collateral Change in collateral amount (signed) + @param d_debt Change in debt amount (signed) + @param full Whether it's a 'full' health or not + @param N Number of bands in case loan doesn't yet exist + @return Signed health value + """ + ns: int256[2] = staticcall 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 = 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(10**18, 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, 10**18) - 10**18 + + 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 tokens_to_liquidate(user: address, frac: uint256 = 10**18) -> uint256: + """ + @notice Calculate the amount of stablecoins 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 + """ + health_limit: uint256 = 0 + if not self._check_approval(user): + health_limit = self.liquidation_discounts[user] + stablecoins: uint256 = unsafe_div( + (staticcall 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) + + return unsafe_sub(max(debt, stablecoins), stablecoins) + + +@view +@external +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] + ) + + +@view +@external +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. + @param _from Loan index to start iteration from + @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[IController.Position, 1000] = [] + for i: uint256 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] = staticcall AMM.get_sum_xy(user) + out.append( + IController.Position( + user=user, x=xy[0], y=xy[1], debt=debt, health=health + ) + ) + ix += 1 + return out + + +@external +@view +def admin_fees() -> uint256: + """ + @notice Calculate the amount of fees obtained from the interest + """ + processed: uint256 = self.processed + return unsafe_sub( + max(self._get_total_debt() + self.repaid, processed), processed + ) + + +@external +@view +def factory() -> IFactory: + """ + @notice Address of the factory + """ + return FACTORY diff --git a/contracts/MintController.vy b/contracts/MintController.vy index a8361043..9d36afc0 100644 --- a/contracts/MintController.vy +++ b/contracts/MintController.vy @@ -2,1591 +2,35 @@ # pragma nonreentrancy on # pragma optimize codesize """ -@title crvUSD Controller +@title Mint 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 This is just a simple adapter not to have to deploy a new + factory for mint markets. """ -from contracts.interfaces import IAMM -from contracts.interfaces import IMonetaryPolicy -from contracts.interfaces import ILMGauge -from contracts.interfaces import IFactory -from ethereum.ercs import IERC20 -from ethereum.ercs import IERC20Detailed +import Controller as core +initializes: core -from contracts.interfaces import IMintController as IController - -implements: IController - -from snekmate.utils import math - -################################################################ -# IMMUTABLES # -################################################################ - -AMM: immutable(IAMM) -MAX_AMM_FEE: immutable( - uint256 -) # let's set to MIN_TICKS / A: for example, 4% max fee for A=100 -A: immutable(uint256) -Aminus1: immutable(uint256) -LOGN_A_RATIO: immutable(int256) # log(A / (A - 1)) -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 # -################################################################ - -# TODO add version - - -from contracts import constants as c - -WAD: constant(uint256) = c.WAD -DEAD_SHARES: constant(uint256) = c.DEAD_SHARES - -MIN_AMM_FEE: constant(uint256) = 10**6 # 1e-12, still needs to be above 0 -MIN_TICKS_UINT: constant(uint256) = 4 - -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, -) - -MAX_LOAN_DISCOUNT: constant(uint256) = 5 * 10**17 -MIN_LIQUIDATION_DISCOUNT: constant(uint256) = ( - 10**16 -) # Start liquidating when threshold reached -MAX_TICKS: constant(int256) = 50 -MAX_TICKS_UINT: constant(uint256) = c.MAX_TICKS_UINT -MIN_TICKS: constant(int256) = 4 -MAX_SKIP_TICKS: constant(uint256) = 1024 -MAX_P_BASE_BANDS: constant(int256) = 5 - -MAX_RATE: constant(uint256) = 43959106799 # 300% APY - -################################################################ -# STORAGE # -################################################################ - -liquidation_discount: public(uint256) -loan_discount: public(uint256) -# TODO make settable -_monetary_policy: IMonetaryPolicy -# TODO can't mark it as public, likely a compiler bug -# TODO make an issue -@external -@view -def monetary_policy() -> IMonetaryPolicy: - """ - @notice Address of the monetary policy - """ - return self._monetary_policy - - -approval: public(HashMap[address, HashMap[address, bool]]) -extra_health: public(HashMap[address, uint256]) - -loan: HashMap[address, IController.Loan] -liquidation_discounts: public(HashMap[address, uint256]) -_total_debt: IController.Loan - -# TODO uniform comment style - -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 - -# cumulative amount of assets ever repaid (including admin fees) -repaid: public(uint256) -# cumulative amount of assets admin fees have been taken from -processed: public(uint256) - -# unused for mint controller as it overlaps with debt ceiling -borrow_cap: uint256 +# Usually a bad practice to expose through +# `__interface__` but this contract is just +# an adapter for the constructor of the Controller +exports: core.__interface__ @deploy def __init__( - _AMM: IAMM, - _collateral_token: IERC20, - _borrowed_token: IERC20, - monetary_policy: IMonetaryPolicy, + collateral_token: core.IERC20, + monetary_policy: core.IMonetaryPolicy, loan_discount: uint256, liquidation_discount: uint256, + amm: core.IAMM, ): - # In MintController the correct way to limit borrowing - # is through the debt ceiling. - self.borrow_cap = max_value(uint256) - - FACTORY = IFactory(msg.sender) - AMM = _AMM - - A = staticcall AMM.A() - Aminus1 = A - 1 - - # TODO check math (removed unsafe) - LOGN_A_RATIO = math._wad_ln(convert(A * WAD // A - 1, int256)) - # TODO check math - SQRT_BAND_RATIO = isqrt(10**36 * A // (A - 1)) - - MAX_AMM_FEE = min(WAD * MIN_TICKS_UINT // A, 10**17) - - COLLATERAL_TOKEN = _collateral_token - collateral_decimals: uint256 = convert( - staticcall IERC20Detailed(COLLATERAL_TOKEN.address).decimals(), uint256 - ) - COLLATERAL_PRECISION = pow_mod256(10, 18 - collateral_decimals) - - BORROWED_TOKEN = _borrowed_token - borrowed_decimals: uint256 = convert( - staticcall IERC20Detailed(BORROWED_TOKEN.address).decimals(), uint256 - ) - BORROWED_PRECISION = pow_mod256(10, 18 - borrowed_decimals) - - self._monetary_policy = monetary_policy - self.liquidation_discount = liquidation_discount - self.loan_discount = loan_discount - self._total_debt.rate_mul = 10**18 - - # TODO check what this is needed for - assert extcall BORROWED_TOKEN.approve( - msg.sender, max_value(uint256), default_return_value=True - ) - - -@view -@external -def minted() -> uint256: - return self.processed - - -@view -@external -def redeemed() -> uint256: - return self.repaid - - -@external -@view -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 - """ - # Cannot borrow beyond the amount of coins Controller has - cap: uint256 = staticcall BORROWED_TOKEN.balanceOf(self) + current_debt - - return self._max_borrowable( - collateral, - N, - cap, - current_debt, - user, - ) - - -################################################################ -# BUILDING BLOCKS # -################################################################ - - -@internal -@view -def _debt(user: address) -> (uint256, uint256): - """ - @notice Get the value of debt and rate_mul and update the rate_mul counter - @param user User address - @return (debt, rate_mul) - """ - rate_mul: uint256 = staticcall AMM.get_rate_mul() - loan: IController.Loan = self.loan[user] - if loan.initial_debt == 0: - return (0, rate_mul) - else: - # Let user repay 1 smallest decimal more so that the system doesn't lose on precision - # Use ceil div - debt: uint256 = loan.initial_debt * rate_mul - 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 - return (debt, rate_mul) - - -@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 -@view -def get_y_effective( - collateral: uint256, N: uint256, discount: uint256 -) -> uint256: - """ - @notice Intermediary method which calculates y_effective defined as x_effective / p_base, - however discounted by loan_discount. - x_effective is an amount which can be obtained from collateral when liquidating - @param collateral Amount of collateral to get the value for - @param N Number of bands the deposit is made into - @param discount Loan discount at 1e18 base (e.g. 1e18 == 100%) - @return y_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_effective * p_oracle_up(n1) - # d_y_effective = y / N / sqrt(A / (A - 1)) - # 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, - ), - ), - unsafe_mul(SQRT_BAND_RATIO, N), - ) - y_effective: uint256 = d_y_effective - for i: uint256 in range(1, MAX_TICKS_UINT): - if i == N: - break - d_y_effective = unsafe_div(d_y_effective * Aminus1, 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: - """ - @notice Calculate the upper band number for the deposit to sit in to support - the given debt. 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 - @return Upper band n1 (n1 <= n2) to deposit into. Signed integer - """ - assert debt > 0, "No loan" - 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], - ) - # p_oracle_up(n1) = base_price * ((A - 1) / A)**n1 - - # We borrow up until min band touches p_oracle, - # or it touches non-empty bands which cannot be skipped. - # We calculate required n1 for given (collateral, debt), - # and if n1 corresponds to price_oracle being too high, or unreachable band - # - 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 - - # 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 = 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_div(n1, LOGN_A_RATIO) - - n1 = min(n1, 1024 - convert(N, int256)) + n0 - if n1 <= n0: - assert staticcall AMM.can_skip_bands(n1 - 1), "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 = 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() * 10**18 // 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 = unsafe_div(p_base * A, Aminus1) - if p_base > p_oracle: - return p_base_prev - return p_base - - -@internal -@view -def _check_approval(_for: address) -> bool: - return msg.sender == _for or self.approval[_for][msg.sender] - - -@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 - ) - - return f_remove - - -@internal -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 - self.loan_ix[_for] = 0 - if loan_ix < last_loan_ix: # Need to replace - last_loan: address = self.loans[last_loan_ix] - self.loans[loan_ix] = last_loan - self.loan_ix[last_loan] = loan_ix - self.n_loans = last_loan_ix - - -@internal -def transferFrom(token: IERC20, _from: address, _to: address, amount: uint256): - if amount > 0: - assert extcall token.transferFrom( - _from, _to, amount, default_return_value=True - ) - - -@internal -def transfer(token: IERC20, _to: address, amount: uint256): - if amount > 0: - assert extcall token.transfer(_to, amount, default_return_value=True) - - -@internal -@view -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 - @param user User address to calculate health for - @param debt The amount of debt to calculate health for - @param full Whether to take into account the price difference above the highest user's band - @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(staticcall AMM.get_x_down(user), int256) * health, - convert(debt, int256), - ) - - 10**18 - ) - - if full: - 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) - * (staticcall AMM.get_sum_xy(user))[1] - * COLLATERAL_PRECISION, - debt * BORROWED_PRECISION, - ), - int256, - ) - return health - - -@internal -def _save_rate(): - """ - @notice Save current rate - """ - rate: uint256 = min(extcall self._monetary_policy.rate_write(), MAX_RATE) - extcall AMM.set_rate(rate) - - -@internal -def execute_callback( - callbacker: address, - callback_sig: bytes4, - user: address, - stablecoins: uint256, - collateral: uint256, - debt: uint256, - calldata: Bytes[10**4], -) -> IController.CallbackData: - assert callbacker != COLLATERAL_TOKEN.address - assert callbacker != BORROWED_TOKEN.address - - 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, calldata), - ), - max_outsize=64, - ) - data.stablecoins = convert(slice(response, 0, 32), uint256) - data.collateral = convert(slice(response, 32, 32), uint256) - - # Checks after callback - 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 -@view -def _check_admin(): - assert msg.sender == staticcall FACTORY.admin(), "only admin" - - -@external -def liquidate( - user: address, - min_x: uint256, - _frac: uint256, - callbacker: address, - calldata: Bytes[10**4], -): - """ - @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 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" - - final_debt: uint256 = debt - # TODO shouldn't clamp max - frac: uint256 = min(_frac, 10**18) - # TODO use wads - debt = unsafe_div(debt * frac + (10**18 - 1), 10**18) - assert debt > 0 - final_debt = unsafe_sub(final_debt, debt) - - # Withdraw sender's stablecoin 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] = 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) - - 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]) - # Request what's left from user - self.transferFrom(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]) - # Callback - cb: IController.CallbackData = self.execute_callback( - callbacker, - CALLBACK_LIQUIDATE, - user, - xy[0], - xy[1], - debt, - calldata, - ) - 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 - ) - else: - # Withdraw collateral - self.transferFrom(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), - ) - self.loan[user] = IController.Loan( - initial_debt=final_debt, rate_mul=rate_mul - ) - log IController.Repay( - user=user, collateral_decrease=xy[1], loan_decrease=debt - ) - log IController.Liquidate( - liquidator=msg.sender, - user=user, - collateral_received=xy[1], - stablecoin_received=xy[0], - debt=debt, - ) - if final_debt == 0: - 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) - - self._update_total_debt(debt, rate_mul, False) - - self.repaid += debt - self._save_rate() - - -@internal -def _add_collateral_borrow( - d_collateral: uint256, - d_debt: uint256, - _for: address, - remove_collateral: bool, - check_rounding: bool, -): - """ - @notice Internal method to borrow and add or remove collateral - @param d_collateral Amount of collateral to add - @param d_debt Amount of debt increase - @param _for Address to transfer tokens to - @param remove_collateral Remove collateral instead of adding - @param check_rounding Check that amount added is no less than the rounding error on the loan - """ - debt: uint256 = 0 - rate_mul: uint256 = 0 - debt, rate_mul = self._debt(_for) - assert debt > 0, "Loan doesn't exist" - debt += d_debt - - xy: uint256[2] = extcall AMM.withdraw(_for, 10**18) - assert xy[0] == 0, "Already in underwater mode" - if remove_collateral: - xy[1] -= d_collateral - else: - xy[1] += d_collateral - if check_rounding: - # We need d(x + p*y) > 1 wei. For that, we do an equivalent check (but with x2 for safety) - # 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 * staticcall AMM.price_oracle() - > 2 * 10**18 * 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]) - - 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: - liquidation_discount = self.liquidation_discount - self.liquidation_discounts[_for] = liquidation_discount - else: - liquidation_discount = self.liquidation_discounts[_for] - - if d_debt != 0: - self._update_total_debt(d_debt, rate_mul, True) - - if remove_collateral: - log IController.RemoveCollateral( - user=_for, collateral_decrease=d_collateral - ) - else: - 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, - ) - - -@external -def borrow_more( - collateral: uint256, - debt: uint256, - _for: address = msg.sender, - callbacker: address = empty(address), - calldata: Bytes[10**4] = b"", -): - """ - @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 _for Address to borrow for - @param callbacker Address of the callback contract - @param calldata Any data for callbacker - """ - _debt: uint256 = self._borrow_more( - collateral, - debt, - _for, - callbacker, - calldata, - ) - - -@internal -def _borrow_more( - collateral: uint256, - debt: uint256, - _for: address = msg.sender, - callbacker: address = empty(address), - calldata: Bytes[10**4] = b"", -) -> uint256: - if debt == 0: - return 0 - assert self._check_approval(_for) - - more_collateral: uint256 = 0 - if callbacker != empty(address): - self.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 - - self._add_collateral_borrow( - collateral + more_collateral, debt, _for, False, False + core.__init__( + collateral_token, + staticcall core.IFactory(msg.sender).stablecoin(), + monetary_policy, + loan_discount, + liquidation_discount, + amm, ) - - self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) - if more_collateral > 0: - self.transferFrom( - COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral - ) - if callbacker == empty(address): - self.transfer(BORROWED_TOKEN, _for, debt) - - self.processed += debt - self._save_rate() - - return debt - - -@internal -def _update_total_debt( - d_debt: uint256, rate_mul: uint256, is_increase: bool -) -> IController.Loan: - """ - @param d_debt Change in debt amount (unsigned) - @param rate_mul New rate_mul - @param is_increase Whether debt increases or decreases - @notice Update total debt of this controller - """ - loan: IController.Loan = self._total_debt - loan.initial_debt = loan.initial_debt * rate_mul // loan.rate_mul - if is_increase: - loan.initial_debt += d_debt - assert loan.initial_debt <= self.borrow_cap, "Borrow cap exceeded" - else: - loan.initial_debt = unsafe_sub(max(loan.initial_debt, d_debt), d_debt) - loan.rate_mul = rate_mul - self._total_debt = loan - - return loan - - -@internal -@view -def _max_borrowable( - collateral: uint256, - N: uint256, - cap: uint256, - current_debt: uint256 = 0, - user: address = empty(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(), 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, cap) - - -@external -def set_callback(cb: ILMGauge): - """ - @notice Set liquidity mining callback - """ - self._check_admin() - extcall AMM.set_callback(cb) - log IController.SetLMCallback(callback=cb) - - -@external -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 - """ - 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 IController.SetBorrowingDiscounts( - loan_discount=loan_discount, liquidation_discount=liquidation_discount - ) - - -@external -def set_monetary_policy(monetary_policy: IMonetaryPolicy): - """ - @notice Set monetary policy contract - @param monetary_policy Address of the monetary policy contract - """ - self._check_admin() - self._monetary_policy = monetary_policy - extcall monetary_policy.rate_write() - log IController.SetMonetaryPolicy(monetary_policy=monetary_policy) - - -@external -def create_loan( - collateral: uint256, - debt: uint256, - N: uint256, - _for: address = msg.sender, - callbacker: address = empty(address), - calldata: Bytes[10**4] = b"", -): - """ - @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 _for Address to create the loan for - @param callbacker Address of the callback contract - @param calldata Any data for callbacker - """ - _debt: uint256 = self._create_loan( - collateral, debt, N, _for, callbacker, calldata - ) - - -@internal -def _create_loan( - collateral: uint256, - debt: uint256, - N: uint256, - _for: address, - callbacker: address = empty(address), - calldata: Bytes[10**4] = b"", -) -> uint256: - 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) - - more_collateral: uint256 = 0 - if callbacker != empty(address): - self.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 - - collateral = collateral + more_collateral - - 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" - - n1: int256 = self._calculate_debt_n1(collateral, debt, N, _for) - n2: int256 = n1 + convert(unsafe_sub(N, 1), int256) - - 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 - - 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._update_total_debt(debt, rate_mul, True) - - extcall AMM.deposit_range(_for, collateral, n1, n2) - - self.processed += debt - self._save_rate() - - log IController.UserState( - user=_for, - collateral=collateral, - debt=debt, - n1=n1, - n2=n2, - liquidation_discount=liquidation_discount, - ) - log IController.Borrow( - user=_for, collateral_increase=collateral, loan_increase=debt - ) - - self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) - if more_collateral > 0: - self.transferFrom( - COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral - ) - if callbacker == empty(address): - self.transfer(BORROWED_TOKEN, _for, debt) - - return debt - - -@external -@reentrant -def set_amm_fee(fee: uint256): - """ - @notice Set the AMM fee (factory admin only) - @dev Reentrant because AMM is nonreentrant TODO check this one - @param fee The fee which should be no higher than MAX_AMM_FEE - """ - self._check_admin() - assert fee <= MAX_AMM_FEE and fee >= MIN_AMM_FEE, "Fee" - extcall AMM.set_fee(fee) - - -@external -def repay( - _d_debt: uint256, - _for: address = msg.sender, - max_active_band: int256 = max_value(int256), - callbacker: address = empty(address), - calldata: Bytes[10**4] = b"", -): - """ - @notice Repay debt (partially or fully) - @param _d_debt The amount of debt to repay from user's wallet. 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) - @param callbacker Address of the callback contract - @param calldata Any data for callbacker - """ - debt: uint256 = 0 - rate_mul: uint256 = 0 - debt, rate_mul = self._debt(_for) - assert debt > 0, "Loan doesn't exist" - approval: bool = self._check_approval(_for) - xy: uint256[2] = empty(uint256[2]) - - cb: IController.CallbackData = empty(IController.CallbackData) - if callbacker != empty(address): - assert approval - xy = extcall AMM.withdraw(_for, 10**18) - self.transferFrom(COLLATERAL_TOKEN, AMM.address, callbacker, xy[1]) - cb = self.execute_callback( - callbacker, CALLBACK_REPAY, _for, xy[0], xy[1], debt, calldata - ) - - total_stablecoins: uint256 = _d_debt + xy[0] + cb.stablecoins - assert total_stablecoins > 0 # dev: no coins to repay - 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 - if callbacker == empty(address): - xy = extcall 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, self, xy[0]) - if cb.stablecoins > 0: - self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins) - if _d_debt > 0: - self.transferFrom(BORROWED_TOKEN, msg.sender, self, _d_debt) - - if total_stablecoins > d_debt: - self.transfer( - BORROWED_TOKEN, _for, unsafe_sub(total_stablecoins, d_debt) - ) - # Transfer collateral to _for - if callbacker == empty(address): - if xy[1] > 0: - self.transferFrom(COLLATERAL_TOKEN, AMM.address, _for, xy[1]) - else: - if cb.collateral > 0: - self.transferFrom( - COLLATERAL_TOKEN, callbacker, _for, cb.collateral - ) - self._remove_from_list(_for) - 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=d_debt - ) - # Else - partial repayment - else: - active_band: int256 = staticcall AMM.active_band_with_skip() - assert active_band <= max_active_band - - d_debt = total_stablecoins - debt = unsafe_sub(debt, d_debt) - ns: int256[2] = staticcall 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 soft-liquidation - can use callback and move bands - new_collateral: uint256 = cb.collateral - if callbacker == empty(address): - xy = extcall AMM.withdraw(_for, 10**18) - new_collateral = xy[1] - ns[0] = self._calculate_debt_n1( - new_collateral, - 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 - cannot use callback or move bands but can avoid a bad liquidation - xy = staticcall AMM.get_sum_xy(_for) - assert callbacker == empty(address) - - 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, debt, False, liquidation_discount) > 0 - - if cb.stablecoins > 0: - self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins) - if _d_debt > 0: - self.transferFrom(BORROWED_TOKEN, msg.sender, self, _d_debt) - - log IController.UserState( - user=_for, - collateral=xy[1], - debt=debt, - n1=ns[0], - n2=ns[1], - liquidation_discount=liquidation_discount, - ) - log IController.Repay( - user=_for, collateral_decrease=0, loan_decrease=d_debt - ) - - self.loan[_for] = IController.Loan(initial_debt=debt, rate_mul=rate_mul) - self._update_total_debt(d_debt, rate_mul, False) - # TODO unify naming between debt and d_debt - self.repaid += d_debt - - self._save_rate() - - -@internal -def _collect_fees(admin_fee: uint256) -> uint256: - - # TODO add early termination condition for admin fee == 0 - _to: address = staticcall FACTORY.fee_receiver() - - # Borrowing-based fees - rate_mul: uint256 = staticcall AMM.get_rate_mul() - loan: IController.Loan = self._update_total_debt(0, rate_mul, False) - self._save_rate() - - # Cumulative amount which would have been repaid if all the debt was repaid now - to_be_repaid: uint256 = loan.initial_debt + self.repaid - # Cumulative amount which was processed (admin fees have been taken from) - processed: uint256 = self.processed - # Difference between to_be_repaid and processed amount is exactly due to interest charged - if to_be_repaid > processed: - self.processed = to_be_repaid - fees: uint256 = ( - unsafe_sub(to_be_repaid, processed) * admin_fee // 10**18 - ) - self.transfer(BORROWED_TOKEN, _to, fees) - log IController.CollectFees(amount=fees, new_supply=loan.initial_debt) - return fees - else: - log IController.CollectFees(amount=0, new_supply=loan.initial_debt) - return 0 - - -################################################################ -# FIGURE OUT A SECTION NAME # -################################################################ - -@external -def approve(_spender: address, _allow: bool): - """ - @notice Allow another address to borrow and repay for the user - @param _spender Address to whitelist for the action - @param _allow Whether to turn the approval on or off (no amounts) - """ - self.approval[msg.sender][_spender] = _allow - log IController.Approval(owner=msg.sender, spender=_spender, allow=_allow) - - -@external -def set_extra_health(_value: uint256): - """ - @notice Add a little bit more to loan_discount to start SL with health higher than usual - @param _value 1e18-based addition to loan_discount - """ - self.extra_health[msg.sender] = _value - log IController.SetExtraHealth(user=msg.sender, health=_value) - - -@external -def save_rate(): - """ - @notice Save current rate - """ - self._save_rate() - - -@external -def collect_fees() -> uint256: - """ - @notice Collect the fees charged as interest. - """ - # In mint controller, 100% (WAD) fees are - # collected as admin fees. - return self._collect_fees(WAD) - - -@external -def add_collateral(collateral: uint256, _for: address = msg.sender): - """ - @notice Add extra collateral to avoid bad liqidations - @param collateral Amount of collateral to add - @param _for Address to add collateral for - """ - if collateral == 0: - return - self._add_collateral_borrow(collateral, 0, _for, False, _for != msg.sender) - self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) - self._save_rate() - - -@external -def remove_collateral(collateral: uint256, _for: address = msg.sender): - """ - @notice Remove some collateral without repaying the debt - @param collateral Amount of collateral to remove - @param _for Address to remove collateral for - """ - if collateral == 0: - return - assert self._check_approval(_for) - self._add_collateral_borrow(collateral, 0, _for, True, False) - self.transferFrom(COLLATERAL_TOKEN, AMM.address, _for, collateral) - self._save_rate() - - -################################################################ -# VIEW METHODS # -################################################################ - -@external -@view -@reentrant -def amm() -> IAMM: - """ - @notice Address of the AMM - """ - return AMM - - -@external -@view -@reentrant -def collateral_token() -> IERC20: - """ - @notice Address of the collateral token - """ - return COLLATERAL_TOKEN - - -@external -@view -@reentrant -def borrowed_token() -> IERC20: - """ - @notice Address of the borrowed token - """ - return BORROWED_TOKEN - - -@external -@view -def debt(user: address) -> uint256: - """ - @notice Get the value of debt without changing the state - @param user User address - @return Value of debt - """ - return self._debt(user)[0] - - -@external -@view -def loan_exists(user: address) -> bool: - """ - @notice Check whether there is a loan of `user` in existence - """ - return self.loan[user].initial_debt > 0 - - -@external -@view -@reentrant -def total_debt() -> uint256: - """ - @notice Total debt of this controller - @dev Marked as reentrant because used by monetary policy - # TODO check if @reentrant is actually needed - """ - return self._get_total_debt() - - -@external -@view -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 - @param user User to calculate the value for (only necessary for nonzero extra_health) - @return Minimal collateral required - """ - # 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, - ) - - -@external -@view -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. - @param collateral Amount of collateral (at its native precision) - @param debt Amount of requested debt - @param N Number of bands to deposit into - @param user User to calculate n1 for (only necessary for nonzero extra_health) - @return Upper band n1 (n1 <= n2) to deposit into. Signed integer - """ - return self._calculate_debt_n1(collateral, debt, N, user) - - -@view -@external -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 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 -@reentrant -def amm_price() -> uint256: - """ - @notice Current price from the AMM - @dev Marked as reentrant because AMM has a nonreentrant decorator - # TODO check if @reentrant is actually needed - """ - return staticcall AMM.get_p() - - -@view -@external -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) - """ - 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)[0], - convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256), - ] - - -@external -@view -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 - @param d_collateral Change in collateral amount (signed) - @param d_debt Change in debt amount (signed) - @param full Whether it's a 'full' health or not - @param N Number of bands in case loan doesn't yet exist - @return Signed health value - """ - ns: int256[2] = staticcall 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 = 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(10**18, 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, 10**18) - 10**18 - - 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 tokens_to_liquidate(user: address, frac: uint256 = 10**18) -> uint256: - """ - @notice Calculate the amount of stablecoins 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 - """ - health_limit: uint256 = 0 - if not self._check_approval(user): - health_limit = self.liquidation_discounts[user] - stablecoins: uint256 = unsafe_div( - (staticcall 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) - - return unsafe_sub(max(debt, stablecoins), stablecoins) - - -@view -@external -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] - ) - - -@view -@external -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. - @param _from Loan index to start iteration from - @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[IController.Position, 1000] = [] - for i: uint256 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] = staticcall AMM.get_sum_xy(user) - out.append( - IController.Position( - user=user, x=xy[0], y=xy[1], debt=debt, health=health - ) - ) - ix += 1 - return out - - -@external -@view -def admin_fees() -> uint256: - """ - @notice Calculate the amount of fees obtained from the interest - """ - processed: uint256 = self.processed - return unsafe_sub( - max(self._get_total_debt() + self.repaid, processed), processed - ) - - -@external -@view -def factory() -> IFactory: - """ - @notice Address of the factory - """ - return FACTORY diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy index bcf8c107..a98a18ca 100644 --- a/contracts/lending/LLController.vy +++ b/contracts/lending/LLController.vy @@ -4,7 +4,7 @@ """ @title LlamaLend Controller @author Curve.Fi -@license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved +@license Copyright (c) Curve.Fi, 2020-2025 - all rights reserved """ from ethereum.ercs import IERC20 @@ -25,53 +25,54 @@ implements: ILlamalendController from snekmate.utils import math # TODO rename to core -from contracts import MintController as ctrl +from contracts import Controller as core -initializes: ctrl +# TODO rename to core +initializes: core exports: ( - ctrl.add_collateral, - ctrl.amm, - ctrl.amm_price, - ctrl.approval, - ctrl.approve, - ctrl.borrowed_token, - ctrl.calculate_debt_n1, - ctrl.collateral_token, - ctrl.debt, - ctrl.extra_health, - ctrl.health, - ctrl.health_calculator, - ctrl.liquidation_discount, - ctrl.liquidation_discounts, - ctrl.loan_discount, - ctrl.loan_exists, - ctrl.loan_ix, - ctrl.loans, - ctrl.min_collateral, - ctrl.monetary_policy, - ctrl.n_loans, - ctrl.remove_collateral, - ctrl.save_rate, - ctrl.set_extra_health, - ctrl.tokens_to_liquidate, - ctrl.total_debt, - ctrl.user_prices, - ctrl.user_state, - ctrl.users_to_liquidate, - ctrl.admin_fees, - ctrl.factory, - ctrl.liquidate, - ctrl.repay, - ctrl.set_amm_fee, - ctrl.set_borrowing_discounts, - ctrl.set_callback, - ctrl.set_monetary_policy, + core.add_collateral, + core.amm, + core.amm_price, + core.approval, + core.approve, + 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.min_collateral, + core.monetary_policy, + core.n_loans, + core.remove_collateral, + core.save_rate, + core.set_extra_health, + core.tokens_to_liquidate, + core.total_debt, + core.user_prices, + core.user_state, + core.users_to_liquidate, + core.admin_fees, + core.factory, + core.liquidate, + core.repay, + core.set_amm_fee, + core.set_borrowing_discounts, + core.set_callback, + core.set_monetary_policy, # For backward compatibility - ctrl.minted, - ctrl.redeemed, - ctrl.processed, - ctrl.repaid, + core.minted, + core.redeemed, + core.processed, + core.repaid, ) # TODO reorder exports in a way that make sense @@ -93,12 +94,12 @@ MAX_ADMIN_FEE: constant(uint256) = 2 * 10**17 # 20% @deploy def __init__( vault: IVault, - amm: IAMM, - borrowed_token: IERC20, collateral_token: IERC20, + borrowed_token: IERC20, monetary_policy: IMonetaryPolicy, loan_discount: uint256, liquidation_discount: uint256, + amm: IAMM, ): """ @notice Controller constructor deployed by the factory from blueprint @@ -111,13 +112,13 @@ def __init__( """ VAULT = vault - ctrl.__init__( - amm, + core.__init__( collateral_token, borrowed_token, monetary_policy, loan_discount, liquidation_discount, + amm, ) @@ -136,7 +137,7 @@ def _borrowed_balance() -> uint256: # (VAULT.deposited() - VAULT.withdrawn()) - (self.lent - self.repaid) - self.collected return ( staticcall VAULT.deposited() - + ctrl.repaid + + core.repaid - staticcall VAULT.withdrawn() - self.lent - self.collected @@ -166,11 +167,11 @@ def max_borrowable( @return Maximum amount of stablecoin to borrow """ # Cannot borrow beyond the amount of coins Controller has or beyond borrow_cap - _total_debt: uint256 = ctrl._get_total_debt() - cap: uint256 = unsafe_sub(max(ctrl.borrow_cap, _total_debt), _total_debt) + _total_debt: uint256 = core._get_total_debt() + cap: uint256 = unsafe_sub(max(core.borrow_cap, _total_debt), _total_debt) cap = min(self._borrowed_balance() + current_debt, cap) - return ctrl._max_borrowable( + return core._max_borrowable( collateral, N, cap, @@ -198,7 +199,7 @@ def create_loan( @param callbacker Address of the callback contract @param calldata Any data for callbacker """ - _debt: uint256 = ctrl._create_loan( + _debt: uint256 = core._create_loan( collateral, debt, N, _for, callbacker, calldata ) self.lent += _debt @@ -212,7 +213,7 @@ def borrow_more( callbacker: address = empty(address), calldata: Bytes[10**4] = b"", ): - _debt: uint256 = ctrl._borrow_more( + _debt: uint256 = core._borrow_more( collateral, debt, _for, @@ -225,14 +226,14 @@ def borrow_more( @external def collect_fees() -> uint256: - fees: uint256 = ctrl._collect_fees(self.admin_fee_percentage) + fees: uint256 = core._collect_fees(self.admin_fee_percentage) self.collected += fees return fees @internal def _set_borrow_cap(_borrow_cap: uint256): - ctrl.borrow_cap = _borrow_cap + core.borrow_cap = _borrow_cap log ILlamalendController.SetBorrowCap(borrow_cap=_borrow_cap) @@ -243,7 +244,7 @@ def set_borrow_cap(_borrow_cap: uint256): @dev Only callable by the factory admin @param _borrow_cap New borrow cap in units of borrowed_token """ - ctrl._check_admin() + core._check_admin() self._set_borrow_cap(_borrow_cap) @@ -252,6 +253,6 @@ def set_admin_fee(admin_fee: uint256): """ @param admin_fee The fee which should be no higher than MAX_ADMIN_FEE """ - ctrl._check_admin() + core._check_admin() assert admin_fee <= MAX_ADMIN_FEE, "admin_fee is higher than MAX_ADMIN_FEE" self.admin_fee_percentage = admin_fee From bbdc523cb243c540f3efba1d964f1a09d6d3816a Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 6 Aug 2025 11:15:11 +0200 Subject: [PATCH 074/413] chore: `admin_fee_percentage` -> `admin_fee` --- contracts/interfaces/ILlamalendController.vyi | 2 +- contracts/lending/LLController.vy | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/interfaces/ILlamalendController.vyi b/contracts/interfaces/ILlamalendController.vyi index 958322cb..59f7277a 100644 --- a/contracts/interfaces/ILlamalendController.vyi +++ b/contracts/interfaces/ILlamalendController.vyi @@ -36,7 +36,7 @@ def collected() -> uint256: @view @external -def admin_fee_percentage() -> uint256: +def admin_fee() -> uint256: ... diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy index a98a18ca..5af4f456 100644 --- a/contracts/lending/LLController.vy +++ b/contracts/lending/LLController.vy @@ -86,7 +86,7 @@ collected: public(uint256) # Unlike mint markets admin fee here is can be less than 100% -admin_fee_percentage: public(uint256) +admin_fee: public(uint256) # TODO check this MAX_ADMIN_FEE: constant(uint256) = 2 * 10**17 # 20% @@ -226,7 +226,7 @@ def borrow_more( @external def collect_fees() -> uint256: - fees: uint256 = core._collect_fees(self.admin_fee_percentage) + fees: uint256 = core._collect_fees(self.admin_fee) self.collected += fees return fees @@ -255,4 +255,4 @@ def set_admin_fee(admin_fee: uint256): """ core._check_admin() assert admin_fee <= MAX_ADMIN_FEE, "admin_fee is higher than MAX_ADMIN_FEE" - self.admin_fee_percentage = admin_fee + self.admin_fee = admin_fee From 256c7b22bb94851b0b21d7ffc7d8ea885a91bd15 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 6 Aug 2025 13:05:09 +0200 Subject: [PATCH 075/413] chore: minimize diff, auditors you're welcome --- contracts/Controller.vy | 1657 +++++++++++++++++++-------------------- 1 file changed, 820 insertions(+), 837 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index a5352b89..45b5708b 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -67,9 +67,7 @@ CALLBACK_LIQUIDATE: constant(bytes4) = method_id( ) 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) = c.MAX_TICKS_UINT MIN_TICKS: constant(int256) = 4 @@ -84,7 +82,6 @@ MAX_RATE: constant(uint256) = 43959106799 # 300% APY liquidation_discount: public(uint256) loan_discount: public(uint256) -# TODO make settable _monetary_policy: IMonetaryPolicy # TODO can't mark it as public, likely a compiler bug # TODO make an issue @@ -177,40 +174,104 @@ def minted() -> uint256: @view @external def redeemed() -> uint256: + # TODO add natspec return self.repaid +@internal +@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: + """ + @param d_debt Change in debt amount (unsigned) + @param rate_mul New rate_mul + @param is_increase Whether debt increases or decreases + @notice Update total debt of this controller + """ + loan: IController.Loan = self._total_debt + loan.initial_debt = loan.initial_debt * rate_mul // loan.rate_mul + if is_increase: + loan.initial_debt += d_debt + assert loan.initial_debt <= self.borrow_cap, "Borrow cap exceeded" + else: + loan.initial_debt = unsafe_sub(max(loan.initial_debt, d_debt), d_debt) + loan.rate_mul = rate_mul + self._total_debt = loan + + return loan + + @external @view -def max_borrowable( - collateral: uint256, - N: uint256, - current_debt: uint256 = 0, - user: address = empty(address), -) -> uint256: +def factory() -> IFactory: """ - @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 + @notice Address of the factory """ - # Cannot borrow beyond the amount of coins Controller has - cap: uint256 = staticcall BORROWED_TOKEN.balanceOf(self) + current_debt + return FACTORY - return self._max_borrowable( - collateral, - N, - cap, - current_debt, - user, - ) +@external +@view +@reentrant +def amm() -> IAMM: + """ + @notice Address of the AMM + """ + return AMM -################################################################ -# BUILDING BLOCKS # -################################################################ + +@external +@view +@reentrant +def collateral_token() -> IERC20: + """ + @notice Address of the collateral token + """ + return COLLATERAL_TOKEN + + +@external +@view +@reentrant +def borrowed_token() -> IERC20: + """ + @notice Address of the borrowed token + """ + return BORROWED_TOKEN + + +@internal +def _save_rate(): + """ + @notice Save current rate + """ + rate: uint256 = min(extcall self._monetary_policy.rate_write(), MAX_RATE) + extcall AMM.set_rate(rate) + + +@external +def save_rate(): + """ + @notice Save current rate + """ + self._save_rate() @internal @@ -238,15 +299,36 @@ def _debt(user: address) -> (uint256, uint256): return (debt, rate_mul) -@internal +@external @view -def _get_total_debt() -> uint256: +def debt(user: address) -> uint256: + """ + @notice Get the value of debt without changing the state + @param user User address + @return Value of debt + """ + return self._debt(user)[0] + + +@external +@view +def loan_exists(user: address) -> bool: + """ + @notice Check whether there is a loan of `user` in existence + """ + return self.loan[user].initial_debt > 0 + + +@external +@view +@reentrant +def total_debt() -> uint256: """ @notice Total debt of this controller + @dev Marked as reentrant because used by monetary policy + # TODO check if @reentrant is actually needed """ - rate_mul: uint256 = staticcall AMM.get_rate_mul() - loan: IController.Loan = self._total_debt - return loan.initial_debt * rate_mul // loan.rate_mul + return self._get_total_debt() @internal @@ -383,112 +465,141 @@ def max_p_base() -> uint256: return p_base -@internal +@external @view -def _check_approval(_for: address) -> bool: - return msg.sender == _for or self.approval[_for][msg.sender] - - -@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 - ) +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 + """ + # Cannot borrow beyond the amount of coins Controller has + cap: uint256 = staticcall BORROWED_TOKEN.balanceOf(self) + current_debt - return f_remove + return self._max_borrowable( + collateral, + N, + cap, + current_debt, + user, + ) @internal -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 - self.loan_ix[_for] = 0 - if loan_ix < last_loan_ix: # Need to replace - last_loan: address = self.loans[last_loan_ix] - self.loans[loan_ix] = last_loan - self.loan_ix[last_loan] = loan_ix - self.n_loans = last_loan_ix +@view +def _max_borrowable( + collateral: uint256, + N: uint256, + cap: uint256, + current_debt: uint256 = 0, + user: address = empty(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 -@internal -def transferFrom(token: IERC20, _from: address, _to: address, amount: uint256): - if amount > 0: - assert extcall token.transferFrom( - _from, _to, amount, default_return_value=True - ) + 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 -@internal -def transfer(token: IERC20, _to: address, amount: uint256): - if amount > 0: - assert extcall token.transfer(_to, amount, default_return_value=True) + return min(x, cap) -@internal +@external @view -def _health( - user: address, debt: uint256, full: bool, liquidation_discount: uint256 -) -> int256: +def min_collateral( + debt: uint256, N: uint256, user: address = empty(address) +) -> uint256: """ - @notice Returns position health normalized to 1e18 for the user. - Liquidation starts when < 0, however devaluation of collateral doesn't cause liquidation - @param user User address to calculate health for - @param debt The amount of debt to calculate health for - @param full Whether to take into account the price difference above the highest user's band - @param liquidation_discount Liquidation discount to use (can be 0) - @return Health: > 0 = good. + @notice Minimal amount of collateral required to support debt + @param debt The debt to support + @param N Number of bands to deposit into + @param user User to calculate the value for (only necessary for nonzero extra_health) + @return Minimal collateral required """ - assert debt > 0, "Loan doesn't exist" - health: int256 = 10**18 - convert(liquidation_discount, int256) - health = ( + # 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( - convert(staticcall AMM.get_x_down(user), int256) * health, - convert(debt, int256), + 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**18 - 10**14, ) - if full: - 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) - * (staticcall AMM.get_sum_xy(user))[1] - * COLLATERAL_PRECISION, - debt * BORROWED_PRECISION, - ), - int256, - ) - return health - -@internal -def _save_rate(): +@external +@view +def calculate_debt_n1( + collateral: uint256, + debt: uint256, + N: uint256, + user: address = empty(address), +) -> int256: """ - @notice Save current rate + @notice Calculate the upper band number for the deposit to sit in to support + the given debt. 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 user User to calculate n1 for (only necessary for nonzero extra_health) + @return Upper band n1 (n1 <= n2) to deposit into. Signed integer """ - rate: uint256 = min(extcall self._monetary_policy.rate_write(), MAX_RATE) - extcall AMM.set_rate(rate) + return self._calculate_debt_n1(collateral, debt, N, user) + + +@internal +def transferFrom(token: IERC20, _from: address, _to: address, amount: uint256): + if amount > 0: + assert extcall token.transferFrom( + _from, _to, amount, default_return_value=True + ) + + +@internal +def transfer(token: IERC20, _to: address, amount: uint256): + if amount > 0: + assert extcall token.transfer(_to, amount, default_return_value=True) @internal @@ -530,134 +641,106 @@ def execute_callback( @internal -@view -def _check_admin(): - assert msg.sender == staticcall FACTORY.admin(), "only admin" +def _create_loan( + collateral: uint256, + debt: uint256, + N: uint256, + _for: address, + callbacker: address = empty(address), + calldata: Bytes[10**4] = b"", +) -> uint256: + 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) + more_collateral: uint256 = 0 + if callbacker != empty(address): + self.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 -@external -def liquidate( - user: address, - min_x: uint256, - _frac: uint256, - callbacker: address, - calldata: Bytes[10**4], -): - """ - @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 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) + collateral = collateral + more_collateral - if health_limit != 0: - assert ( - self._health(user, debt, True, health_limit) < 0 - ), "Not enough rekt" + 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" - final_debt: uint256 = debt - # TODO shouldn't clamp max - frac: uint256 = min(_frac, 10**18) - # TODO use wads - debt = unsafe_div(debt * frac + (10**18 - 1), 10**18) - assert debt > 0 - final_debt = unsafe_sub(final_debt, debt) + n1: int256 = self._calculate_debt_n1(collateral, debt, N, _for) + n2: int256 = n1 + convert(unsafe_sub(N, 1), int256) - # Withdraw sender's stablecoin 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] = extcall AMM.withdraw( - user, self._get_f_remove(frac, health_limit) - ) # [stable, 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 - # x increase in same block -> price up -> good - # x decrease in same block -> price down -> bad - assert xy[0] >= min_x, "Slippage" + 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) - min_amm_burn: uint256 = min(xy[0], debt) - self.transferFrom(BORROWED_TOKEN, AMM.address, self, min_amm_burn) + self._update_total_debt(debt, rate_mul, True) - if debt > xy[0]: - to_repay: uint256 = unsafe_sub(debt, xy[0]) + extcall AMM.deposit_range(_for, collateral, n1, n2) - if callbacker == empty(address): - # Withdraw collateral if no callback is present - self.transferFrom(COLLATERAL_TOKEN, AMM.address, msg.sender, xy[1]) - # Request what's left from user - self.transferFrom(BORROWED_TOKEN, msg.sender, self, to_repay) + self.processed += debt + self._save_rate() - else: - # Move collateral to callbacker, call it and remove everything from it back in - self.transferFrom(COLLATERAL_TOKEN, AMM.address, callbacker, xy[1]) - # Callback - cb: IController.CallbackData = self.execute_callback( - callbacker, - CALLBACK_LIQUIDATE, - user, - xy[0], - xy[1], - debt, - calldata, - ) - 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 - ) - else: - # Withdraw collateral - self.transferFrom(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), - ) - self.loan[user] = IController.Loan( - initial_debt=final_debt, rate_mul=rate_mul - ) - log IController.Repay( - user=user, collateral_decrease=xy[1], loan_decrease=debt - ) - log IController.Liquidate( - liquidator=msg.sender, - user=user, - collateral_received=xy[1], - stablecoin_received=xy[0], + log IController.UserState( + user=_for, + collateral=collateral, debt=debt, + n1=n1, + n2=n2, + liquidation_discount=liquidation_discount, + ) + log IController.Borrow( + user=_for, collateral_increase=collateral, loan_increase=debt ) - if final_debt == 0: - 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) - self._update_total_debt(debt, rate_mul, False) + self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) + if more_collateral > 0: + self.transferFrom( + COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral + ) + if callbacker == empty(address): + self.transfer(BORROWED_TOKEN, _for, debt) - self.repaid += debt - self._save_rate() + return debt -@internal -def _add_collateral_borrow( +@external +def create_loan( + collateral: uint256, + debt: uint256, + N: uint256, + _for: address = msg.sender, + callbacker: address = empty(address), + calldata: Bytes[10**4] = b"", +): + """ + @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 _for Address to create the loan for + @param callbacker Address of the callback contract + @param calldata Any data for callbacker + """ + self._create_loan(collateral, debt, N, _for, callbacker, calldata) + + +@internal +def _add_collateral_borrow( d_collateral: uint256, d_debt: uint256, _for: address, @@ -730,6 +813,35 @@ def _add_collateral_borrow( ) +@external +def add_collateral(collateral: uint256, _for: address = msg.sender): + """ + @notice Add extra collateral to avoid bad liqidations + @param collateral Amount of collateral to add + @param _for Address to add collateral for + """ + if collateral == 0: + return + self._add_collateral_borrow(collateral, 0, _for, False, _for != msg.sender) + self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) + self._save_rate() + + +@external +def remove_collateral(collateral: uint256, _for: address = msg.sender): + """ + @notice Remove some collateral without repaying the debt + @param collateral Amount of collateral to remove + @param _for Address to remove collateral for + """ + if collateral == 0: + return + assert self._check_approval(_for) + self._add_collateral_borrow(collateral, 0, _for, True, False) + self.transferFrom(COLLATERAL_TOKEN, AMM.address, _for, collateral) + self._save_rate() + + @external def borrow_more( collateral: uint256, @@ -800,625 +912,198 @@ def _borrow_more( @internal -def _update_total_debt( - d_debt: uint256, rate_mul: uint256, is_increase: bool -) -> IController.Loan: - """ - @param d_debt Change in debt amount (unsigned) - @param rate_mul New rate_mul - @param is_increase Whether debt increases or decreases - @notice Update total debt of this controller - """ - loan: IController.Loan = self._total_debt - loan.initial_debt = loan.initial_debt * rate_mul // loan.rate_mul - if is_increase: - loan.initial_debt += d_debt - assert loan.initial_debt <= self.borrow_cap, "Borrow cap exceeded" - else: - loan.initial_debt = unsafe_sub(max(loan.initial_debt, d_debt), d_debt) - loan.rate_mul = rate_mul - self._total_debt = loan - - return loan - - -@internal -@view -def _max_borrowable( - collateral: uint256, - N: uint256, - cap: uint256, - current_debt: uint256 = 0, - user: address = empty(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(), 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, cap) - - -@external -def set_callback(cb: ILMGauge): - """ - @notice Set liquidity mining callback - """ - self._check_admin() - extcall AMM.set_callback(cb) - log IController.SetLMCallback(callback=cb) - - -@external -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 - """ - 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 IController.SetBorrowingDiscounts( - loan_discount=loan_discount, liquidation_discount=liquidation_discount - ) - - -@external -def set_monetary_policy(monetary_policy: IMonetaryPolicy): - """ - @notice Set monetary policy contract - @param monetary_policy Address of the monetary policy contract - """ - self._check_admin() - self._monetary_policy = monetary_policy - extcall monetary_policy.rate_write() - log IController.SetMonetaryPolicy(monetary_policy=monetary_policy) +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 + self.loan_ix[_for] = 0 + if loan_ix < last_loan_ix: # Need to replace + last_loan: address = self.loans[last_loan_ix] + self.loans[loan_ix] = last_loan + self.loan_ix[last_loan] = loan_ix + self.n_loans = last_loan_ix @external -def create_loan( - collateral: uint256, - debt: uint256, - N: uint256, +def repay( + _d_debt: uint256, _for: address = msg.sender, + max_active_band: int256 = max_value(int256), callbacker: address = empty(address), calldata: Bytes[10**4] = b"", ): """ - @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 _for Address to create the loan for + @notice Repay debt (partially or fully) + @param _d_debt The amount of debt to repay from user's wallet. 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) @param callbacker Address of the callback contract @param calldata Any data for callbacker """ - _debt: uint256 = self._create_loan( - collateral, debt, N, _for, callbacker, calldata - ) - - -@internal -def _create_loan( - collateral: uint256, - debt: uint256, - N: uint256, - _for: address, - callbacker: address = empty(address), - calldata: Bytes[10**4] = b"", -) -> uint256: - 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) + debt: uint256 = 0 + rate_mul: uint256 = 0 + debt, rate_mul = self._debt(_for) + assert debt > 0, "Loan doesn't exist" + approval: bool = self._check_approval(_for) + xy: uint256[2] = empty(uint256[2]) - more_collateral: uint256 = 0 + cb: IController.CallbackData = empty(IController.CallbackData) if callbacker != empty(address): - self.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 - - collateral = collateral + more_collateral + assert approval + xy = extcall AMM.withdraw(_for, 10**18) + self.transferFrom(COLLATERAL_TOKEN, AMM.address, callbacker, xy[1]) + cb = self.execute_callback( + callbacker, CALLBACK_REPAY, _for, xy[0], xy[1], debt, calldata + ) - 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" + total_stablecoins: uint256 = _d_debt + xy[0] + cb.stablecoins + assert total_stablecoins > 0 # dev: no coins to repay + d_debt: uint256 = 0 - n1: int256 = self._calculate_debt_n1(collateral, debt, N, _for) - n2: int256 = n1 + convert(unsafe_sub(N, 1), int256) + # If we have more stablecoins than the debt - full repayment and closing the position + if total_stablecoins >= debt: + d_debt = debt + debt = 0 + if callbacker == empty(address): + xy = extcall AMM.withdraw(_for, 10**18) - 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 - - 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._update_total_debt(debt, rate_mul, True) - - extcall AMM.deposit_range(_for, collateral, n1, n2) - - self.processed += debt - self._save_rate() - - log IController.UserState( - user=_for, - collateral=collateral, - debt=debt, - n1=n1, - n2=n2, - liquidation_discount=liquidation_discount, - ) - log IController.Borrow( - user=_for, collateral_increase=collateral, loan_increase=debt - ) - - self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) - if more_collateral > 0: - self.transferFrom( - COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral - ) - if callbacker == empty(address): - self.transfer(BORROWED_TOKEN, _for, debt) - - return debt - - -@external -@reentrant -def set_amm_fee(fee: uint256): - """ - @notice Set the AMM fee (factory admin only) - @dev Reentrant because AMM is nonreentrant TODO check this one - @param fee The fee which should be no higher than MAX_AMM_FEE - """ - self._check_admin() - assert fee <= MAX_AMM_FEE and fee >= MIN_AMM_FEE, "Fee" - extcall AMM.set_fee(fee) - - -@external -def repay( - _d_debt: uint256, - _for: address = msg.sender, - max_active_band: int256 = max_value(int256), - callbacker: address = empty(address), - calldata: Bytes[10**4] = b"", -): - """ - @notice Repay debt (partially or fully) - @param _d_debt The amount of debt to repay from user's wallet. 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) - @param callbacker Address of the callback contract - @param calldata Any data for callbacker - """ - debt: uint256 = 0 - rate_mul: uint256 = 0 - debt, rate_mul = self._debt(_for) - assert debt > 0, "Loan doesn't exist" - approval: bool = self._check_approval(_for) - xy: uint256[2] = empty(uint256[2]) - - cb: IController.CallbackData = empty(IController.CallbackData) - if callbacker != empty(address): - assert approval - xy = extcall AMM.withdraw(_for, 10**18) - self.transferFrom(COLLATERAL_TOKEN, AMM.address, callbacker, xy[1]) - cb = self.execute_callback( - callbacker, CALLBACK_REPAY, _for, xy[0], xy[1], debt, calldata - ) - - total_stablecoins: uint256 = _d_debt + xy[0] + cb.stablecoins - assert total_stablecoins > 0 # dev: no coins to repay - 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 - if callbacker == empty(address): - xy = extcall 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, self, xy[0]) - if cb.stablecoins > 0: - self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins) - if _d_debt > 0: - self.transferFrom(BORROWED_TOKEN, msg.sender, self, _d_debt) + if xy[0] > 0: + # Only allow full repayment when underwater for the sender to do + assert approval + self.transferFrom(BORROWED_TOKEN, AMM.address, self, xy[0]) + if cb.stablecoins > 0: + self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins) + if _d_debt > 0: + self.transferFrom(BORROWED_TOKEN, msg.sender, self, _d_debt) if total_stablecoins > d_debt: self.transfer( BORROWED_TOKEN, _for, unsafe_sub(total_stablecoins, d_debt) ) - # Transfer collateral to _for - if callbacker == empty(address): - if xy[1] > 0: - self.transferFrom(COLLATERAL_TOKEN, AMM.address, _for, xy[1]) - else: - if cb.collateral > 0: - self.transferFrom( - COLLATERAL_TOKEN, callbacker, _for, cb.collateral - ) - self._remove_from_list(_for) - 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=d_debt - ) - # Else - partial repayment - else: - active_band: int256 = staticcall AMM.active_band_with_skip() - assert active_band <= max_active_band - - d_debt = total_stablecoins - debt = unsafe_sub(debt, d_debt) - ns: int256[2] = staticcall 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 soft-liquidation - can use callback and move bands - new_collateral: uint256 = cb.collateral - if callbacker == empty(address): - xy = extcall AMM.withdraw(_for, 10**18) - new_collateral = xy[1] - ns[0] = self._calculate_debt_n1( - new_collateral, - 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 - cannot use callback or move bands but can avoid a bad liquidation - xy = staticcall AMM.get_sum_xy(_for) - assert callbacker == empty(address) - - 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, debt, False, liquidation_discount) > 0 - - if cb.stablecoins > 0: - self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins) - if _d_debt > 0: - self.transferFrom(BORROWED_TOKEN, msg.sender, self, _d_debt) - - log IController.UserState( - user=_for, - collateral=xy[1], - debt=debt, - n1=ns[0], - n2=ns[1], - liquidation_discount=liquidation_discount, - ) - log IController.Repay( - user=_for, collateral_decrease=0, loan_decrease=d_debt - ) - - self.loan[_for] = IController.Loan(initial_debt=debt, rate_mul=rate_mul) - self._update_total_debt(d_debt, rate_mul, False) - # TODO unify naming between debt and d_debt - self.repaid += d_debt - - self._save_rate() - - -@internal -def _collect_fees(admin_fee: uint256) -> uint256: - - # TODO add early termination condition for admin fee == 0 - _to: address = staticcall FACTORY.fee_receiver() - - # Borrowing-based fees - rate_mul: uint256 = staticcall AMM.get_rate_mul() - loan: IController.Loan = self._update_total_debt(0, rate_mul, False) - self._save_rate() - - # Cumulative amount which would have been repaid if all the debt was repaid now - to_be_repaid: uint256 = loan.initial_debt + self.repaid - # Cumulative amount which was processed (admin fees have been taken from) - processed: uint256 = self.processed - # Difference between to_be_repaid and processed amount is exactly due to interest charged - if to_be_repaid > processed: - self.processed = to_be_repaid - fees: uint256 = ( - unsafe_sub(to_be_repaid, processed) * admin_fee // 10**18 - ) - self.transfer(BORROWED_TOKEN, _to, fees) - log IController.CollectFees(amount=fees, new_supply=loan.initial_debt) - return fees - else: - log IController.CollectFees(amount=0, new_supply=loan.initial_debt) - return 0 - - -################################################################ -# FIGURE OUT A SECTION NAME # -################################################################ - -@external -def approve(_spender: address, _allow: bool): - """ - @notice Allow another address to borrow and repay for the user - @param _spender Address to whitelist for the action - @param _allow Whether to turn the approval on or off (no amounts) - """ - self.approval[msg.sender][_spender] = _allow - log IController.Approval(owner=msg.sender, spender=_spender, allow=_allow) - - -@external -def set_extra_health(_value: uint256): - """ - @notice Add a little bit more to loan_discount to start SL with health higher than usual - @param _value 1e18-based addition to loan_discount - """ - self.extra_health[msg.sender] = _value - log IController.SetExtraHealth(user=msg.sender, health=_value) - - -@external -def save_rate(): - """ - @notice Save current rate - """ - self._save_rate() - - -@external -def collect_fees() -> uint256: - """ - @notice Collect the fees charged as interest. - """ - # In mint controller, 100% (WAD) fees are - # collected as admin fees. - return self._collect_fees(WAD) - - -@external -def add_collateral(collateral: uint256, _for: address = msg.sender): - """ - @notice Add extra collateral to avoid bad liqidations - @param collateral Amount of collateral to add - @param _for Address to add collateral for - """ - if collateral == 0: - return - self._add_collateral_borrow(collateral, 0, _for, False, _for != msg.sender) - self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) - self._save_rate() - - -@external -def remove_collateral(collateral: uint256, _for: address = msg.sender): - """ - @notice Remove some collateral without repaying the debt - @param collateral Amount of collateral to remove - @param _for Address to remove collateral for - """ - if collateral == 0: - return - assert self._check_approval(_for) - self._add_collateral_borrow(collateral, 0, _for, True, False) - self.transferFrom(COLLATERAL_TOKEN, AMM.address, _for, collateral) - self._save_rate() - - -################################################################ -# VIEW METHODS # -################################################################ - -@external -@view -@reentrant -def amm() -> IAMM: - """ - @notice Address of the AMM - """ - return AMM - - -@external -@view -@reentrant -def collateral_token() -> IERC20: - """ - @notice Address of the collateral token - """ - return COLLATERAL_TOKEN - - -@external -@view -@reentrant -def borrowed_token() -> IERC20: - """ - @notice Address of the borrowed token - """ - return BORROWED_TOKEN - - -@external -@view -def debt(user: address) -> uint256: - """ - @notice Get the value of debt without changing the state - @param user User address - @return Value of debt - """ - return self._debt(user)[0] - - -@external -@view -def loan_exists(user: address) -> bool: - """ - @notice Check whether there is a loan of `user` in existence - """ - return self.loan[user].initial_debt > 0 - - -@external -@view -@reentrant -def total_debt() -> uint256: - """ - @notice Total debt of this controller - @dev Marked as reentrant because used by monetary policy - # TODO check if @reentrant is actually needed - """ - return self._get_total_debt() - - -@external -@view -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 - @param user User to calculate the value for (only necessary for nonzero extra_health) - @return Minimal collateral required - """ - # 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, + # Transfer collateral to _for + if callbacker == empty(address): + if xy[1] > 0: + self.transferFrom(COLLATERAL_TOKEN, AMM.address, _for, xy[1]) + else: + if cb.collateral > 0: + self.transferFrom( + COLLATERAL_TOKEN, callbacker, _for, cb.collateral + ) + self._remove_from_list(_for) + log IController.UserState( + user=_for, collateral=0, debt=0, n1=0, n2=0, liquidation_discount=0 ) - * 10**18, - 10**18 - 10**14, - ) + log IController.Repay( + user=_for, collateral_decrease=xy[1], loan_decrease=d_debt + ) + # Else - partial repayment + else: + active_band: int256 = staticcall AMM.active_band_with_skip() + assert active_band <= max_active_band + d_debt = total_stablecoins + debt = unsafe_sub(debt, d_debt) + ns: int256[2] = staticcall AMM.read_user_tick_numbers(_for) + size: int256 = unsafe_sub(ns[1], ns[0]) + liquidation_discount: uint256 = self.liquidation_discounts[_for] -@external -@view -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. - @param collateral Amount of collateral (at its native precision) - @param debt Amount of requested debt - @param N Number of bands to deposit into - @param user User to calculate n1 for (only necessary for nonzero extra_health) - @return Upper band n1 (n1 <= n2) to deposit into. Signed integer - """ - return self._calculate_debt_n1(collateral, debt, N, user) + if ns[0] > active_band: + # Not in soft-liquidation - can use callback and move bands + new_collateral: uint256 = cb.collateral + if callbacker == empty(address): + xy = extcall AMM.withdraw(_for, 10**18) + new_collateral = xy[1] + ns[0] = self._calculate_debt_n1( + new_collateral, + 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 - cannot use callback or move bands but can avoid a bad liquidation + xy = staticcall AMM.get_sum_xy(_for) + assert callbacker == empty(address) + 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, debt, False, liquidation_discount) > 0 -@view -@external -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 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]) - ] + if cb.stablecoins > 0: + self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins) + if _d_debt > 0: + self.transferFrom(BORROWED_TOKEN, msg.sender, self, _d_debt) + + log IController.UserState( + user=_for, + collateral=xy[1], + debt=debt, + n1=ns[0], + n2=ns[1], + liquidation_discount=liquidation_discount, + ) + log IController.Repay( + user=_for, collateral_decrease=0, loan_decrease=d_debt + ) + self.loan[_for] = IController.Loan(initial_debt=debt, rate_mul=rate_mul) + self._update_total_debt(d_debt, rate_mul, False) + # TODO unify naming between debt and d_debt + self.repaid += d_debt -@view -@external -@reentrant -def amm_price() -> uint256: - """ - @notice Current price from the AMM - @dev Marked as reentrant because AMM has a nonreentrant decorator - # TODO check if @reentrant is actually needed - """ - return staticcall AMM.get_p() + self._save_rate() +@internal @view -@external -def user_state(user: address) -> uint256[4]: +def _health( + user: address, debt: uint256, full: bool, liquidation_discount: uint256 +) -> int256: """ - @notice Return the user state in one call - @param user User to return the state for - @return (collateral, stablecoin, debt, N) + @notice Returns position health normalized to 1e18 for the user. + Liquidation starts when < 0, however devaluation of collateral doesn't cause liquidation + @param user User address to calculate health for + @param debt The amount of debt to calculate health for + @param full Whether to take into account the price difference above the highest user's band + @param liquidation_discount Liquidation discount to use (can be 0) + @return Health: > 0 = good. """ - 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)[0], - convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256), - ] + assert debt > 0, "Loan doesn't exist" + health: int256 = 10**18 - convert(liquidation_discount, int256) + health = ( + unsafe_div( + convert(staticcall AMM.get_x_down(user), int256) * health, + convert(debt, int256), + ) + - 10**18 + ) + + if full: + 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) + * (staticcall AMM.get_sum_xy(user))[1] + * COLLATERAL_PRECISION, + debt * BORROWED_PRECISION, + ), + int256, + ) + return health @external @@ -1476,28 +1161,169 @@ def health_calculator( int256, ) - debt *= convert(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, 10**18) - 10**18 + + 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 + + +@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 + ) + + return f_remove + + +@external +def liquidate( + user: address, + min_x: uint256, + _frac: uint256, + callbacker: address, + calldata: Bytes[10**4], +): + """ + @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 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" + + final_debt: uint256 = debt + # TODO shouldn't clamp max + frac: uint256 = min(_frac, 10**18) + # TODO use wads + debt = unsafe_div(debt * frac + (10**18 - 1), 10**18) + assert debt > 0 + final_debt = unsafe_sub(final_debt, debt) + + # Withdraw sender's stablecoin 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] = 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" - 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 - ) + min_amm_burn: uint256 = min(xy[0], debt) + self.transferFrom(BORROWED_TOKEN, AMM.address, self, min_amm_burn) - health: int256 = unsafe_div(x_eff, debt) - health = health - unsafe_div(health * ld, 10**18) - 10**18 + if debt > xy[0]: + to_repay: uint256 = unsafe_sub(debt, xy[0]) - 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 callbacker == empty(address): + # Withdraw collateral if no callback is present + self.transferFrom(COLLATERAL_TOKEN, AMM.address, msg.sender, xy[1]) + # Request what's left from user + self.transferFrom(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]) + # Callback + cb: IController.CallbackData = self.execute_callback( + callbacker, + CALLBACK_LIQUIDATE, + user, + xy[0], + xy[1], + debt, + calldata, ) - if p_diff > 0: - health += unsafe_div(p_diff * collateral, debt) - return health + 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 + ) + else: + # Withdraw collateral + self.transferFrom(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), + ) + self.loan[user] = IController.Loan( + initial_debt=final_debt, rate_mul=rate_mul + ) + log IController.Repay( + user=user, collateral_decrease=xy[1], loan_decrease=debt + ) + log IController.Liquidate( + liquidator=msg.sender, + user=user, + collateral_received=xy[1], + stablecoin_received=xy[0], + debt=debt, + ) + if final_debt == 0: + 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) + + self._update_total_debt(debt, rate_mul, False) + + self.repaid += debt + self._save_rate() @view @@ -1571,6 +1397,106 @@ def users_to_liquidate( return out +@view +@external +@reentrant +def amm_price() -> uint256: + """ + @notice Current price from the AMM + @dev Marked as reentrant because AMM has a nonreentrant decorator + # TODO check if @reentrant is actually needed + """ + return staticcall AMM.get_p() + + +@view +@external +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 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 Return the user state in one call + @param user User to return the state for + @return (collateral, stablecoin, debt, N) + """ + 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)[0], + convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256), + ] + + +@external +@reentrant +def set_amm_fee(fee: uint256): + """ + @notice Set the AMM fee (factory admin only) + @dev Reentrant because AMM is nonreentrant TODO check this one + @param fee The fee which should be no higher than MAX_AMM_FEE + """ + self._check_admin() + assert fee <= MAX_AMM_FEE and fee >= MIN_AMM_FEE, "Fee" + extcall AMM.set_fee(fee) + + +@external +def set_monetary_policy(monetary_policy: IMonetaryPolicy): + """ + @notice Set monetary policy contract + @param monetary_policy Address of the monetary policy contract + """ + self._check_admin() + self._monetary_policy = monetary_policy + extcall monetary_policy.rate_write() + log IController.SetMonetaryPolicy(monetary_policy=monetary_policy) + + +@external +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 + """ + 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 IController.SetBorrowingDiscounts( + loan_discount=loan_discount, liquidation_discount=liquidation_discount + ) + + +@external +def set_callback(cb: ILMGauge): + """ + @notice Set liquidity mining callback + """ + self._check_admin() + extcall AMM.set_callback(cb) + log IController.SetLMCallback(callback=cb) + + @external @view def admin_fees() -> uint256: @@ -1584,9 +1510,66 @@ def admin_fees() -> uint256: @external +def collect_fees() -> uint256: + """ + @notice Collect the fees charged as interest. + """ + # In mint controller, 100% (WAD) fees are + # collected as admin fees. + return self._collect_fees(WAD) + + +@internal +def _collect_fees(admin_fee: uint256) -> uint256: + + # TODO add early termination condition for admin fee == 0 + _to: address = staticcall FACTORY.fee_receiver() + + # Borrowing-based fees + rate_mul: uint256 = staticcall AMM.get_rate_mul() + loan: IController.Loan = self._update_total_debt(0, rate_mul, False) + self._save_rate() + + # Cumulative amount which would have been repaid if all the debt was repaid now + to_be_repaid: uint256 = loan.initial_debt + self.repaid + # Cumulative amount which was processed (admin fees have been taken from) + processed: uint256 = self.processed + # Difference between to_be_repaid and processed amount is exactly due to interest charged + if to_be_repaid > processed: + self.processed = to_be_repaid + fees: uint256 = ( + unsafe_sub(to_be_repaid, processed) * admin_fee // 10**18 + ) + self.transfer(BORROWED_TOKEN, _to, fees) + log IController.CollectFees(amount=fees, new_supply=loan.initial_debt) + return fees + else: + log IController.CollectFees(amount=0, new_supply=loan.initial_debt) + return 0 + + +@external +def approve(_spender: address, _allow: bool): + """ + @notice Allow another address to borrow and repay for the user + @param _spender Address to whitelist for the action + @param _allow Whether to turn the approval on or off (no amounts) + """ + self.approval[msg.sender][_spender] = _allow + log IController.Approval(owner=msg.sender, spender=_spender, allow=_allow) + + +@internal @view -def factory() -> IFactory: +def _check_approval(_for: address) -> bool: + return msg.sender == _for or self.approval[_for][msg.sender] + + +@external +def set_extra_health(_value: uint256): """ - @notice Address of the factory + @notice Add a little bit more to loan_discount to start SL with health higher than usual + @param _value 1e18-based addition to loan_discount """ - return FACTORY + self.extra_health[msg.sender] = _value + log IController.SetExtraHealth(user=msg.sender, health=_value) From ee87862ad73271fcf31012f18d79b3c879a0f65a Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 6 Aug 2025 15:07:16 +0200 Subject: [PATCH 076/413] chore: kill poetry --- poetry.lock | 3645 ------------------------------------- pyproject.toml | 39 +- tests/lending/conftest.py | 2 +- uv.lock | 1710 +++++++++++++++++ 4 files changed, 1729 insertions(+), 3667 deletions(-) delete mode 100644 poetry.lock create mode 100644 uv.lock diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 93f37518..00000000 --- a/poetry.lock +++ /dev/null @@ -1,3645 +0,0 @@ -# This file is automatically @generated by Poetry 1.8.5 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 = "3.0.0" -description = "Annotate AST trees with source code positions" -optional = false -python-versions = ">=3.8" -files = [ - {file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"}, - {file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"}, -] - -[package.extras] -astroid = ["astroid (>=2,<4)"] -test = ["astroid (>=2,<4)", "pytest", "pytest-cov", "pytest-xdist"] - -[[package]] -name = "attrs" -version = "25.3.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.8" -files = [ - {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, - {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, -] - -[package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] - -[[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 = "3.6.0" -description = "efficient arrays of booleans -- C extension" -optional = false -python-versions = "*" -files = [ - {file = "bitarray-3.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6841c08b51417f8ffe398b2828fc0593440c99525c868f640e0302476745320b"}, - {file = "bitarray-3.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a04b7a9017b8d0341ebbe77f61b74df1cf1b714f42b671a06f4912dc93d82597"}, - {file = "bitarray-3.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:664d462a4c0783fd755fe3440f07b7e46d149859c96caacadf3f28890f19a8de"}, - {file = "bitarray-3.6.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e997d22e0d1e08c8752f61675a75d93659f7aa4dbeaee54207f8d877817b4a0c"}, - {file = "bitarray-3.6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6755cfcfa7d8966e704d580c831e39818f85e7b2b7852ad22708973176f0009e"}, - {file = "bitarray-3.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4798f6744fa2633666e17b4ea8ff70250781b52a25afdbf5ffb5e176c58848f1"}, - {file = "bitarray-3.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:efa5834ba5e6c70b22afdca3894097e5a592d8d483c976359654ba990477799a"}, - {file = "bitarray-3.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d47e2bdeba4fb1986af2ba395ce51223f4d460e6e77119439e78f2b592cafade"}, - {file = "bitarray-3.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2a324e3007afb5c667026f5235b35efe3c4a95f1b83cd93aa9fce67b42f08e7c"}, - {file = "bitarray-3.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:080a7bf55c432abdae74f25dc3dbff407418346aeae1d43e31f65e8ef114f785"}, - {file = "bitarray-3.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3eb1390a8b062fe9125e5cc4c5eba990b5d383eec54f2b996e7ce73ac43150f9"}, - {file = "bitarray-3.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2020102a40edd094c0aa80e09203af71c533c41f76ce3237c99fd194a473ea33"}, - {file = "bitarray-3.6.0-cp310-cp310-win32.whl", hash = "sha256:01d6dc548e7fe5c66913c2274f44855b0f8474935acff7811e84fe1f4024c94f"}, - {file = "bitarray-3.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:8d759cecfa8aab4a1eb4e23b6420126b15c7743e85b33f389916bb98c4ecbb84"}, - {file = "bitarray-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7c20d6e6cafce5027e7092beb2ac6eec0d71045d6318b34f36e1387a8c8859a3"}, - {file = "bitarray-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cf36cadeb9c989f760a13058dbc455e5406ec3d2d247c705c8d4bc6dd1b0fcc6"}, - {file = "bitarray-3.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30ba4fba3de1dca653de41c879349ec6ca521d85cff6a7ca5d2fdd8f76c93781"}, - {file = "bitarray-3.6.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77d2368a06a86a18919c05a9b4b0ee9869f770e6a5f414b0fecc911870fe3974"}, - {file = "bitarray-3.6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a39be79a7c36e9a2e20376261c30deb3cdca86b50f7462ae9ff10a755c6720b9"}, - {file = "bitarray-3.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4695fcd37478988b1d0a16d5bc0df56dcb677fd5db37f1893d993fd3ebef914b"}, - {file = "bitarray-3.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52328192d454ca2ddad09fbc088872b014c74b22ecdd5164717dc7e6442014fa"}, - {file = "bitarray-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96117212229905da864794df9ea7bd54987c30a5dcbab3432edc3f344231adae"}, - {file = "bitarray-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:68f6e64d4867ee79e25c49d7f35b2b1f04a6d6f778176dcf5b759f3b17a02b2b"}, - {file = "bitarray-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:29ed022189a7997de46cb9bd4e2e49d6163d4f8d78dea72ac5a0e0293b856810"}, - {file = "bitarray-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e71c9dba78671d38a549e3b2d52514f50e199f9d7e18ed9b0180adeef0d04130"}, - {file = "bitarray-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ddb319f869d497ef2d3d56319360b61284a9a1d8b3de3bc936748698acfda6be"}, - {file = "bitarray-3.6.0-cp311-cp311-win32.whl", hash = "sha256:25060e7162e44242a449ed1a14a4e94b5aef340812754c443459f19c7954be91"}, - {file = "bitarray-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2d951002b11962b26afb31f758c18ad39771f287b100fa5adb1d09a47eaaf5b"}, - {file = "bitarray-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9616ea14917d06736339cf36bb9eaf4eb52110a74136b0dc5eff94e92417d22"}, - {file = "bitarray-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7e2e1ff784c2cdfd863bad31985851427f2d2796e445cec85080c7510cba4315"}, - {file = "bitarray-3.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:911b4a16dce370657e5b8d8b6ba0fbb50dd5e2b24c4416f4b9e664503d3f0502"}, - {file = "bitarray-3.6.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b47843f2f288fa746dead4394591a3432a358aaad48240283fa230d6e74b0e7"}, - {file = "bitarray-3.6.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8f95daf0ce2b24815ddf62667229ba5dfc0cfee43eb43b2549766170d0f24ae9"}, - {file = "bitarray-3.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c15b9e37bbca59657e4dcc63ad068c821a4676def15f04742c406748a0a11b9c"}, - {file = "bitarray-3.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9d247fcc33c90f2758f4162693250341e3f38cd094f64390076ef33ad0887f9"}, - {file = "bitarray-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:84bb57010a1ab76cf880424a2e0bce8dd26989849d2122ff073aa11bfc271c27"}, - {file = "bitarray-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:27d13c7b886afc5d2fc49d6e92f9c96b1f0a14dc7b5502520c29f3da7550d401"}, - {file = "bitarray-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c4e75bbf9ade3d2cdf1b607a8b353b17d9b3cf54e88b2a5a773f50ae6f1bfbc"}, - {file = "bitarray-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:975a118aa019d745f1398613b27fd8789f60a8cea057a00cdc1abedee123ffe6"}, - {file = "bitarray-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9ed4a2852b3de7a64884afcc6936db771707943249a060aec8e551c16361d478"}, - {file = "bitarray-3.6.0-cp312-cp312-win32.whl", hash = "sha256:5dd9edcab8979a50c2c4dec6d5b66789fb6f630bb52ab90a4548111075a75e48"}, - {file = "bitarray-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:552a93be286ca485914777461b384761519db313e0a7f3012dca424c9610a4d5"}, - {file = "bitarray-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3f96f57cea35ba19fd23a20b38fa0dfa3d87d582507129b8c8e314aa298f59b"}, - {file = "bitarray-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:81e84054b22babcd6c5cc1eac0de2bfc1054ecdf742720cbfb36efbe89ec6c30"}, - {file = "bitarray-3.6.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca643295bf5441dd38dadf7571ca4b63961820eedbffbe46ceba0893bf226203"}, - {file = "bitarray-3.6.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:139963494fc3dd5caee5e38c0a03783ef50be118565e94b1dbb0210770f0b32d"}, - {file = "bitarray-3.6.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:243825f56b58bef28bfc602992a8c6d09bbc625628c195498d6020120d632a09"}, - {file = "bitarray-3.6.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:583b46b3ba44121de5e87e95ae379932dc5fd2e37ebdf2c11a6d7975891425c1"}, - {file = "bitarray-3.6.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f0be27d06732e2833b672a8fcc32fa195bdb22161eb88f8890de15e30264a01"}, - {file = "bitarray-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:507e567aee4806576e20752f22533e8b7ec61e7e75062a7ce9222a0675aa0da6"}, - {file = "bitarray-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:22188943a29072b684cd7c99e0b2cfc0af317cea3366c583d820507e6d1f2ed4"}, - {file = "bitarray-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f92462ea3888c99439f58f7561ecd5dd4cf8b8b1b259ccf5376667b8c46ee747"}, - {file = "bitarray-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3800f3c8c9780f281cf590543fd4b3278fea6988202273a260ecc58136895efb"}, - {file = "bitarray-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a50a66fa34dd7f9dcdbc7602a1b7bf6f9ab030b4f43e892324193423d9ede180"}, - {file = "bitarray-3.6.0-cp313-cp313-win32.whl", hash = "sha256:afa24e5750c9b89ad5a7efef037efe49f4e339f20a94bf678c422c0c71e1207a"}, - {file = "bitarray-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4c5e7edf1e7bcbde3b52058f171a411e2a24a081b3e951d685dfea4c3c383d5"}, - {file = "bitarray-3.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fefd18b29f3b84a0cdea1d86340219d9871c3b0673a38e722a73a2c39591eaa7"}, - {file = "bitarray-3.6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:679856547f0b27b98811b73756bdf53769c23b045a6f95177cae634daabf1ddf"}, - {file = "bitarray-3.6.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51947a00ae9924584fb14c0c1b1f4c1fd916d9abd6f47582f318ab9c9cb9f3d0"}, - {file = "bitarray-3.6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0956322bf4d5e2293e57600aa929c241edf1e209e94e12483bf58c5c691432db"}, - {file = "bitarray-3.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3b521e117ab991d6b3b830656f464b354a42dbea2ca16a0e7d93d573f7ab7ff"}, - {file = "bitarray-3.6.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27eeee915258b105a21a4b0f8aebc5f77bb4dc4fb4063a09dd329fa1fdcbd234"}, - {file = "bitarray-3.6.0-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:f738051052abc95dc17f9a4c92044294a263fb7f762efdb13e528d419005c0e4"}, - {file = "bitarray-3.6.0-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:1971050b447023288a2b694a03b400bd5163829cd67b10f19e757fe87cd1161e"}, - {file = "bitarray-3.6.0-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:a290a417608f50137bec731d1f22ff3efebac72845530807a8433b2db9358c95"}, - {file = "bitarray-3.6.0-cp36-cp36m-musllinux_1_2_s390x.whl", hash = "sha256:8ef3f0977c21190f949d5cfd71ded09de87d330c6d98bd5ecb5bb1135d666d0d"}, - {file = "bitarray-3.6.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:357e07c827bad01f98d0bd0dfdc722f483febeed39140fd75ffd016a451b60b9"}, - {file = "bitarray-3.6.0-cp36-cp36m-win32.whl", hash = "sha256:bdd6412c1f38da7565126b174f4e644f362e317ef0560fac1fb9d0c70900ff4d"}, - {file = "bitarray-3.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a1b3c4ca3bec8e0ad9d32ce62444c5f3913588124a922629aa7d39357b2adf3f"}, - {file = "bitarray-3.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:531e6dfec8058fcf5d69e863b61e6b28e3749b615a4dcc0ab8ad36307c4017fc"}, - {file = "bitarray-3.6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6f9e897907757e9c2d722ae6c203d48a04826a14e1495e33935c8583c163a9"}, - {file = "bitarray-3.6.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c9f36055a89b9517db66eb8e80137126bf629c767ceeade4d004e40bc8bcd99"}, - {file = "bitarray-3.6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d6f3a94abd8b44b2bf346ca81ab2ff41ab9146c53905eedf5178b19d9fe53bf"}, - {file = "bitarray-3.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79db23eda81627132327ed292bd813a9af64399b98aaac3d42ad8deeed24cd5e"}, - {file = "bitarray-3.6.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d6c9bc14bacdfbfd51fed85f0576973eaaa7b30d81ef93264f8e22b86a9c9f7"}, - {file = "bitarray-3.6.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:60408ec9c0bd76f1fa00d28034429a0316246d31069b982a86aec8d5c99e910a"}, - {file = "bitarray-3.6.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:870ed23361e2918ab1ffc23fe0ab293abf3c372a68ee7387456d13da3e213008"}, - {file = "bitarray-3.6.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:c677849947d523a082be6e0b5c9137f443a54e951a1711ef003ec52910c41ece"}, - {file = "bitarray-3.6.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:a5ce1bdee102f7e60c075274df10b892d9ff5183ad6f5f515973eda8903dfe4c"}, - {file = "bitarray-3.6.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:a33f7c5acf44961f29018b13f0b5f5e1589ac0cfdf75a97c9774cf7ec84d09e0"}, - {file = "bitarray-3.6.0-cp37-cp37m-win32.whl", hash = "sha256:16d0edab54bb9d214319418f65bd15cfc4210ec41a16c3dd0b71e626c803212d"}, - {file = "bitarray-3.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f76784355060999c36fa807b59faecb38f5769ae58283d00270835773f95e35b"}, - {file = "bitarray-3.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cc060bc17b9de27874997d612e37d52f72092f9b59d1e04284a90ed8113cadca"}, - {file = "bitarray-3.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bfc417e58f277e949ed662d9cd050ddbb00c0dea8a828abaccc93dc357b7a6d1"}, - {file = "bitarray-3.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:129165b68a3e0c2a633ed0d8557cf5ade24a0b37ca97d7805fa6fc5fb73c19d5"}, - {file = "bitarray-3.6.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50d702149747852923be60cae125285eca8d189d4c7d8832c0c958d4071a0f78"}, - {file = "bitarray-3.6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ccf4a73e07bfbd790443d6b3c1f1447ffda23cc9391e40c035d9b7d3514b57b8"}, - {file = "bitarray-3.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7d2dbe628f3db935622a5b80a5c4d95665cdefc4904372aa3c4d786289477f"}, - {file = "bitarray-3.6.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5da4939e241301f5e1d18118695e8d2c300be90431b66bd43a00376acec45e1e"}, - {file = "bitarray-3.6.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9930853d451086c4c084d83a87294bdb0c5bc0fa4105a26c487ac09ea62e565b"}, - {file = "bitarray-3.6.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:e0e4fdeae6c0a9d886749780ec5dcf469e98f27b312efa93008d03eaa2426fd5"}, - {file = "bitarray-3.6.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:79ab1c5f26f23e51d4a44c4397c8a3bf56c306c125dfab6b3eebdfa13d1dca6f"}, - {file = "bitarray-3.6.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:b9a03767c937b621ee267507bc394df97fb2f8f61130f39f2033f3c6c191f124"}, - {file = "bitarray-3.6.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:54bd71f14a5fa9bae73ef92f2e2be894dc36c7a6d1c4962e5969bd8a9aa39325"}, - {file = "bitarray-3.6.0-cp38-cp38-win32.whl", hash = "sha256:7e0851a985a7b10f634188117c825ef99d63402555cca5bc32c7bfc5adaf0d6f"}, - {file = "bitarray-3.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:00628196dd3592972a5183194ab1475dadf9ef2a4cf3fd8c7c184a94934012e8"}, - {file = "bitarray-3.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:69d2d507c1174330c71c834b5d65e66181ad7b42b0d88b5b31804ee9b4f5dae7"}, - {file = "bitarray-3.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:727f7a969416f02ef5c1256541e06f0836fb615022699fa8e2591e85296c5570"}, - {file = "bitarray-3.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42622c42c159ea4535bba7e1e3c97f1fec79505bc6873ae657dc0a8f861c60de"}, - {file = "bitarray-3.6.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f8b12424f8fdf29d1c0749c628bd1530cecfc77374935d096cccc0e4eada232"}, - {file = "bitarray-3.6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:963cbcf296943f7017470d0b705e63e908f32b4f7dbe43f72c22f6fe1bd9ef66"}, - {file = "bitarray-3.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e304f94c0353f6ae5711533b5793b3a45b17aa2c5b07e656649b0af4e0939b5"}, - {file = "bitarray-3.6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c533c828d0007fac27cf45e5c1a711e5914dd469db5fe6be5f4e606bf2d7f63"}, - {file = "bitarray-3.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:220d4b8649ef54ac98e5e0e3dd92230247f67270d1524a8b31aa9859007affb0"}, - {file = "bitarray-3.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:407920e9318d94cc6c9611aaa5b5e5963a09f1cbfa17b16b66edea453b3754f4"}, - {file = "bitarray-3.6.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:056fe779f01a867d572e071c0944ac2f3bf58d8bced326040f0bd060af33a209"}, - {file = "bitarray-3.6.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ca87f639094c72268e17bc7f57c1225cc38f9e191a489a0134762e3fec402c1a"}, - {file = "bitarray-3.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b5ad8261f47c2a72d0f676bc40f752db8cfdcab911e970753343836e41d5a9a7"}, - {file = "bitarray-3.6.0-cp39-cp39-win32.whl", hash = "sha256:a773199dc42b5d02fcd46c8add552da2c4725ce2caa069527c7e27b5b6089e85"}, - {file = "bitarray-3.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:157313a124287cbc8a11b55a75def0dd59e68badbc82c2dc2d204dc852742874"}, - {file = "bitarray-3.6.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a763dd33d6e27c9b4db3f8089a5fa39179a8a3cf48ce702b24a857d7c621333c"}, - {file = "bitarray-3.6.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8cf44b012e7493127ce7ca6e469138ac96b3295a117877d5469aabe7c8728d87"}, - {file = "bitarray-3.6.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e297fd2e58afe17e33dd80c231c3a9d850279a2a8625aed1d39f9be9534809e"}, - {file = "bitarray-3.6.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fc8bc65f964c7278deb1b7a69379dab3ecc90095f252deb17365637ebb274d"}, - {file = "bitarray-3.6.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa3c925502bd0b957a96a5619134bcdc0382ef73cffd40bad218ced3586bcf8d"}, - {file = "bitarray-3.6.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9f7796959c9c036a115d34696563f75d4a2912d3b97c15c15f2a36bdd5496ce9"}, - {file = "bitarray-3.6.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b02cc1cac9099c0ec72da09593e7fadb1b6cf88a101acc8153592c700d732d80"}, - {file = "bitarray-3.6.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26691454a6770628882b68fe74e9f84ca2a51512edd49cbb025b14df5a9dd85a"}, - {file = "bitarray-3.6.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a5b0d277087a5bf261a607fc6ff4aaffcf80b300cd19b7a1e9754a4649f5fd4"}, - {file = "bitarray-3.6.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af670708e145b048ead87375b899229443f2d0b4af2d1450d7701c74cd932b03"}, - {file = "bitarray-3.6.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:aeb6db2f4ab54ac21a3851d05130a2aa78a6f6a5f14003f9ae3114fb8b210850"}, - {file = "bitarray-3.6.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:99d16862e802e7c50c3b6cdd1bf041b6142335c9c2b426631f731257adfe5a15"}, - {file = "bitarray-3.6.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:66d8b7a89fac6042f7df9ea97d97ed0f5e404281110a882e3babd909161f85b6"}, - {file = "bitarray-3.6.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62f71b268f14ee6cc3045b95441bfe0518cef1d0b2ffbc6f3e9758f786ff5a03"}, - {file = "bitarray-3.6.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72760411d60d8d76979a20ed3f15586d824db04668b581b86e61158c2b616db0"}, - {file = "bitarray-3.6.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc5029c61f9ebb2d4c247f13584a0ef0e8e49abb13e56460310821aca3ffcaf"}, - {file = "bitarray-3.6.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:0ac446f557eb28e3f7c65372608810ff073840627e9037e22ed10bd081793a34"}, - {file = "bitarray-3.6.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b9ae0008cff25e154ef1e3975a1705d344e844ffdeb34c25b007fd48c876e95d"}, - {file = "bitarray-3.6.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:db78cc5c03b446a43413165aa873e2f408e9fd5ddb45533e7bd3b638bace867c"}, - {file = "bitarray-3.6.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cfbdccddaa0ff07789e9e180db127906c676e479e05c04830cd458945de3511"}, - {file = "bitarray-3.6.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:222cb27ff05bc0aec72498d075dba1facec49a76a7da45740690cebbe3e81e43"}, - {file = "bitarray-3.6.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b58a672ec448fb36839a5fc7bf2b2f60df9a97b872d8bd6ca1a28da6126f5c7"}, - {file = "bitarray-3.6.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b37c9ea942395de029be270f0eca8c141eb14e8455941495cd3b6f95bbe465f4"}, - {file = "bitarray-3.6.0.tar.gz", hash = "sha256:20febc849a1f858e6a57a7d47b323fe9e727c579ddd526d317ad8831748a66a8"}, -] - -[[package]] -name = "build" -version = "1.2.2.post1" -description = "A simple, correct Python build frontend" -optional = false -python-versions = ">=3.8" -files = [ - {file = "build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5"}, - {file = "build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7"}, -] - -[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.3" -description = "httplib2 caching for requests" -optional = false -python-versions = ">=3.9" -files = [ - {file = "cachecontrol-0.14.3-py3-none-any.whl", hash = "sha256:b35e44a3113f17d2a31c1e6b27b9de6d4405f84ae51baa8c1d3cc5b633010cae"}, - {file = "cachecontrol-0.14.3.tar.gz", hash = "sha256:73e7efec4b06b20d9267b441c1f733664f989fb8688391b670ca812d70795d11"}, -] - -[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]", "build", "cherrypy", "codespell[tomli]", "furo", "mypy", "pytest", "pytest-cov", "ruff", "sphinx", "sphinx-copybutton", "tox", "types-redis", "types-requests"] -filecache = ["filelock (>=3.8.0)"] -redis = ["redis (>=2.10.5)"] - -[[package]] -name = "cached-property" -version = "2.0.1" -description = "A decorator for caching properties in classes." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cached_property-2.0.1-py3-none-any.whl", hash = "sha256:f617d70ab1100b7bcf6e42228f9ddcb78c676ffa167278d9f730d1c2fba69ccb"}, - {file = "cached_property-2.0.1.tar.gz", hash = "sha256:484d617105e3ee0e4f1f58725e72a8ef9e93deee462222dbd51cd91230897641"}, -] - -[[package]] -name = "cbor2" -version = "5.6.5" -description = "CBOR (de)serializer with extensive tag support" -optional = false -python-versions = ">=3.8" -files = [ - {file = "cbor2-5.6.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e16c4a87fc999b4926f5c8f6c696b0d251b4745bc40f6c5aee51d69b30b15ca2"}, - {file = "cbor2-5.6.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87026fc838370d69f23ed8572939bd71cea2b3f6c8f8bb8283f573374b4d7f33"}, - {file = "cbor2-5.6.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a88f029522aec5425fc2f941b3df90da7688b6756bd3f0472ab886d21208acbd"}, - {file = "cbor2-5.6.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9d15b638539b68aa5d5eacc56099b4543a38b2d2c896055dccf7e83d24b7955"}, - {file = "cbor2-5.6.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:47261f54a024839ec649b950013c4de5b5f521afe592a2688eebbe22430df1dc"}, - {file = "cbor2-5.6.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:559dcf0d897260a9e95e7b43556a62253e84550b77147a1ad4d2c389a2a30192"}, - {file = "cbor2-5.6.5-cp310-cp310-win_amd64.whl", hash = "sha256:5b856fda4c50c5bc73ed3664e64211fa4f015970ed7a15a4d6361bd48462feaf"}, - {file = "cbor2-5.6.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:863e0983989d56d5071270790e7ed8ddbda88c9e5288efdb759aba2efee670bc"}, - {file = "cbor2-5.6.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5cff06464b8f4ca6eb9abcba67bda8f8334a058abc01005c8e616728c387ad32"}, - {file = "cbor2-5.6.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4c7dbcdc59ea7f5a745d3e30ee5e6b6ff5ce7ac244aa3de6786391b10027bb3"}, - {file = "cbor2-5.6.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34cf5ab0dc310c3d0196caa6ae062dc09f6c242e2544bea01691fe60c0230596"}, - {file = "cbor2-5.6.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6797b824b26a30794f2b169c0575301ca9b74ae99064e71d16e6ba0c9057de51"}, - {file = "cbor2-5.6.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:73b9647eed1493097db6aad61e03d8f1252080ee041a1755de18000dd2c05f37"}, - {file = "cbor2-5.6.5-cp311-cp311-win_amd64.whl", hash = "sha256:6e14a1bf6269d25e02ef1d4008e0ce8880aa271d7c6b4c329dba48645764f60e"}, - {file = "cbor2-5.6.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e25c2aebc9db99af7190e2261168cdde8ed3d639ca06868e4f477cf3a228a8e9"}, - {file = "cbor2-5.6.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fde21ac1cf29336a31615a2c469a9cb03cf0add3ae480672d4d38cda467d07fc"}, - {file = "cbor2-5.6.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8947c102cac79d049eadbd5e2ffb8189952890df7cbc3ee262bbc2f95b011a9"}, - {file = "cbor2-5.6.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38886c41bebcd7dca57739439455bce759f1e4c551b511f618b8e9c1295b431b"}, - {file = "cbor2-5.6.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae2b49226224e92851c333b91d83292ec62eba53a19c68a79890ce35f1230d70"}, - {file = "cbor2-5.6.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2764804ffb6553283fc4afb10a280715905a4cea4d6dc7c90d3e89c4a93bc8d"}, - {file = "cbor2-5.6.5-cp312-cp312-win_amd64.whl", hash = "sha256:a3ac50485cf67dfaab170a3e7b527630e93cb0a6af8cdaa403054215dff93adf"}, - {file = "cbor2-5.6.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f0d0a9c5aabd48ecb17acf56004a7542a0b8d8212be52f3102b8218284bd881e"}, - {file = "cbor2-5.6.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61ceb77e6aa25c11c814d4fe8ec9e3bac0094a1f5bd8a2a8c95694596ea01e08"}, - {file = "cbor2-5.6.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97a7e409b864fecf68b2ace8978eb5df1738799a333ec3ea2b9597bfcdd6d7d2"}, - {file = "cbor2-5.6.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f6d69f38f7d788b04c09ef2b06747536624b452b3c8b371ab78ad43b0296fab"}, - {file = "cbor2-5.6.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f91e6d74fa6917df31f8757fdd0e154203b0dd0609ec53eb957016a2b474896a"}, - {file = "cbor2-5.6.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5ce13a27ef8fddf643fc17a753fe34aa72b251d03c23da6a560c005dc171085b"}, - {file = "cbor2-5.6.5-cp313-cp313-win_amd64.whl", hash = "sha256:54c72a3207bb2d4480c2c39dad12d7971ce0853a99e3f9b8d559ce6eac84f66f"}, - {file = "cbor2-5.6.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4586a4f65546243096e56a3f18f29d60752ee9204722377021b3119a03ed99ff"}, - {file = "cbor2-5.6.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d1a18b3a58dcd9b40ab55c726160d4a6b74868f2a35b71f9e726268b46dc6a2"}, - {file = "cbor2-5.6.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a83b76367d1c3e69facbcb8cdf65ed6948678e72f433137b41d27458aa2a40cb"}, - {file = "cbor2-5.6.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90bfa36944caccec963e6ab7e01e64e31cc6664535dc06e6295ee3937c999cbb"}, - {file = "cbor2-5.6.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:37096663a5a1c46a776aea44906cbe5fa3952f29f50f349179c00525d321c862"}, - {file = "cbor2-5.6.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:93676af02bd9a0b4a62c17c5b20f8e9c37b5019b1a24db70a2ee6cb770423568"}, - {file = "cbor2-5.6.5-cp38-cp38-win_amd64.whl", hash = "sha256:8f747b7a9aaa58881a0c5b4cd4a9b8fb27eca984ed261a769b61de1f6b5bd1e6"}, - {file = "cbor2-5.6.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:94885903105eec66d7efb55f4ce9884fdc5a4d51f3bd75b6fedc68c5c251511b"}, - {file = "cbor2-5.6.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fe11c2eb518c882cfbeed456e7a552e544893c17db66fe5d3230dbeaca6b615c"}, - {file = "cbor2-5.6.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66dd25dd919cddb0b36f97f9ccfa51947882f064729e65e6bef17c28535dc459"}, - {file = "cbor2-5.6.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa61a02995f3a996c03884cf1a0b5733f88cbfd7fa0e34944bf678d4227ee712"}, - {file = "cbor2-5.6.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:824f202b556fc204e2e9a67d6d6d624e150fbd791278ccfee24e68caec578afd"}, - {file = "cbor2-5.6.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7488aec919f8408f9987a3a32760bd385d8628b23a35477917aa3923ff6ad45f"}, - {file = "cbor2-5.6.5-cp39-cp39-win_amd64.whl", hash = "sha256:a34ee99e86b17444ecbe96d54d909dd1a20e2da9f814ae91b8b71cf1ee2a95e4"}, - {file = "cbor2-5.6.5-py3-none-any.whl", hash = "sha256:3038523b8fc7de312bb9cdcbbbd599987e64307c4db357cd2030c472a6c7d468"}, - {file = "cbor2-5.6.5.tar.gz", hash = "sha256:b682820677ee1dbba45f7da11898d2720f92e06be36acec290867d5ebf3d7e09"}, -] - -[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 = "2025.7.14" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.7" -files = [ - {file = "certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2"}, - {file = "certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995"}, -] - -[[package]] -name = "cffi" -version = "1.17.1" -description = "Foreign Function Interface for Python calling C code." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, - {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, - {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, - {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, - {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, - {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, - {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, - {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, - {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, - {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, - {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, - {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, - {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, - {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, - {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, - {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, -] - -[package.dependencies] -pycparser = "*" - -[[package]] -name = "charset-normalizer" -version = "3.4.2" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7" -files = [ - {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, - {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, - {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, -] - -[[package]] -name = "ckzg" -version = "2.1.1" -description = "Python bindings for C-KZG-4844" -optional = false -python-versions = "*" -files = [ - {file = "ckzg-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b9825a1458219e8b4b023012b8ef027ef1f47e903f9541cbca4615f80132730"}, - {file = "ckzg-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e2a40a3ba65cca4b52825d26829e6f7eb464aa27a9e9efb6b8b2ce183442c741"}, - {file = "ckzg-2.1.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1d753fbe85be7c21602eddc2d40e0915e25fce10329f4f801a0002a4f886cc7"}, - {file = "ckzg-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d76b50527f1d12430bf118aff6fa4051e9860eada43f29177258b8d399448ea"}, - {file = "ckzg-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44c8603e43c021d100f355f50189183135d1df3cbbddb8881552d57fbf421dde"}, - {file = "ckzg-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:38707a638c9d715b3c30b29352b969f78d8fc10faed7db5faf517f04359895c0"}, - {file = "ckzg-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:52c4d257bdcbe822d20c5cd24c8154ec5aac33c49a8f5a19e716d9107a1c8785"}, - {file = "ckzg-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1507f7bfb9bcf51d816db5d8d0f0ed53c8289605137820d437b69daea8333e16"}, - {file = "ckzg-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:d02eaaf4f841910133552b3a051dea53bcfe60cd98199fc4cf80b27609d8baa2"}, - {file = "ckzg-2.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:465e2b71cf9dc383f66f1979269420a0da9274a3a9e98b1a4455e84927dfe491"}, - {file = "ckzg-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ee2f26f17a64ad0aab833d637b276f28486b82a29e34f32cf54b237b8f8ab72d"}, - {file = "ckzg-2.1.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99cc2c4e9fb8c62e3e0862c7f4df9142f07ba640da17fded5f6e0fd09f75909f"}, - {file = "ckzg-2.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773dd016693d74aca1f5d7982db2bad7dde2e147563aeb16a783f7e5f69c01fe"}, - {file = "ckzg-2.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af2b2144f87ba218d8db01382a961b3ecbdde5ede4fa0d9428d35f8c8a595ba"}, - {file = "ckzg-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8f55e63d3f7c934a2cb53728ed1d815479e177aca8c84efe991c2920977cff6"}, - {file = "ckzg-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ecb42aaa0ffa427ff14a9dde9356ba69e5ae6014650b397af55b31bdae7a9b6e"}, - {file = "ckzg-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5a01514239f12fb1a7ad9009c20062a4496e13b09541c1a65f97e295da648c70"}, - {file = "ckzg-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:6516b9684aae262c85cf7fddd8b585b8139ad20e08ec03994e219663abbb0916"}, - {file = "ckzg-2.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c60e8903344ce98ce036f0fabacce952abb714cad4607198b2f0961c28b8aa72"}, - {file = "ckzg-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4299149dd72448e5a8d2d1cc6cc7472c92fc9d9f00b1377f5b017c089d9cd92"}, - {file = "ckzg-2.1.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:025dd31ffdcc799f3ff842570a2a6683b6c5b01567da0109c0c05d11768729c4"}, - {file = "ckzg-2.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b42ab8385c273f40a693657c09d2bba40cb4f4666141e263906ba2e519e80bd"}, - {file = "ckzg-2.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1be3890fc1543f4fcfc0063e4baf5c036eb14bcf736dabdc6171ab017e0f1671"}, - {file = "ckzg-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b754210ded172968b201e2d7252573af6bf52d6ad127ddd13d0b9a45a51dae7b"}, - {file = "ckzg-2.1.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2f8fda87865897a269c4e951e3826c2e814427a6cdfed6731cccfe548f12b36"}, - {file = "ckzg-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:98e70b5923d77c7359432490145e9d1ab0bf873eb5de56ec53f4a551d7eaec79"}, - {file = "ckzg-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:42af7bde4ca45469cd93a96c3d15d69d51d40e7f0d30e3a20711ebd639465fcb"}, - {file = "ckzg-2.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e4edfdaf87825ff43b9885fabfdea408737a714f4ce5467100d9d1d0a03b673"}, - {file = "ckzg-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:815fd2a87d6d6c57d669fda30c150bc9bf387d47e67d84535aa42b909fdc28ea"}, - {file = "ckzg-2.1.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c32466e809b1ab3ff01d3b0bb0b9912f61dcf72957885615595f75e3f7cc10e5"}, - {file = "ckzg-2.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f11b73ccf37b12993f39a7dbace159c6d580aacacde6ee17282848476550ddbc"}, - {file = "ckzg-2.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3b9433a1f2604bd9ac1646d3c83ad84a850d454d3ac589fe8e70c94b38a6b0"}, - {file = "ckzg-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b7d7e1b5ea06234558cd95c483666fd785a629b720a7f1622b3cbffebdc62033"}, - {file = "ckzg-2.1.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9f5556e6675866040cc4335907be6c537051e7f668da289fa660fdd8a30c9ddb"}, - {file = "ckzg-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55b2ba30c5c9daac0c55f1aac851f1b7bf1f7aa0028c2db4440e963dd5b866d6"}, - {file = "ckzg-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:10d201601fc8f28c0e8cec3406676797024dd374c367bbeec5a7a9eac9147237"}, - {file = "ckzg-2.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:5f46c8fd5914db62b446baf62c8599da07e6f91335779a9709c554ef300a7b60"}, - {file = "ckzg-2.1.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60f14612c2be84f405755d734b0ad4e445db8af357378b95b72339b59e1f4fcf"}, - {file = "ckzg-2.1.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:929e6e793039f42325988004a90d16b0ef4fc7e1330142e180f0298f2ed4527c"}, - {file = "ckzg-2.1.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2beac2af53ea181118179570ecc81d8a8fc52c529553d7fd8786fd100a2aa39b"}, - {file = "ckzg-2.1.1-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:2432d48aec296baee79556bfde3bddd2799bcc7753cd1f0d0c9a3b0333935637"}, - {file = "ckzg-2.1.1-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:4c2e8180b54261ccae2bf8acd003ccee7394d88d073271af19c5f2ac4a54c607"}, - {file = "ckzg-2.1.1-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:c44e36bd53d9dd0ab29bd6ed2d67ea43c48eecd57f8197854a75742213938bf5"}, - {file = "ckzg-2.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:10befd86e643d38ac468151cdfb71e79b2d46aa6397b81db4224f4f6995262eb"}, - {file = "ckzg-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:138a9324ad8e8a9ade464043dc3a84afe12996516788f2ed841bdbe5d123af81"}, - {file = "ckzg-2.1.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:635af0a33a10c9ac275f3efc142880a6b46ac63f4495f600aae05266af4fadff"}, - {file = "ckzg-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:360e263677ee5aedb279b42cf54b51c905ddcac9181c65d89ec0b298d3f31ec0"}, - {file = "ckzg-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f81395f77bfd069831cbb1de9d473c7044abe9ce6cd562ef6ccd76d23abcef43"}, - {file = "ckzg-2.1.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:db1ff122f8dc10c9500a00a4d680c3c38f4e19b01d95f38e0f5bc55a77c8ab98"}, - {file = "ckzg-2.1.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:1f82f539949ff3c6a5accfdd211919a3e374d354b3665d062395ebdbf8befaeb"}, - {file = "ckzg-2.1.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:5bc8ae85df97467e84abb491b516e25dbca36079e766eafce94d1bc45e4aaa35"}, - {file = "ckzg-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:e749ce9fcb26e37101f2af8ba9c6376b66eb598880d35e457890044ba77c1cf7"}, - {file = "ckzg-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b00201979a64fd7e6029f64d791af42374febb42452537933e881b49d4e8c77"}, - {file = "ckzg-2.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c61c437ba714ab7c802b51fb30125e8f8550e1320fe9050d20777420c153a2b3"}, - {file = "ckzg-2.1.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8bd54394376598a7c081df009cfde3cc447beb640b6c6b7534582a31e6290ac7"}, - {file = "ckzg-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67d8c6680a7b370718af59cc17a983752706407cfbcace013ee707646d1f7b00"}, - {file = "ckzg-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55f6c57b24bc4fe16b1b50324ef8548f2a5053ad76bf90c618e2f88c040120d7"}, - {file = "ckzg-2.1.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f55fc10fb1b217c66bfe14e05535e5e61cfbb2a95dbb9b93a80984fa2ab4a7c0"}, - {file = "ckzg-2.1.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:2e23e3198f8933f0140ef8b2aeba717d8de03ec7b8fb1ee946f8d39986ce0811"}, - {file = "ckzg-2.1.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2f9caf88bf216756bb1361b92616c750a933c9afb67972ad05c212649a9be520"}, - {file = "ckzg-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:30e0c2d258bbc0c099c2d1854c6ffa2fd9abf6138b9c81f855e1936f6cb259aa"}, - {file = "ckzg-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6239d3d2e30cb894ca4e7765b1097eb6a70c0ecbe5f8e0b023fbf059472d4ac"}, - {file = "ckzg-2.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:909ebabc253a98d9dc1d51f93dc75990134bfe296c947e1ecf3b7142aba5108e"}, - {file = "ckzg-2.1.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0700dace6559b288b42ca8622be89c2a43509881ed6f4f0bfb6312bcceed0cb9"}, - {file = "ckzg-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a36aeabd243e906314694b4a107de99b0c4473ff1825fcb06acd147ffb1951a"}, - {file = "ckzg-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d884e8f9c7d7839f1a95561f4479096dce21d45b0c5dd013dc0842550cea1cad"}, - {file = "ckzg-2.1.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:338fdf4a0b463973fc7b7e4dc289739db929e61d7cb9ba984ebbe9c49d3aa6f9"}, - {file = "ckzg-2.1.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c594036d3408eebdcd8ab2c7aab7308239ed4df3d94f3211b7cf253f228fb0b7"}, - {file = "ckzg-2.1.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b0912ebb328ced510250a2325b095917db19c1a014792a0bf4c389f0493e39de"}, - {file = "ckzg-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:5046aceb03482ddf7200f2f5c643787b100e6fb96919852faf1c79f8870c80a1"}, - {file = "ckzg-2.1.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:375918e25eafb9bafe5215ab91698504cba3fe51b4fe92f5896af6c5663f50c6"}, - {file = "ckzg-2.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:38b3b7802c76d4ad015db2b7a79a49c193babae50ee5f77e9ac2865c9e9ddb09"}, - {file = "ckzg-2.1.1-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:438a5009fd254ace0bc1ad974d524547f1a41e6aa5e778c5cd41f4ee3106bcd6"}, - {file = "ckzg-2.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ce11cc163a2e0dab3af7455aca7053f9d5bb8d157f231acc7665fd230565d48"}, - {file = "ckzg-2.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b53964c07f6a076e97eaa1ef35045e935d7040aff14f80bae7e9105717702d05"}, - {file = "ckzg-2.1.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cf085f15ae52ab2599c9b5a3d5842794bcf5613b7f58661fbfb0c5d9eac988b9"}, - {file = "ckzg-2.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4b0c850bd6cad22ac79b2a2ab884e0e7cd2b54a67d643cd616c145ebdb535a11"}, - {file = "ckzg-2.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:26951f36bb60c9150bbd38110f5e1625596f9779dad54d1d492d8ec38bc84e3a"}, - {file = "ckzg-2.1.1-pp311-pypy311_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbe12445e49c4bee67746b7b958e90a973b0de116d0390749b0df351d94e9a8c"}, - {file = "ckzg-2.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71c5d4f66f09de4a99271acac74d2acb3559a77de77a366b34a91e99e8822667"}, - {file = "ckzg-2.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42673c1d007372a4e8b48f6ef8f0ce31a9688a463317a98539757d1e2fb1ecc7"}, - {file = "ckzg-2.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:57a7dc41ec6b69c1d9117eb61cf001295e6b4f67a736020442e71fb4367fb1a5"}, - {file = "ckzg-2.1.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:22e4606857660b2ffca2f7b96c01d0b18b427776d8a93320caf2b1c7342881fe"}, - {file = "ckzg-2.1.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b55475126a9efc82d61718b2d2323502e33d9733b7368c407954592ccac87faf"}, - {file = "ckzg-2.1.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5939ae021557c64935a7649b13f4a58f1bd35c39998fd70d0cefb5cbaf77d1be"}, - {file = "ckzg-2.1.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad1ec5f9726a9946508a4a2aace298172aa778de9ebbe97e21c873c3688cc87"}, - {file = "ckzg-2.1.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93d7edea3bb1602b18b394ebeec231d89dfd8d48fdd06571cb7656107aa62226"}, - {file = "ckzg-2.1.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c450d77af61011ced3777f97431d5f1bc148ca5362c67caf516aa2f6ef7e4817"}, - {file = "ckzg-2.1.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:8fc8df4e17e08974961d6c14f6c57ccfd3ad5aede74598292ec6e5d6fc2dbcac"}, - {file = "ckzg-2.1.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93338da8011790ef53a68475678bc951fa7b337db027d8edbf1889e59691161c"}, - {file = "ckzg-2.1.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4889f24b4ff614f39e3584709de1a3b0f1556675b33e360dbcb28cda827296d4"}, - {file = "ckzg-2.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b58fbb1a9be4ae959feede8f103e12d80ef8453bdc6483bfdaf164879a2b80"}, - {file = "ckzg-2.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6136c5b5377c7f7033323b25bc2c7b43c025d44ed73e338c02f9f59df9460e5b"}, - {file = "ckzg-2.1.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fa419b92a0e8766deb7157fb28b6542c1c3f8dde35d2a69d1f91ec8e41047d35"}, - {file = "ckzg-2.1.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:95cd6c8eb3ab5148cd97ab5bf44b84fd7f01adf4b36ffd070340ad2d9309b3f9"}, - {file = "ckzg-2.1.1-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:848191201052b48bdde18680ebb77bf8da99989270e5aea8b0290051f5ac9468"}, - {file = "ckzg-2.1.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4716c0564131b0d609fb8856966e83892b9809cf6719c7edd6495b960451f8b"}, - {file = "ckzg-2.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c399168ba199827dee3104b00cdc7418d4dbdf47a5fcbe7cf938fc928037534"}, - {file = "ckzg-2.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:724f29f9f110d9ef42a6a1a1a7439548c61070604055ef96b2ab7a884cad4192"}, - {file = "ckzg-2.1.1.tar.gz", hash = "sha256:d6b306b7ec93a24e4346aa53d07f7f75053bc0afc7398e35fa649e5f9d48fcc4"}, -] - -[[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.2.1" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.10" -files = [ - {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, - {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, -] - -[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.10.1" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "coverage-7.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1c86eb388bbd609d15560e7cc0eb936c102b6f43f31cf3e58b4fd9afe28e1372"}, - {file = "coverage-7.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b4ba0f488c1bdb6bd9ba81da50715a372119785458831c73428a8566253b86b"}, - {file = "coverage-7.10.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083442ecf97d434f0cb3b3e3676584443182653da08b42e965326ba12d6b5f2a"}, - {file = "coverage-7.10.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c1a40c486041006b135759f59189385da7c66d239bad897c994e18fd1d0c128f"}, - {file = "coverage-7.10.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3beb76e20b28046989300c4ea81bf690df84ee98ade4dc0bbbf774a28eb98440"}, - {file = "coverage-7.10.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bc265a7945e8d08da28999ad02b544963f813a00f3ed0a7a0ce4165fd77629f8"}, - {file = "coverage-7.10.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:47c91f32ba4ac46f1e224a7ebf3f98b4b24335bad16137737fe71a5961a0665c"}, - {file = "coverage-7.10.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1a108dd78ed185020f66f131c60078f3fae3f61646c28c8bb4edd3fa121fc7fc"}, - {file = "coverage-7.10.1-cp310-cp310-win32.whl", hash = "sha256:7092cc82382e634075cc0255b0b69cb7cada7c1f249070ace6a95cb0f13548ef"}, - {file = "coverage-7.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:ac0c5bba938879c2fc0bc6c1b47311b5ad1212a9dcb8b40fe2c8110239b7faed"}, - {file = "coverage-7.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b45e2f9d5b0b5c1977cb4feb5f594be60eb121106f8900348e29331f553a726f"}, - {file = "coverage-7.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a7a4d74cb0f5e3334f9aa26af7016ddb94fb4bfa11b4a573d8e98ecba8c34f1"}, - {file = "coverage-7.10.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d4b0aab55ad60ead26159ff12b538c85fbab731a5e3411c642b46c3525863437"}, - {file = "coverage-7.10.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dcc93488c9ebd229be6ee1f0d9aad90da97b33ad7e2912f5495804d78a3cd6b7"}, - {file = "coverage-7.10.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa309df995d020f3438407081b51ff527171cca6772b33cf8f85344b8b4b8770"}, - {file = "coverage-7.10.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cfb8b9d8855c8608f9747602a48ab525b1d320ecf0113994f6df23160af68262"}, - {file = "coverage-7.10.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:320d86da829b012982b414c7cdda65f5d358d63f764e0e4e54b33097646f39a3"}, - {file = "coverage-7.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dc60ddd483c556590da1d9482a4518292eec36dd0e1e8496966759a1f282bcd0"}, - {file = "coverage-7.10.1-cp311-cp311-win32.whl", hash = "sha256:4fcfe294f95b44e4754da5b58be750396f2b1caca8f9a0e78588e3ef85f8b8be"}, - {file = "coverage-7.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:efa23166da3fe2915f8ab452dde40319ac84dc357f635737174a08dbd912980c"}, - {file = "coverage-7.10.1-cp311-cp311-win_arm64.whl", hash = "sha256:d12b15a8c3759e2bb580ffa423ae54be4f184cf23beffcbd641f4fe6e1584293"}, - {file = "coverage-7.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6b7dc7f0a75a7eaa4584e5843c873c561b12602439d2351ee28c7478186c4da4"}, - {file = "coverage-7.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:607f82389f0ecafc565813aa201a5cade04f897603750028dd660fb01797265e"}, - {file = "coverage-7.10.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f7da31a1ba31f1c1d4d5044b7c5813878adae1f3af8f4052d679cc493c7328f4"}, - {file = "coverage-7.10.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51fe93f3fe4f5d8483d51072fddc65e717a175490804e1942c975a68e04bf97a"}, - {file = "coverage-7.10.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e59d00830da411a1feef6ac828b90bbf74c9b6a8e87b8ca37964925bba76dbe"}, - {file = "coverage-7.10.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:924563481c27941229cb4e16eefacc35da28563e80791b3ddc5597b062a5c386"}, - {file = "coverage-7.10.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ca79146ee421b259f8131f153102220b84d1a5e6fb9c8aed13b3badfd1796de6"}, - {file = "coverage-7.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2b225a06d227f23f386fdc0eab471506d9e644be699424814acc7d114595495f"}, - {file = "coverage-7.10.1-cp312-cp312-win32.whl", hash = "sha256:5ba9a8770effec5baaaab1567be916c87d8eea0c9ad11253722d86874d885eca"}, - {file = "coverage-7.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:9eb245a8d8dd0ad73b4062135a251ec55086fbc2c42e0eb9725a9b553fba18a3"}, - {file = "coverage-7.10.1-cp312-cp312-win_arm64.whl", hash = "sha256:7718060dd4434cc719803a5e526838a5d66e4efa5dc46d2b25c21965a9c6fcc4"}, - {file = "coverage-7.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ebb08d0867c5a25dffa4823377292a0ffd7aaafb218b5d4e2e106378b1061e39"}, - {file = "coverage-7.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f32a95a83c2e17422f67af922a89422cd24c6fa94041f083dd0bb4f6057d0bc7"}, - {file = "coverage-7.10.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c4c746d11c8aba4b9f58ca8bfc6fbfd0da4efe7960ae5540d1a1b13655ee8892"}, - {file = "coverage-7.10.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7f39edd52c23e5c7ed94e0e4bf088928029edf86ef10b95413e5ea670c5e92d7"}, - {file = "coverage-7.10.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab6e19b684981d0cd968906e293d5628e89faacb27977c92f3600b201926b994"}, - {file = "coverage-7.10.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5121d8cf0eacb16133501455d216bb5f99899ae2f52d394fe45d59229e6611d0"}, - {file = "coverage-7.10.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df1c742ca6f46a6f6cbcaef9ac694dc2cb1260d30a6a2f5c68c5f5bcfee1cfd7"}, - {file = "coverage-7.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:40f9a38676f9c073bf4b9194707aa1eb97dca0e22cc3766d83879d72500132c7"}, - {file = "coverage-7.10.1-cp313-cp313-win32.whl", hash = "sha256:2348631f049e884839553b9974f0821d39241c6ffb01a418efce434f7eba0fe7"}, - {file = "coverage-7.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:4072b31361b0d6d23f750c524f694e1a417c1220a30d3ef02741eed28520c48e"}, - {file = "coverage-7.10.1-cp313-cp313-win_arm64.whl", hash = "sha256:3e31dfb8271937cab9425f19259b1b1d1f556790e98eb266009e7a61d337b6d4"}, - {file = "coverage-7.10.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1c4f679c6b573a5257af6012f167a45be4c749c9925fd44d5178fd641ad8bf72"}, - {file = "coverage-7.10.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:871ebe8143da284bd77b84a9136200bd638be253618765d21a1fce71006d94af"}, - {file = "coverage-7.10.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:998c4751dabf7d29b30594af416e4bf5091f11f92a8d88eb1512c7ba136d1ed7"}, - {file = "coverage-7.10.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:780f750a25e7749d0af6b3631759c2c14f45de209f3faaa2398312d1c7a22759"}, - {file = "coverage-7.10.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:590bdba9445df4763bdbebc928d8182f094c1f3947a8dc0fc82ef014dbdd8324"}, - {file = "coverage-7.10.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b2df80cb6a2af86d300e70acb82e9b79dab2c1e6971e44b78dbfc1a1e736b53"}, - {file = "coverage-7.10.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d6a558c2725bfb6337bf57c1cd366c13798bfd3bfc9e3dd1f4a6f6fc95a4605f"}, - {file = "coverage-7.10.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e6150d167f32f2a54690e572e0a4c90296fb000a18e9b26ab81a6489e24e78dd"}, - {file = "coverage-7.10.1-cp313-cp313t-win32.whl", hash = "sha256:d946a0c067aa88be4a593aad1236493313bafaa27e2a2080bfe88db827972f3c"}, - {file = "coverage-7.10.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e37c72eaccdd5ed1130c67a92ad38f5b2af66eeff7b0abe29534225db2ef7b18"}, - {file = "coverage-7.10.1-cp313-cp313t-win_arm64.whl", hash = "sha256:89ec0ffc215c590c732918c95cd02b55c7d0f569d76b90bb1a5e78aa340618e4"}, - {file = "coverage-7.10.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:166d89c57e877e93d8827dac32cedae6b0277ca684c6511497311249f35a280c"}, - {file = "coverage-7.10.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bed4a2341b33cd1a7d9ffc47df4a78ee61d3416d43b4adc9e18b7d266650b83e"}, - {file = "coverage-7.10.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ddca1e4f5f4c67980533df01430184c19b5359900e080248bbf4ed6789584d8b"}, - {file = "coverage-7.10.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:37b69226001d8b7de7126cad7366b0778d36777e4d788c66991455ba817c5b41"}, - {file = "coverage-7.10.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2f22102197bcb1722691296f9e589f02b616f874e54a209284dd7b9294b0b7f"}, - {file = "coverage-7.10.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1e0c768b0f9ac5839dac5cf88992a4bb459e488ee8a1f8489af4cb33b1af00f1"}, - {file = "coverage-7.10.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:991196702d5e0b120a8fef2664e1b9c333a81d36d5f6bcf6b225c0cf8b0451a2"}, - {file = "coverage-7.10.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae8e59e5f4fd85d6ad34c2bb9d74037b5b11be072b8b7e9986beb11f957573d4"}, - {file = "coverage-7.10.1-cp314-cp314-win32.whl", hash = "sha256:042125c89cf74a074984002e165d61fe0e31c7bd40ebb4bbebf07939b5924613"}, - {file = "coverage-7.10.1-cp314-cp314-win_amd64.whl", hash = "sha256:a22c3bfe09f7a530e2c94c87ff7af867259c91bef87ed2089cd69b783af7b84e"}, - {file = "coverage-7.10.1-cp314-cp314-win_arm64.whl", hash = "sha256:ee6be07af68d9c4fca4027c70cea0c31a0f1bc9cb464ff3c84a1f916bf82e652"}, - {file = "coverage-7.10.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d24fb3c0c8ff0d517c5ca5de7cf3994a4cd559cde0315201511dbfa7ab528894"}, - {file = "coverage-7.10.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1217a54cfd79be20512a67ca81c7da3f2163f51bbfd188aab91054df012154f5"}, - {file = "coverage-7.10.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:51f30da7a52c009667e02f125737229d7d8044ad84b79db454308033a7808ab2"}, - {file = "coverage-7.10.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ed3718c757c82d920f1c94089066225ca2ad7f00bb904cb72b1c39ebdd906ccb"}, - {file = "coverage-7.10.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc452481e124a819ced0c25412ea2e144269ef2f2534b862d9f6a9dae4bda17b"}, - {file = "coverage-7.10.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9d6f494c307e5cb9b1e052ec1a471060f1dea092c8116e642e7a23e79d9388ea"}, - {file = "coverage-7.10.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fc0e46d86905ddd16b85991f1f4919028092b4e511689bbdaff0876bd8aab3dd"}, - {file = "coverage-7.10.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80b9ccd82e30038b61fc9a692a8dc4801504689651b281ed9109f10cc9fe8b4d"}, - {file = "coverage-7.10.1-cp314-cp314t-win32.whl", hash = "sha256:e58991a2b213417285ec866d3cd32db17a6a88061a985dbb7e8e8f13af429c47"}, - {file = "coverage-7.10.1-cp314-cp314t-win_amd64.whl", hash = "sha256:e88dd71e4ecbc49d9d57d064117462c43f40a21a1383507811cf834a4a620651"}, - {file = "coverage-7.10.1-cp314-cp314t-win_arm64.whl", hash = "sha256:1aadfb06a30c62c2eb82322171fe1f7c288c80ca4156d46af0ca039052814bab"}, - {file = "coverage-7.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:57b6e8789cbefdef0667e4a94f8ffa40f9402cee5fc3b8e4274c894737890145"}, - {file = "coverage-7.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:85b22a9cce00cb03156334da67eb86e29f22b5e93876d0dd6a98646bb8a74e53"}, - {file = "coverage-7.10.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:97b6983a2f9c76d345ca395e843a049390b39652984e4a3b45b2442fa733992d"}, - {file = "coverage-7.10.1-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ddf2a63b91399a1c2f88f40bc1705d5a7777e31c7e9eb27c602280f477b582ba"}, - {file = "coverage-7.10.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47ab6dbbc31a14c5486420c2c1077fcae692097f673cf5be9ddbec8cdaa4cdbc"}, - {file = "coverage-7.10.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:21eb7d8b45d3700e7c2936a736f732794c47615a20f739f4133d5230a6512a88"}, - {file = "coverage-7.10.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:283005bb4d98ae33e45f2861cd2cde6a21878661c9ad49697f6951b358a0379b"}, - {file = "coverage-7.10.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:fefe31d61d02a8b2c419700b1fade9784a43d726de26495f243b663cd9fe1513"}, - {file = "coverage-7.10.1-cp39-cp39-win32.whl", hash = "sha256:e8ab8e4c7ec7f8a55ac05b5b715a051d74eac62511c6d96d5bb79aaafa3b04cf"}, - {file = "coverage-7.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:c36baa0ecde742784aa76c2b816466d3ea888d5297fda0edbac1bf48fa94688a"}, - {file = "coverage-7.10.1-py3-none-any.whl", hash = "sha256:fa2a258aa6bf188eb9a8948f7102a83da7c430a0dce918dbd8b60ef8fcb772d7"}, - {file = "coverage-7.10.1.tar.gz", hash = "sha256:ae2b4856f29ddfe827106794f3589949a57da6f0d38ab01e24ec35107979ba57"}, -] - -[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 = "45.0.5" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -optional = false -python-versions = "!=3.9.0,!=3.9.1,>=3.7" -files = [ - {file = "cryptography-45.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:101ee65078f6dd3e5a028d4f19c07ffa4dd22cce6a20eaa160f8b5219911e7d8"}, - {file = "cryptography-45.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a264aae5f7fbb089dbc01e0242d3b67dffe3e6292e1f5182122bdf58e65215d"}, - {file = "cryptography-45.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e74d30ec9c7cb2f404af331d5b4099a9b322a8a6b25c4632755c8757345baac5"}, - {file = "cryptography-45.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3af26738f2db354aafe492fb3869e955b12b2ef2e16908c8b9cb928128d42c57"}, - {file = "cryptography-45.0.5-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e6c00130ed423201c5bc5544c23359141660b07999ad82e34e7bb8f882bb78e0"}, - {file = "cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:dd420e577921c8c2d31289536c386aaa30140b473835e97f83bc71ea9d2baf2d"}, - {file = "cryptography-45.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d05a38884db2ba215218745f0781775806bde4f32e07b135348355fe8e4991d9"}, - {file = "cryptography-45.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ad0caded895a00261a5b4aa9af828baede54638754b51955a0ac75576b831b27"}, - {file = "cryptography-45.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9024beb59aca9d31d36fcdc1604dd9bbeed0a55bface9f1908df19178e2f116e"}, - {file = "cryptography-45.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91098f02ca81579c85f66df8a588c78f331ca19089763d733e34ad359f474174"}, - {file = "cryptography-45.0.5-cp311-abi3-win32.whl", hash = "sha256:926c3ea71a6043921050eaa639137e13dbe7b4ab25800932a8498364fc1abec9"}, - {file = "cryptography-45.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:b85980d1e345fe769cfc57c57db2b59cff5464ee0c045d52c0df087e926fbe63"}, - {file = "cryptography-45.0.5-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3562c2f23c612f2e4a6964a61d942f891d29ee320edb62ff48ffb99f3de9ae8"}, - {file = "cryptography-45.0.5-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3fcfbefc4a7f332dece7272a88e410f611e79458fab97b5efe14e54fe476f4fd"}, - {file = "cryptography-45.0.5-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:460f8c39ba66af7db0545a8c6f2eabcbc5a5528fc1cf6c3fa9a1e44cec33385e"}, - {file = "cryptography-45.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9b4cf6318915dccfe218e69bbec417fdd7c7185aa7aab139a2c0beb7468c89f0"}, - {file = "cryptography-45.0.5-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2089cc8f70a6e454601525e5bf2779e665d7865af002a5dec8d14e561002e135"}, - {file = "cryptography-45.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0027d566d65a38497bc37e0dd7c2f8ceda73597d2ac9ba93810204f56f52ebc7"}, - {file = "cryptography-45.0.5-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:be97d3a19c16a9be00edf79dca949c8fa7eff621763666a145f9f9535a5d7f42"}, - {file = "cryptography-45.0.5-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:7760c1c2e1a7084153a0f68fab76e754083b126a47d0117c9ed15e69e2103492"}, - {file = "cryptography-45.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ff8728d8d890b3dda5765276d1bc6fb099252915a2cd3aff960c4c195745dd0"}, - {file = "cryptography-45.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7259038202a47fdecee7e62e0fd0b0738b6daa335354396c6ddebdbe1206af2a"}, - {file = "cryptography-45.0.5-cp37-abi3-win32.whl", hash = "sha256:1e1da5accc0c750056c556a93c3e9cb828970206c68867712ca5805e46dc806f"}, - {file = "cryptography-45.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:90cb0a7bb35959f37e23303b7eed0a32280510030daba3f7fdfbb65defde6a97"}, - {file = "cryptography-45.0.5-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:206210d03c1193f4e1ff681d22885181d47efa1ab3018766a7b32a7b3d6e6afd"}, - {file = "cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c648025b6840fe62e57107e0a25f604db740e728bd67da4f6f060f03017d5097"}, - {file = "cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b8fa8b0a35a9982a3c60ec79905ba5bb090fc0b9addcfd3dc2dd04267e45f25e"}, - {file = "cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:14d96584701a887763384f3c47f0ca7c1cce322aa1c31172680eb596b890ec30"}, - {file = "cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57c816dfbd1659a367831baca4b775b2a5b43c003daf52e9d57e1d30bc2e1b0e"}, - {file = "cryptography-45.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b9e38e0a83cd51e07f5a48ff9691cae95a79bea28fe4ded168a8e5c6c77e819d"}, - {file = "cryptography-45.0.5-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8c4a6ff8a30e9e3d38ac0539e9a9e02540ab3f827a3394f8852432f6b0ea152e"}, - {file = "cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bd4c45986472694e5121084c6ebbd112aa919a25e783b87eb95953c9573906d6"}, - {file = "cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:982518cd64c54fcada9d7e5cf28eabd3ee76bd03ab18e08a48cad7e8b6f31b18"}, - {file = "cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:12e55281d993a793b0e883066f590c1ae1e802e3acb67f8b442e721e475e6463"}, - {file = "cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:5aa1e32983d4443e310f726ee4b071ab7569f58eedfdd65e9675484a4eb67bd1"}, - {file = "cryptography-45.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e357286c1b76403dd384d938f93c46b2b058ed4dfcdce64a770f0537ed3feb6f"}, - {file = "cryptography-45.0.5.tar.gz", hash = "sha256:72e76caa004ab63accdf26023fccd1d087f6d90ec6048ff33ad0445abf7f605a"}, -] - -[package.dependencies] -cffi = {version = ">=1.14", markers = "platform_python_implementation != \"PyPy\""} - -[package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] -docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] -nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"] -pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] -sdist = ["build (>=1.0.0)"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==45.0.5)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] -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.2.1" -description = "Decorators for Humans" -optional = false -python-versions = ">=3.8" -files = [ - {file = "decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"}, - {file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"}, -] - -[[package]] -name = "distlib" -version = "0.4.0" -description = "Distribution utilities" -optional = false -python-versions = "*" -files = [ - {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, - {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, -] - -[[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.2.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.2.0-py3-none-any.whl", hash = "sha256:17abe47560ad753f18054f5b3089fcb588f3e3a092136a416b6c1502cb7e8877"}, - {file = "eth_abi-5.2.0.tar.gz", hash = "sha256:178703fa98c07d8eecd5ae569e7e8d159e493ebb6eeb534a8fe973fbc4e40ef0"}, -] - -[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)", "bump_my_version (>=0.19.0)", "eth-hash[pycryptodome]", "hypothesis (>=6.22.0,<6.108.7)", "ipython", "mypy (==1.10.0)", "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-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] -test = ["eth-hash[pycryptodome]", "hypothesis (>=6.22.0,<6.108.7)", "pytest (>=7.0.0)", "pytest-pythonpath (>=0.7.1)", "pytest-timeout (>=2.0.0)", "pytest-xdist (>=2.4.0)"] -tools = ["hypothesis (>=6.22.0,<6.108.7)"] - -[[package]] -name = "eth-account" -version = "0.13.7" -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.7-py3-none-any.whl", hash = "sha256:39727de8c94d004ff61d10da7587509c04d2dc7eac71e04830135300bdfc6d24"}, - {file = "eth_account-0.13.7.tar.gz", hash = "sha256:5853ecbcbb22e65411176f121f5f24b8afeeaf13492359d254b16d8b18c77a46"}, -] - -[package.dependencies] -bitarray = ">=2.4.0" -ckzg = ">=2.0.0" -eth-abi = ">=4.0.0-b.2" -eth-keyfile = ">=0.7.0,<0.9.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)", "bump_my_version (>=0.19.0)", "coverage", "hypothesis (>=6.22.0,<6.108.7)", "ipython", "mypy (==1.10.0)", "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 (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] -test = ["coverage", "hypothesis (>=6.22.0,<6.108.7)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] - -[[package]] -name = "eth-bloom" -version = "3.1.0" -description = "A python implementation of the bloom filter used by Ethereum" -optional = false -python-versions = "<4,>=3.8" -files = [ - {file = "eth_bloom-3.1.0-py3-none-any.whl", hash = "sha256:c96b2dd6cafa407373bca1a9d74b650378ba672d5b17f2771bf7d3c3aaa7651c"}, - {file = "eth_bloom-3.1.0.tar.gz", hash = "sha256:4bc918f6fde44334e92b23cfb345db961e2e3af620535cbc872444f7a143cb88"}, -] - -[package.dependencies] -eth-hash = {version = ">=0.4.0", extras = ["pycryptodome"]} - -[package.extras] -dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "hypothesis (>=3.31.2)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["towncrier (>=24,<25)"] -test = ["hypothesis (>=3.31.2)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] - -[[package]] -name = "eth-hash" -version = "0.7.1" -description = "eth-hash: The Ethereum hashing function, keccak256, sometimes (erroneously) called sha3" -optional = false -python-versions = "<4,>=3.8" -files = [ - {file = "eth_hash-0.7.1-py3-none-any.whl", hash = "sha256:0fb1add2adf99ef28883fd6228eb447ef519ea72933535ad1a0b28c6f65f868a"}, - {file = "eth_hash-0.7.1.tar.gz", hash = "sha256:d2411a403a0b0a62e8247b4117932d900ffb4c8c64b15f92620547ca5ce46be5"}, -] - -[package.dependencies] -pycryptodome = {version = ">=3.6.6,<4", optional = true, markers = "extra == \"pycryptodome\""} - -[package.extras] -dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "ipython", "mypy (==1.10.0)", "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 (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] -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.7.0" -description = "eth-keys: Common API for Ethereum key operations" -optional = false -python-versions = "<4,>=3.8" -files = [ - {file = "eth_keys-0.7.0-py3-none-any.whl", hash = "sha256:b0cdda8ffe8e5ba69c7c5ca33f153828edcace844f67aabd4542d7de38b159cf"}, - {file = "eth_keys-0.7.0.tar.gz", hash = "sha256:79d24fd876201df67741de3e3fefb3f4dbcbb6ace66e47e6fe662851a4547814"}, -] - -[package.dependencies] -eth-typing = ">=3" -eth-utils = ">=2" - -[package.extras] -coincurve = ["coincurve (>=17.0.0)"] -dev = ["asn1tools (>=0.146.2)", "build (>=0.9.0)", "bump_my_version (>=0.19.0)", "coincurve (>=17.0.0)", "eth-hash[pysha3]", "factory-boy (>=3.0.1)", "hypothesis (>=5.10.3)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pyasn1 (>=0.4.5)", "pytest (>=7.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["towncrier (>=24,<25)"] -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.2.0" -description = "eth-rlp: RLP definitions for common Ethereum objects in Python" -optional = false -python-versions = "<4,>=3.8" -files = [ - {file = "eth_rlp-2.2.0-py3-none-any.whl", hash = "sha256:5692d595a741fbaef1203db6a2fedffbd2506d31455a6ad378c8449ee5985c47"}, - {file = "eth_rlp-2.2.0.tar.gz", hash = "sha256:5e4b2eb1b8213e303d6a232dfe35ab8c29e2d3051b86e8d359def80cd21db83d"}, -] - -[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)", "bump_my_version (>=0.19.0)", "eth-hash[pycryptodome]", "ipython", "mypy (==1.10.0)", "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 (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] -test = ["eth-hash[pycryptodome]", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] - -[[package]] -name = "eth-stdlib" -version = "0.2.8" -description = "Ethereum Standard Library for Python" -optional = false -python-versions = "<4.0,>=3.8" -files = [ - {file = "eth_stdlib-0.2.8-py3-none-any.whl", hash = "sha256:77979707e01899c4626b78d20f05a1da6ba1dc3224c0a173259618b7970326e9"}, - {file = "eth_stdlib-0.2.8.tar.gz", hash = "sha256:cda115a841142d4646e3f05818cbb08e2ca3daafc740646fa5888f9b8db08f97"}, -] - -[package.dependencies] -pycryptodome = ">=3.18.0,<4.0.0" - -[package.extras] -hypothesis = ["hypothesis (>=6.58.0,<7.0.0)"] - -[[package]] -name = "eth-typing" -version = "5.2.1" -description = "eth-typing: Common type annotations for ethereum python packages" -optional = false -python-versions = "<4,>=3.8" -files = [ - {file = "eth_typing-5.2.1-py3-none-any.whl", hash = "sha256:b0c2812ff978267563b80e9d701f487dd926f1d376d674f3b535cfe28b665d3d"}, - {file = "eth_typing-5.2.1.tar.gz", hash = "sha256:7557300dbf02a93c70fa44af352b5c4a58f94e997a0fd6797fb7d1c29d9538ee"}, -] - -[package.dependencies] -typing_extensions = ">=4.5.0" - -[package.extras] -dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "ipython", "mypy (==1.10.0)", "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 (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] -test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] - -[[package]] -name = "eth-utils" -version = "5.3.0" -description = "eth-utils: Common utility functions for python code that interacts with Ethereum" -optional = false -python-versions = "<4,>=3.8" -files = [ - {file = "eth_utils-5.3.0-py3-none-any.whl", hash = "sha256:ac184883ab299d923428bbe25dae5e356979a3993e0ef695a864db0a20bc262d"}, - {file = "eth_utils-5.3.0.tar.gz", hash = "sha256:1f096867ac6be895f456fa3acb26e9573ae66e753abad9208f316d24d6178156"}, -] - -[package.dependencies] -cytoolz = {version = ">=0.10.1", markers = "implementation_name == \"cpython\""} -eth-hash = ">=0.3.1" -eth-typing = ">=5.0.0" -pydantic = ">=2.0.0,<3" -toolz = {version = ">0.8.2", markers = "implementation_name == \"pypy\""} - -[package.extras] -dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "eth-hash[pycryptodome]", "hypothesis (>=4.43.0)", "ipython", "mypy (==1.10.0)", "mypy (==1.10.0)", "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 (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] -test = ["hypothesis (>=4.43.0)", "mypy (==1.10.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] - -[[package]] -name = "exceptiongroup" -version = "1.3.0" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, - {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} - -[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.2.0" -description = "Get the currently executing AST node of a frame, and other information" -optional = false -python-versions = ">=3.8" -files = [ - {file = "executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa"}, - {file = "executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755"}, -] - -[package.extras] -tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] - -[[package]] -name = "fancycompleter" -version = "0.11.1" -description = "colorful TAB completion for Python prompt" -optional = false -python-versions = ">=3.8" -files = [ - {file = "fancycompleter-0.11.1-py3-none-any.whl", hash = "sha256:44243d7fab37087208ca5acacf8f74c0aa4d733d04d593857873af7513cdf8a6"}, - {file = "fancycompleter-0.11.1.tar.gz", hash = "sha256:5b4ad65d76b32b1259251516d0f1cb2d82832b1ff8506697a707284780757f69"}, -] - -[package.dependencies] -pyreadline3 = {version = "*", markers = "platform_system == \"Windows\" and python_version < \"3.13\""} -pyrepl = {version = ">=0.11.3", markers = "python_version < \"3.13\""} - -[package.extras] -dev = ["fancycompleter[tests]", "mypy", "ruff (==0.11.8)"] -tests = ["pytest", "pytest-cov"] - -[[package]] -name = "fastjsonschema" -version = "2.21.1" -description = "Fastest Python implementation of JSON schema" -optional = false -python-versions = "*" -files = [ - {file = "fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667"}, - {file = "fastjsonschema-2.21.1.tar.gz", hash = "sha256:794d4f0a58f848961ba16af7b9c85a3e88cd360df008c59aac6fc5ae9323b5d4"}, -] - -[package.extras] -devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] - -[[package]] -name = "filelock" -version = "3.18.0" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.9" -files = [ - {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, - {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] -typing = ["typing-extensions (>=4.12.2)"] - -[[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.3.1" -description = "hexbytes: Python `bytes` subclass that decodes hex, with a readable console output" -optional = false -python-versions = "<4,>=3.8" -files = [ - {file = "hexbytes-1.3.1-py3-none-any.whl", hash = "sha256:da01ff24a1a9a2b1881c4b85f0e9f9b0f51b526b379ffa23832ae7899d29c2c7"}, - {file = "hexbytes-1.3.1.tar.gz", hash = "sha256:a657eebebdfe27254336f98d8af6e2236f3f83aed164b87466b6cf6c5f5a4765"}, -] - -[package.extras] -dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "eth_utils (>=2.0.0)", "hypothesis (>=3.44.24)", "ipython", "mypy (==1.10.0)", "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 (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] -test = ["eth_utils (>=2.0.0)", "hypothesis (>=3.44.24)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] - -[[package]] -name = "hypothesis" -version = "6.136.6" -description = "A library for property-based testing" -optional = false -python-versions = ">=3.9" -files = [ - {file = "hypothesis-6.136.6-py3-none-any.whl", hash = "sha256:1d6296dde36d42263bd44a084c74e91467e78186676e410167f920aa0374a9e7"}, - {file = "hypothesis-6.136.6.tar.gz", hash = "sha256:2ad2e4f2012be4d41c6515b0628d84d48af6e6c38b4db50840bd9ac0899f5856"}, -] - -[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 = ["black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.93)", "django (>=4.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.24)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.19.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2025.2)", "watchdog (>=4.0.0)"] -cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"] -codemods = ["libcst (>=0.3.16)"] -crosshair = ["crosshair-tool (>=0.0.93)", "hypothesis-crosshair (>=0.0.24)"] -dateutil = ["python-dateutil (>=1.4)"] -django = ["django (>=4.2)"] -dpcontracts = ["dpcontracts (>=0.4)"] -ghostwriter = ["black (>=19.10b0)"] -lark = ["lark (>=0.10.1)"] -numpy = ["numpy (>=1.19.3)"] -pandas = ["pandas (>=1.1)"] -pytest = ["pytest (>=4.6)"] -pytz = ["pytz (>=2014.1)"] -redis = ["redis (>=3.0.0)"] -watchdog = ["watchdog (>=4.0.0)"] -zoneinfo = ["tzdata (>=2025.2)"] - -[[package]] -name = "idna" -version = "3.10" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.6" -files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, -] - -[package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "immutables" -version = "0.21" -description = "Immutable Collections" -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "immutables-0.21-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:14cb09d4f4577ad9ab8770a340dc2158e0a5ab5775cb34c75960167a31104212"}, - {file = "immutables-0.21-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:22ba593f95044ac60d2af463f3dc86cd0e223f8c51df85dff65d663d93e19f51"}, - {file = "immutables-0.21-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25afc81a7bcf26c8364f85e52a14e0095344343e79493c73b0e9a765310a0bed"}, - {file = "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"}, - {file = "immutables-0.21-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ba8bca21a1d034f4577ede1e9553a681dd01199c06b563f1a8316f2623b64985"}, - {file = "immutables-0.21-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:39337bfb42f83dd787a81e2d00e90efa17c4a39a9cf1210b8a50dafe32438aae"}, - {file = "immutables-0.21-cp310-cp310-win32.whl", hash = "sha256:b24aa98f6cdae4ba15baf3aa00e84223bafcd0d3fd7f0443474527ec951845e1"}, - {file = "immutables-0.21-cp310-cp310-win_amd64.whl", hash = "sha256:715f8e5f8e1c35f036f9ac62eaf8b672eec1cdc2b4f9b73864cc64eccc76661c"}, - {file = "immutables-0.21-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5d780c38067047911a2e06a86ba063ba0055618ab5573c8198ef3f368e321303"}, - {file = "immutables-0.21-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9aab9d0f0016f6e0bfe7e4a4cb831ef20063da6468b1bbc71d06ef285781ee9e"}, - {file = "immutables-0.21-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ff83390b05d3372acb9a0c928f6cc20c78e74ca20ed88eb941f84a63b65e444"}, - {file = "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"}, - {file = "immutables-0.21-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc7844c9fbb5bece5bfdf2bf8ea74d308f42f40b0665fd25c58abf56d7db024a"}, - {file = "immutables-0.21-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:984106fa4345efd9f96de22e9949fc97bac8598bdebee03c20b2497a88bff3b7"}, - {file = "immutables-0.21-cp311-cp311-win32.whl", hash = "sha256:1bdb5200518518601377e4877d5034e7c535e9ea8a9d601ed8b0eedef0c7becd"}, - {file = "immutables-0.21-cp311-cp311-win_amd64.whl", hash = "sha256:dd00c34f431c54c95e7b84bfdbdeacb4f039a6a24eb0c1f7aa4b168bb9a6ad0a"}, - {file = "immutables-0.21-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef1ed262094b755903122c3c3a83ad0e0d5c3ab7887cda12b2fe878769d1ee0d"}, - {file = "immutables-0.21-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce604f81d9d8f26e60b52ebcb56bb5c0462c8ea50fb17868487d15f048a2f13e"}, - {file = "immutables-0.21-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b48b116aaca4500398058b5a87814857a60c4cb09417fecc12d7da0f5639b73d"}, - {file = "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"}, - {file = "immutables-0.21-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e44346e2221a5a676c880ca8e0e6429fa24d1a4ae562573f5c04d7f2e759b030"}, - {file = "immutables-0.21-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8b10139b529a460e53fe8be699ebd848c54c8a33ebe67763bcfcc809a475a26f"}, - {file = "immutables-0.21-cp312-cp312-win32.whl", hash = "sha256:fc512d808662614feb17d2d92e98f611d69669a98c7af15910acf1dc72737038"}, - {file = "immutables-0.21-cp312-cp312-win_amd64.whl", hash = "sha256:461dcb0f58a131045155e52a2c43de6ec2fe5ba19bdced6858a3abb63cee5111"}, - {file = "immutables-0.21-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:79674b51aa8dd983f9ac55f7f67b433b1df84a6b4f28ab860588389a5659485b"}, - {file = "immutables-0.21-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93c8350f8f7d0d9693f708229d9d0578e6f3b785ce6da4bced1da97137aacfad"}, - {file = "immutables-0.21-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:583d2a63e444ce1538cc2bda56ae1f4a1a11473dbc0377c82b516bc7eec3b81e"}, - {file = "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"}, - {file = "immutables-0.21-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:338bede057250b33716a3e4892e15df0bf5a5ddbf1d67ead996b3e680b49ef9e"}, - {file = "immutables-0.21-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8781c89583b68f604cf30f0978b722165824c3075888639fde771bf1a3e12dc0"}, - {file = "immutables-0.21-cp313-cp313-win32.whl", hash = "sha256:e97ea83befad873712f283c0cccd630f70cba753e207b4868af28d5b85e9dc54"}, - {file = "immutables-0.21-cp313-cp313-win_amd64.whl", hash = "sha256:cfcb23bd898f5a4ef88692b42c51f52ca7373a35ba4dcc215060a668639eb5da"}, - {file = "immutables-0.21-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07a37d8699255402a10784d4d45f2bcc00ca7dba8da763207a834b15767e6c62"}, - {file = "immutables-0.21-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9139fd80bb05501216f49c4306bb80d0c1a08c3f0f621ed2939bf52d7f762661"}, - {file = "immutables-0.21-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc6fc7e917e281361ad243be1a3cb56a7633de88ee67c94cdf5651958ead30d9"}, - {file = "immutables-0.21-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6a577f55eaaf763b685eef9710edbeb7ee95e2e5f54e7e5e0fd0f60ae2eb648"}, - {file = "immutables-0.21-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ca912c1bb35615ccbe361a6bb76e6fd43827394102467967d5599d78b50dd0f4"}, - {file = "immutables-0.21-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:586e88ca7ed44b7bb2cd7b212abd2637b51bd95bdb4856ab111b44715a62071c"}, - {file = "immutables-0.21-cp38-cp38-win32.whl", hash = "sha256:21adc6b478a58692c79c5bf316b39d3fd0552441d2b38eef1782a7555deee484"}, - {file = "immutables-0.21-cp38-cp38-win_amd64.whl", hash = "sha256:ecff5274357dc18aae053e5e10b8eee5e9b4d6cc774d34148c992cb2eb787ec3"}, - {file = "immutables-0.21-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e2aadf3bdd90daa0e8cb9c3cde4070e1021036e3b57f571a007ce24f323e47a9"}, - {file = "immutables-0.21-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5f8f507731d4d15e0c579aa77d8482471f988dc0f451e4bf3853ec36ccd42627"}, - {file = "immutables-0.21-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb9a378a4480381d7d3d63b0d201cf610eae0bf70e26a9306e3e631c9bd64010"}, - {file = "immutables-0.21-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7b5920bbfcaf038894c8ce4ed2eff0b31c3559810a61806db751be8ab4d703"}, - {file = "immutables-0.21-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8b90702d1fe313e8273ae7abb46fc0f0a87b47c1c9a83aed9a161301146e655c"}, - {file = "immutables-0.21-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:71cbbc6fbe7e7321648047ff9273f4605f8bd5ce456841a65ef151080e9d3481"}, - {file = "immutables-0.21-cp39-cp39-win32.whl", hash = "sha256:c44f286c47dc0d4d7b5bf19fbe975e6d57c56d2878cea413e1ec7a4bfffb2727"}, - {file = "immutables-0.21-cp39-cp39-win_amd64.whl", hash = "sha256:cf15314c39484b8947a4e20c3526021272510592fb2807b5136a2fcd6ab0151b"}, - {file = "immutables-0.21.tar.gz", hash = "sha256:b55ffaf0449790242feb4c56ab799ea7af92801a0a43f9e2f4f8af2ab24dfc4a"}, -] - -[package.extras] -test = ["flake8 (>=5.0,<6.0)", "mypy (>=1.4,<2.0)", "pycodestyle (>=2.9,<3.0)", "pytest (>=7.4,<8.0)"] - -[[package]] -name = "importlib-metadata" -version = "8.7.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.9" -files = [ - {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, - {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -perf = ["ipython"] -test = ["flufl.flake8", "importlib_resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["pytest-mypy"] - -[[package]] -name = "iniconfig" -version = "2.1.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.8" -files = [ - {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, - {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, -] - -[[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.37.0" -description = "IPython: Productive Interactive Computing" -optional = false -python-versions = ">=3.10" -files = [ - {file = "ipython-8.37.0-py3-none-any.whl", hash = "sha256:ed87326596b878932dbcb171e3e698845434d8c61b8d8cd474bf663041a9dcf2"}, - {file = "ipython-8.37.0.tar.gz", hash = "sha256:ca815841e1a41a1e6b73a0b08f3038af9b2252564d01fc405356d34033012216"}, -] - -[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 = ["packaging", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"] -test-extra = ["curio", "ipython[test]", "jupyter_ai", "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.2" -description = "An autocompletion tool for Python that can be used for text editors." -optional = false -python-versions = ">=3.6" -files = [ - {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"}, - {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"}, -] - -[package.dependencies] -parso = ">=0.8.4,<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 (<9.0.0)"] - -[[package]] -name = "jeepney" -version = "0.9.0" -description = "Low-level, pure Python DBus protocol wrapper." -optional = false -python-versions = ">=3.7" -files = [ - {file = "jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683"}, - {file = "jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732"}, -] - -[package.extras] -test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] -trio = ["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.2" -description = "Python implementation of John Gruber's Markdown." -optional = false -python-versions = ">=3.9" -files = [ - {file = "markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24"}, - {file = "markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45"}, -] - -[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.7.0" -description = "More routines for operating on iterables, beyond itertools" -optional = false -python-versions = ">=3.9" -files = [ - {file = "more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e"}, - {file = "more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3"}, -] - -[[package]] -name = "msgpack" -version = "1.1.1" -description = "MessagePack serializer" -optional = false -python-versions = ">=3.8" -files = [ - {file = "msgpack-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:353b6fc0c36fde68b661a12949d7d49f8f51ff5fa019c1e47c87c4ff34b080ed"}, - {file = "msgpack-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:79c408fcf76a958491b4e3b103d1c417044544b68e96d06432a189b43d1215c8"}, - {file = "msgpack-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78426096939c2c7482bf31ef15ca219a9e24460289c00dd0b94411040bb73ad2"}, - {file = "msgpack-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b17ba27727a36cb73aabacaa44b13090feb88a01d012c0f4be70c00f75048b4"}, - {file = "msgpack-1.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a17ac1ea6ec3c7687d70201cfda3b1e8061466f28f686c24f627cae4ea8efd0"}, - {file = "msgpack-1.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:88d1e966c9235c1d4e2afac21ca83933ba59537e2e2727a999bf3f515ca2af26"}, - {file = "msgpack-1.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f6d58656842e1b2ddbe07f43f56b10a60f2ba5826164910968f5933e5178af75"}, - {file = "msgpack-1.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:96decdfc4adcbc087f5ea7ebdcfd3dee9a13358cae6e81d54be962efc38f6338"}, - {file = "msgpack-1.1.1-cp310-cp310-win32.whl", hash = "sha256:6640fd979ca9a212e4bcdf6eb74051ade2c690b862b679bfcb60ae46e6dc4bfd"}, - {file = "msgpack-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:8b65b53204fe1bd037c40c4148d00ef918eb2108d24c9aaa20bc31f9810ce0a8"}, - {file = "msgpack-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:71ef05c1726884e44f8b1d1773604ab5d4d17729d8491403a705e649116c9558"}, - {file = "msgpack-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:36043272c6aede309d29d56851f8841ba907a1a3d04435e43e8a19928e243c1d"}, - {file = "msgpack-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a32747b1b39c3ac27d0670122b57e6e57f28eefb725e0b625618d1b59bf9d1e0"}, - {file = "msgpack-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a8b10fdb84a43e50d38057b06901ec9da52baac6983d3f709d8507f3889d43f"}, - {file = "msgpack-1.1.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba0c325c3f485dc54ec298d8b024e134acf07c10d494ffa24373bea729acf704"}, - {file = "msgpack-1.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:88daaf7d146e48ec71212ce21109b66e06a98e5e44dca47d853cbfe171d6c8d2"}, - {file = "msgpack-1.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8b55ea20dc59b181d3f47103f113e6f28a5e1c89fd5b67b9140edb442ab67f2"}, - {file = "msgpack-1.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a28e8072ae9779f20427af07f53bbb8b4aa81151054e882aee333b158da8752"}, - {file = "msgpack-1.1.1-cp311-cp311-win32.whl", hash = "sha256:7da8831f9a0fdb526621ba09a281fadc58ea12701bc709e7b8cbc362feabc295"}, - {file = "msgpack-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fd1b58e1431008a57247d6e7cc4faa41c3607e8e7d4aaf81f7c29ea013cb458"}, - {file = "msgpack-1.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae497b11f4c21558d95de9f64fff7053544f4d1a17731c866143ed6bb4591238"}, - {file = "msgpack-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:33be9ab121df9b6b461ff91baac6f2731f83d9b27ed948c5b9d1978ae28bf157"}, - {file = "msgpack-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f64ae8fe7ffba251fecb8408540c34ee9df1c26674c50c4544d72dbf792e5ce"}, - {file = "msgpack-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a494554874691720ba5891c9b0b39474ba43ffb1aaf32a5dac874effb1619e1a"}, - {file = "msgpack-1.1.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb643284ab0ed26f6957d969fe0dd8bb17beb567beb8998140b5e38a90974f6c"}, - {file = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d275a9e3c81b1093c060c3837e580c37f47c51eca031f7b5fb76f7b8470f5f9b"}, - {file = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fd6b577e4541676e0cc9ddc1709d25014d3ad9a66caa19962c4f5de30fc09ef"}, - {file = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb29aaa613c0a1c40d1af111abf025f1732cab333f96f285d6a93b934738a68a"}, - {file = "msgpack-1.1.1-cp312-cp312-win32.whl", hash = "sha256:870b9a626280c86cff9c576ec0d9cbcc54a1e5ebda9cd26dab12baf41fee218c"}, - {file = "msgpack-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:5692095123007180dca3e788bb4c399cc26626da51629a31d40207cb262e67f4"}, - {file = "msgpack-1.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3765afa6bd4832fc11c3749be4ba4b69a0e8d7b728f78e68120a157a4c5d41f0"}, - {file = "msgpack-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8ddb2bcfd1a8b9e431c8d6f4f7db0773084e107730ecf3472f1dfe9ad583f3d9"}, - {file = "msgpack-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:196a736f0526a03653d829d7d4c5500a97eea3648aebfd4b6743875f28aa2af8"}, - {file = "msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d592d06e3cc2f537ceeeb23d38799c6ad83255289bb84c2e5792e5a8dea268a"}, - {file = "msgpack-1.1.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4df2311b0ce24f06ba253fda361f938dfecd7b961576f9be3f3fbd60e87130ac"}, - {file = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4141c5a32b5e37905b5940aacbc59739f036930367d7acce7a64e4dec1f5e0b"}, - {file = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b1ce7f41670c5a69e1389420436f41385b1aa2504c3b0c30620764b15dded2e7"}, - {file = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4147151acabb9caed4e474c3344181e91ff7a388b888f1e19ea04f7e73dc7ad5"}, - {file = "msgpack-1.1.1-cp313-cp313-win32.whl", hash = "sha256:500e85823a27d6d9bba1d057c871b4210c1dd6fb01fbb764e37e4e8847376323"}, - {file = "msgpack-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:6d489fba546295983abd142812bda76b57e33d0b9f5d5b71c09a583285506f69"}, - {file = "msgpack-1.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bba1be28247e68994355e028dcd668316db30c1f758d3241a7b903ac78dcd285"}, - {file = "msgpack-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8f93dcddb243159c9e4109c9750ba5b335ab8d48d9522c5308cd05d7e3ce600"}, - {file = "msgpack-1.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fbbc0b906a24038c9958a1ba7ae0918ad35b06cb449d398b76a7d08470b0ed9"}, - {file = "msgpack-1.1.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:61e35a55a546a1690d9d09effaa436c25ae6130573b6ee9829c37ef0f18d5e78"}, - {file = "msgpack-1.1.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:1abfc6e949b352dadf4bce0eb78023212ec5ac42f6abfd469ce91d783c149c2a"}, - {file = "msgpack-1.1.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:996f2609ddf0142daba4cefd767d6db26958aac8439ee41db9cc0db9f4c4c3a6"}, - {file = "msgpack-1.1.1-cp38-cp38-win32.whl", hash = "sha256:4d3237b224b930d58e9d83c81c0dba7aacc20fcc2f89c1e5423aa0529a4cd142"}, - {file = "msgpack-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:da8f41e602574ece93dbbda1fab24650d6bf2a24089f9e9dbb4f5730ec1e58ad"}, - {file = "msgpack-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5be6b6bc52fad84d010cb45433720327ce886009d862f46b26d4d154001994b"}, - {file = "msgpack-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3a89cd8c087ea67e64844287ea52888239cbd2940884eafd2dcd25754fb72232"}, - {file = "msgpack-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d75f3807a9900a7d575d8d6674a3a47e9f227e8716256f35bc6f03fc597ffbf"}, - {file = "msgpack-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d182dac0221eb8faef2e6f44701812b467c02674a322c739355c39e94730cdbf"}, - {file = "msgpack-1.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b13fe0fb4aac1aa5320cd693b297fe6fdef0e7bea5518cbc2dd5299f873ae90"}, - {file = "msgpack-1.1.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:435807eeb1bc791ceb3247d13c79868deb22184e1fc4224808750f0d7d1affc1"}, - {file = "msgpack-1.1.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4835d17af722609a45e16037bb1d4d78b7bdf19d6c0128116d178956618c4e88"}, - {file = "msgpack-1.1.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a8ef6e342c137888ebbfb233e02b8fbd689bb5b5fcc59b34711ac47ebd504478"}, - {file = "msgpack-1.1.1-cp39-cp39-win32.whl", hash = "sha256:61abccf9de335d9efd149e2fff97ed5974f2481b3353772e8e2dd3402ba2bd57"}, - {file = "msgpack-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:40eae974c873b2992fd36424a5d9407f93e97656d999f43fca9d29f820899084"}, - {file = "msgpack-1.1.1.tar.gz", hash = "sha256:77b79ce34a2bdab2594f490c8e80dd62a02d650b91a75159a63ec413b8d104cd"}, -] - -[[package]] -name = "packaging" -version = "24.2" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, -] - -[[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.12.1.2" -description = "Query metadata from sdists / bdists / installed packages." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pkginfo-1.12.1.2-py3-none-any.whl", hash = "sha256:c783ac885519cab2c34927ccfa6bf64b5a704d7c69afaea583dd9b7afe969343"}, - {file = "pkginfo-1.12.1.2.tar.gz", hash = "sha256:5cd957824ac36f140260964eba3c6be6442a8359b8c48f4adf90210f33a04b7b"}, -] - -[package.extras] -testing = ["pytest", "pytest-cov", "wheel"] - -[[package]] -name = "platformdirs" -version = "4.3.8" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.9" -files = [ - {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, - {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.14.1)"] - -[[package]] -name = "pluggy" -version = "1.6.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, - {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["coverage", "pytest", "pytest-benchmark"] - -[[package]] -name = "poetry" -version = "1.8.5" -description = "Python dependency management and packaging made easy." -optional = false -python-versions = "<4.0,>=3.8" -files = [ - {file = "poetry-1.8.5-py3-none-any.whl", hash = "sha256:5505fba69bf2a792b5d7402d21839c853644337392b745109b86a23010cce5f3"}, - {file = "poetry-1.8.5.tar.gz", hash = "sha256:eb2c88d224f58f36df8f7b36d6c380c07d1001bca28bde620f68fc086e881b70"}, -] - -[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.12,<2.0" -platformdirs = ">=3.0.0,<5" -poetry-core = "1.9.1" -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.26.6,<21.0.0" -xattr = {version = ">=1.0.0,<2.0.0", markers = "sys_platform == \"darwin\""} - -[[package]] -name = "poetry-core" -version = "1.9.1" -description = "Poetry PEP 517 Build Backend" -optional = false -python-versions = "<4.0,>=3.8" -files = [ - {file = "poetry_core-1.9.1-py3-none-any.whl", hash = "sha256:6f45dd3598e0de8d9b0367360253d4c5d4d0110c8f5c71120a14f0e0f116c1a0"}, - {file = "poetry_core-1.9.1.tar.gz", hash = "sha256:7a2d49214bf58b4f17f99d6891d947a9836c9899a67a5069f52d7b67217f61b8"}, -] - -[[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.51" -description = "Library for building powerful interactive command lines in Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07"}, - {file = "prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed"}, -] - -[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.3" -description = "Safely evaluate AST nodes without side effects" -optional = false -python-versions = "*" -files = [ - {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, - {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, -] - -[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 = "8.0.0" -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-8.0.0-py3-none-any.whl", hash = "sha256:c0b2dfc4bde67a55122a392591a10e851a986d5128f680628c80b405f7663e13"}, - {file = "py_ecc-8.0.0.tar.gz", hash = "sha256:56aca19e5dc37294f60c1cc76666c03c2276e7666412b9a559fa0145d099933d"}, -] - -[package.dependencies] -eth-typing = ">=3.0.0" -eth-utils = ">=2.0.0" - -[package.extras] -dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "ipython", "mypy (==1.10.0)", "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 (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] -test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] - -[[package]] -name = "py-evm" -version = "0.12.1b1" -description = "Python implementation of the Ethereum Virtual Machine" -optional = false -python-versions = "<4,>=3.8" -files = [ - {file = "py_evm-0.12.1b1-py3-none-any.whl", hash = "sha256:015ebc8dd95925030be87ce4b3fd31e3c70df626c5ad8665fb06cd611c73eb68"}, - {file = "py_evm-0.12.1b1.tar.gz", hash = "sha256:7bcd9935a3ac2989c8f068b2006f136189281ebc6e279663405cb2c5397ed890"}, -] - -[package.dependencies] -cached-property = ">=1.5.1" -ckzg = ">=2.0.0" -eth-bloom = ">=1.0.3" -eth-keys = ">=0.4.0" -eth-typing = ">=5.2.0" -eth-utils = ">=2.0.0" -lru-dict = ">=1.1.6" -py-ecc = ">=8.0.0" -rlp = ">=3.0.0" -trie = ">=2.0.0" - -[package.extras] -benchmark = ["termcolor (>=1.1.0)", "web3 (>=6.0.0)"] -dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "cached-property (>=1.5.1)", "ckzg (>=2.0.0)", "eth-bloom (>=1.0.3)", "eth-keys (>=0.4.0)", "eth-typing (>=5.2.0)", "eth-utils (>=2.0.0)", "factory-boy (>=3.0.0)", "hypothesis (>=6,<7)", "ipython", "lru-dict (>=1.1.6)", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "py-ecc (>=8.0.0)", "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-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "sphinxcontrib-asyncio (>=0.2.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "trie (>=2.0.0)", "twine", "wheel"] -docs = ["py-evm (>=0.8.0b1)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "sphinxcontrib-asyncio (>=0.2.0)", "towncrier (>=24,<25)"] -eth = ["cached-property (>=1.5.1)", "ckzg (>=2.0.0)", "eth-bloom (>=1.0.3)", "eth-keys (>=0.4.0)", "eth-typing (>=5.2.0)", "eth-utils (>=2.0.0)", "lru-dict (>=1.1.6)", "py-ecc (>=8.0.0)", "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.23.0" -description = "Cryptographic library for Python" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "pycryptodome-3.23.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a176b79c49af27d7f6c12e4b178b0824626f40a7b9fed08f712291b6d54bf566"}, - {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:573a0b3017e06f2cffd27d92ef22e46aa3be87a2d317a5abf7cc0e84e321bd75"}, - {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:63dad881b99ca653302b2c7191998dd677226222a3f2ea79999aa51ce695f720"}, - {file = "pycryptodome-3.23.0-cp27-cp27m-win32.whl", hash = "sha256:b34e8e11d97889df57166eda1e1ddd7676da5fcd4d71a0062a760e75060514b4"}, - {file = "pycryptodome-3.23.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:7ac1080a8da569bde76c0a104589c4f414b8ba296c0b3738cf39a466a9fb1818"}, - {file = "pycryptodome-3.23.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6fe8258e2039eceb74dfec66b3672552b6b7d2c235b2dfecc05d16b8921649a8"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39"}, - {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27"}, - {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843"}, - {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490"}, - {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575"}, - {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b"}, - {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a"}, - {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f"}, - {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa"}, - {file = "pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886"}, - {file = "pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2"}, - {file = "pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c"}, - {file = "pycryptodome-3.23.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:350ebc1eba1da729b35ab7627a833a1a355ee4e852d8ba0447fafe7b14504d56"}, - {file = "pycryptodome-3.23.0-pp27-pypy_73-win32.whl", hash = "sha256:93837e379a3e5fd2bb00302a47aee9fdf7940d83595be3915752c74033d17ca7"}, - {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379"}, - {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4"}, - {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630"}, - {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353"}, - {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5"}, - {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:865d83c906b0fc6a59b510deceee656b6bc1c4fa0d82176e2b77e97a420a996a"}, - {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d4d56153efc4d81defe8b65fd0821ef8b2d5ddf8ed19df31ba2f00872b8002"}, - {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3f2d0aaf8080bda0587d58fc9fe4766e012441e2eed4269a77de6aea981c8be"}, - {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64093fc334c1eccfd3933c134c4457c34eaca235eeae49d69449dc4728079339"}, - {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ce64e84a962b63a47a592690bdc16a7eaf709d2c2697ababf24a0def566899a6"}, - {file = "pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef"}, -] - -[[package]] -name = "pydantic" -version = "2.11.7" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, - {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, -] - -[package.dependencies] -annotated-types = ">=0.6.0" -pydantic-core = "2.33.2" -typing-extensions = ">=4.12.2" -typing-inspection = ">=0.4.0" - -[package.extras] -email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] - -[[package]] -name = "pydantic-core" -version = "2.33.2" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, - {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, - {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, - {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, - {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, - {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, - {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, - {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, - {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, - {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, - {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, - {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, - {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, - {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, - {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, - {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, - {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, - {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, -] - -[package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" - -[[package]] -name = "pygments" -version = "2.19.2" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, - {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pymdown-extensions" -version = "10.16.1" -description = "Extension pack for Python Markdown." -optional = false -python-versions = ">=3.9" -files = [ - {file = "pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d"}, - {file = "pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91"}, -] - -[package.dependencies] -markdown = ">=3.6" -pyyaml = "*" - -[package.extras] -extra = ["pygments (>=2.19.1)"] - -[[package]] -name = "pyproject-hooks" -version = "1.2.0" -description = "Wrappers to call pyproject.toml-based build backend hooks." -optional = false -python-versions = ">=3.7" -files = [ - {file = "pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913"}, - {file = "pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8"}, -] - -[[package]] -name = "pyreadline3" -version = "3.5.4" -description = "A python implementation of GNU readline." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"}, - {file = "pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7"}, -] - -[package.extras] -dev = ["build", "flake8", "mypy", "pytest", "twine"] - -[[package]] -name = "pyrepl" -version = "0.11.4" -description = "A library for building flexible command line interfaces" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyrepl-0.11.4-py3-none-any.whl", hash = "sha256:ac30d6340267a21c39e1b1934f92bca6b8735017d14b17e40f903b2d1563541d"}, - {file = "pyrepl-0.11.4.tar.gz", hash = "sha256:efe988b4a6e5eed587e9769dc2269aeec2b6feec2f5d77995ee85b9ad7cf7063"}, -] - -[package.extras] -dev = ["pyrepl[tests]", "ruff (==0.11.8)"] -tests = ["pexpect", "pytest", "pytest-coverage", "pytest-timeout"] - -[[package]] -name = "pytest" -version = "8.4.1" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, - {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, -] - -[package.dependencies] -colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} -iniconfig = ">=1" -packaging = ">=20" -pluggy = ">=1.5,<2" -pygments = ">=2.7.2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-cov" -version = "6.2.1" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.9" -files = [ - {file = "pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5"}, - {file = "pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2"}, -] - -[package.dependencies] -coverage = {version = ">=7.5", extras = ["toml"]} -pluggy = ">=1.2" -pytest = ">=6.2.5" - -[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.8.0" -description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88"}, - {file = "pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1"}, -] - -[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.3" -description = "A (partial) reimplementation of pywin32 using ctypes/cffi" -optional = false -python-versions = ">=3.6" -files = [ - {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, - {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, -] - -[[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 = "1.1" -description = "A custom YAML tag for referencing environment variables in YAML files." -optional = false -python-versions = ">=3.9" -files = [ - {file = "pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04"}, - {file = "pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff"}, -] - -[package.dependencies] -pyyaml = "*" - -[[package]] -name = "rapidfuzz" -version = "3.13.0" -description = "rapid fuzzy string matching" -optional = false -python-versions = ">=3.9" -files = [ - {file = "rapidfuzz-3.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aafc42a1dc5e1beeba52cd83baa41372228d6d8266f6d803c16dbabbcc156255"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:85c9a131a44a95f9cac2eb6e65531db014e09d89c4f18c7b1fa54979cb9ff1f3"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d7cec4242d30dd521ef91c0df872e14449d1dffc2a6990ede33943b0dae56c3"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e297c09972698c95649e89121e3550cee761ca3640cd005e24aaa2619175464e"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef0f5f03f61b0e5a57b1df7beafd83df993fd5811a09871bad6038d08e526d0d"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d8cf5f7cd6e4d5eb272baf6a54e182b2c237548d048e2882258336533f3f02b7"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9256218ac8f1a957806ec2fb9a6ddfc6c32ea937c0429e88cf16362a20ed8602"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1bdd2e6d0c5f9706ef7595773a81ca2b40f3b33fd7f9840b726fb00c6c4eb2e"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5280be8fd7e2bee5822e254fe0a5763aa0ad57054b85a32a3d9970e9b09bbcbf"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd742c03885db1fce798a1cd87a20f47f144ccf26d75d52feb6f2bae3d57af05"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5435fcac94c9ecf0504bf88a8a60c55482c32e18e108d6079a0089c47f3f8cf6"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:93a755266856599be4ab6346273f192acde3102d7aa0735e2f48b456397a041f"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-win32.whl", hash = "sha256:3abe6a4e8eb4cfc4cda04dd650a2dc6d2934cbdeda5def7e6fd1c20f6e7d2a0b"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:e8ddb58961401da7d6f55f185512c0d6bd24f529a637078d41dd8ffa5a49c107"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-win_arm64.whl", hash = "sha256:c523620d14ebd03a8d473c89e05fa1ae152821920c3ff78b839218ff69e19ca3"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d395a5cad0c09c7f096433e5fd4224d83b53298d53499945a9b0e5a971a84f3a"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7b3eda607a019169f7187328a8d1648fb9a90265087f6903d7ee3a8eee01805"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98e0bfa602e1942d542de077baf15d658bd9d5dcfe9b762aff791724c1c38b70"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bef86df6d59667d9655905b02770a0c776d2853971c0773767d5ef8077acd624"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fedd316c165beed6307bf754dee54d3faca2c47e1f3bcbd67595001dfa11e969"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5158da7f2ec02a930be13bac53bb5903527c073c90ee37804090614cab83c29e"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b6f913ee4618ddb6d6f3e387b76e8ec2fc5efee313a128809fbd44e65c2bbb2"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d25fdbce6459ccbbbf23b4b044f56fbd1158b97ac50994eaae2a1c0baae78301"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:25343ccc589a4579fbde832e6a1e27258bfdd7f2eb0f28cb836d6694ab8591fc"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a9ad1f37894e3ffb76bbab76256e8a8b789657183870be11aa64e306bb5228fd"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5dc71ef23845bb6b62d194c39a97bb30ff171389c9812d83030c1199f319098c"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b7f4c65facdb94f44be759bbd9b6dda1fa54d0d6169cdf1a209a5ab97d311a75"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-win32.whl", hash = "sha256:b5104b62711565e0ff6deab2a8f5dbf1fbe333c5155abe26d2cfd6f1849b6c87"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:9093cdeb926deb32a4887ebe6910f57fbcdbc9fbfa52252c10b56ef2efb0289f"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:f70f646751b6aa9d05be1fb40372f006cc89d6aad54e9d79ae97bd1f5fce5203"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a1a6a906ba62f2556372282b1ef37b26bca67e3d2ea957277cfcefc6275cca7"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2fd0975e015b05c79a97f38883a11236f5a24cca83aa992bd2558ceaa5652b26"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d4e13593d298c50c4f94ce453f757b4b398af3fa0fd2fde693c3e51195b7f69"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed6f416bda1c9133000009d84d9409823eb2358df0950231cc936e4bf784eb97"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1dc82b6ed01acb536b94a43996a94471a218f4d89f3fdd9185ab496de4b2a981"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9d824de871daa6e443b39ff495a884931970d567eb0dfa213d234337343835f"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d18228a2390375cf45726ce1af9d36ff3dc1f11dce9775eae1f1b13ac6ec50f"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5fe634c9482ec5d4a6692afb8c45d370ae86755e5f57aa6c50bfe4ca2bdd87"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:694eb531889f71022b2be86f625a4209c4049e74be9ca836919b9e395d5e33b3"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11b47b40650e06147dee5e51a9c9ad73bb7b86968b6f7d30e503b9f8dd1292db"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:98b8107ff14f5af0243f27d236bcc6e1ef8e7e3b3c25df114e91e3a99572da73"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b836f486dba0aceb2551e838ff3f514a38ee72b015364f739e526d720fdb823a"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-win32.whl", hash = "sha256:4671ee300d1818d7bdfd8fa0608580d7778ba701817216f0c17fb29e6b972514"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e2065f68fb1d0bf65adc289c1bdc45ba7e464e406b319d67bb54441a1b9da9e"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:65cc97c2fc2c2fe23586599686f3b1ceeedeca8e598cfcc1b7e56dc8ca7e2aa7"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:09e908064d3684c541d312bd4c7b05acb99a2c764f6231bd507d4b4b65226c23"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:57c390336cb50d5d3bfb0cfe1467478a15733703af61f6dffb14b1cd312a6fae"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0da54aa8547b3c2c188db3d1c7eb4d1bb6dd80baa8cdaeaec3d1da3346ec9caa"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df8e8c21e67afb9d7fbe18f42c6111fe155e801ab103c81109a61312927cc611"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:461fd13250a2adf8e90ca9a0e1e166515cbcaa5e9c3b1f37545cbbeff9e77f6b"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2b3dd5d206a12deca16870acc0d6e5036abeb70e3cad6549c294eff15591527"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1343d745fbf4688e412d8f398c6e6d6f269db99a54456873f232ba2e7aeb4939"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b1b065f370d54551dcc785c6f9eeb5bd517ae14c983d2784c064b3aa525896df"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:11b125d8edd67e767b2295eac6eb9afe0b1cdc82ea3d4b9257da4b8e06077798"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c33f9c841630b2bb7e69a3fb5c84a854075bb812c47620978bddc591f764da3d"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ae4574cb66cf1e85d32bb7e9ec45af5409c5b3970b7ceb8dea90168024127566"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e05752418b24bbd411841b256344c26f57da1148c5509e34ea39c7eb5099ab72"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-win32.whl", hash = "sha256:0e1d08cb884805a543f2de1f6744069495ef527e279e05370dd7c83416af83f8"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9a7c6232be5f809cd39da30ee5d24e6cadd919831e6020ec6c2391f4c3bc9264"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:3f32f15bacd1838c929b35c84b43618481e1b3d7a61b5ed2db0291b70ae88b53"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cc64da907114d7a18b5e589057e3acaf2fec723d31c49e13fedf043592a3f6a7"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4d9d7f84c8e992a8dbe5a3fdbea73d733da39bf464e62c912ac3ceba9c0cff93"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a79a2f07786a2070669b4b8e45bd96a01c788e7a3c218f531f3947878e0f956"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f338e71c45b69a482de8b11bf4a029993230760120c8c6e7c9b71760b6825a1"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adb40ca8ddfcd4edd07b0713a860be32bdf632687f656963bcbce84cea04b8d8"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48719f7dcf62dfb181063b60ee2d0a39d327fa8ad81b05e3e510680c44e1c078"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9327a4577f65fc3fb712e79f78233815b8a1c94433d0c2c9f6bc5953018b3565"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:200030dfc0a1d5d6ac18e993c5097c870c97c41574e67f227300a1fb74457b1d"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cc269e74cad6043cb8a46d0ce580031ab642b5930562c2bb79aa7fbf9c858d26"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:e62779c6371bd2b21dbd1fdce89eaec2d93fd98179d36f61130b489f62294a92"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f4797f821dc5d7c2b6fc818b89f8a3f37bcc900dd9e4369e6ebf1e525efce5db"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d21f188f6fe4fbf422e647ae9d5a68671d00218e187f91859c963d0738ccd88c"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-win32.whl", hash = "sha256:45dd4628dd9c21acc5c97627dad0bb791764feea81436fb6e0a06eef4c6dceaa"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:624a108122039af89ddda1a2b7ab2a11abe60c1521956f142f5d11bcd42ef138"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-win_arm64.whl", hash = "sha256:435071fd07a085ecbf4d28702a66fd2e676a03369ee497cc38bcb69a46bc77e2"}, - {file = "rapidfuzz-3.13.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe5790a36d33a5d0a6a1f802aa42ecae282bf29ac6f7506d8e12510847b82a45"}, - {file = "rapidfuzz-3.13.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:cdb33ee9f8a8e4742c6b268fa6bd739024f34651a06b26913381b1413ebe7590"}, - {file = "rapidfuzz-3.13.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c99b76b93f7b495eee7dcb0d6a38fb3ce91e72e99d9f78faa5664a881cb2b7d"}, - {file = "rapidfuzz-3.13.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6af42f2ede8b596a6aaf6d49fdee3066ca578f4856b85ab5c1e2145de367a12d"}, - {file = "rapidfuzz-3.13.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c0efa73afbc5b265aca0d8a467ae2a3f40d6854cbe1481cb442a62b7bf23c99"}, - {file = "rapidfuzz-3.13.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7ac21489de962a4e2fc1e8f0b0da4aa1adc6ab9512fd845563fecb4b4c52093a"}, - {file = "rapidfuzz-3.13.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1ba007f4d35a45ee68656b2eb83b8715e11d0f90e5b9f02d615a8a321ff00c27"}, - {file = "rapidfuzz-3.13.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d7a217310429b43be95b3b8ad7f8fc41aba341109dc91e978cd7c703f928c58f"}, - {file = "rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:558bf526bcd777de32b7885790a95a9548ffdcce68f704a81207be4a286c1095"}, - {file = "rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:202a87760f5145140d56153b193a797ae9338f7939eb16652dd7ff96f8faf64c"}, - {file = "rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcccc08f671646ccb1e413c773bb92e7bba789e3a1796fd49d23c12539fe2e4"}, - {file = "rapidfuzz-3.13.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1f219f1e3c3194d7a7de222f54450ce12bc907862ff9a8962d83061c1f923c86"}, - {file = "rapidfuzz-3.13.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ccbd0e7ea1a216315f63ffdc7cd09c55f57851afc8fe59a74184cb7316c0598b"}, - {file = "rapidfuzz-3.13.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a50856f49a4016ef56edd10caabdaf3608993f9faf1e05c3c7f4beeac46bd12a"}, - {file = "rapidfuzz-3.13.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fd05336db4d0b8348d7eaaf6fa3c517b11a56abaa5e89470ce1714e73e4aca7"}, - {file = "rapidfuzz-3.13.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:573ad267eb9b3f6e9b04febce5de55d8538a87c56c64bf8fd2599a48dc9d8b77"}, - {file = "rapidfuzz-3.13.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30fd1451f87ccb6c2f9d18f6caa483116bbb57b5a55d04d3ddbd7b86f5b14998"}, - {file = "rapidfuzz-3.13.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6dd36d4916cf57ddb05286ed40b09d034ca5d4bca85c17be0cb6a21290597d9"}, - {file = "rapidfuzz-3.13.0.tar.gz", hash = "sha256:d2eaf3839e52cbcc0accbe9817a67b4b0fcf70aaeb229cfddc1c28061f9ce5d8"}, -] - -[package.extras] -all = ["numpy"] - -[[package]] -name = "regex" -version = "2025.7.34" -description = "Alternative regular expression module, to replace re." -optional = false -python-versions = ">=3.9" -files = [ - {file = "regex-2025.7.34-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d856164d25e2b3b07b779bfed813eb4b6b6ce73c2fd818d46f47c1eb5cd79bd6"}, - {file = "regex-2025.7.34-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d15a9da5fad793e35fb7be74eec450d968e05d2e294f3e0e77ab03fa7234a83"}, - {file = "regex-2025.7.34-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:95b4639c77d414efa93c8de14ce3f7965a94d007e068a94f9d4997bb9bd9c81f"}, - {file = "regex-2025.7.34-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7de1ceed5a5f84f342ba4a9f4ae589524adf9744b2ee61b5da884b5b659834"}, - {file = "regex-2025.7.34-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02e5860a250cd350c4933cf376c3bc9cb28948e2c96a8bc042aee7b985cfa26f"}, - {file = "regex-2025.7.34-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a5966220b9a1a88691282b7e4350e9599cf65780ca60d914a798cb791aa1177"}, - {file = "regex-2025.7.34-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48fb045bbd4aab2418dc1ba2088a5e32de4bfe64e1457b948bb328a8dc2f1c2e"}, - {file = "regex-2025.7.34-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:20ff8433fa45e131f7316594efe24d4679c5449c0ca69d91c2f9d21846fdf064"}, - {file = "regex-2025.7.34-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c436fd1e95c04c19039668cfb548450a37c13f051e8659f40aed426e36b3765f"}, - {file = "regex-2025.7.34-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0b85241d3cfb9f8a13cefdfbd58a2843f208f2ed2c88181bf84e22e0c7fc066d"}, - {file = "regex-2025.7.34-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:075641c94126b064c65ab86e7e71fc3d63e7ff1bea1fb794f0773c97cdad3a03"}, - {file = "regex-2025.7.34-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:70645cad3407d103d1dbcb4841839d2946f7d36cf38acbd40120fee1682151e5"}, - {file = "regex-2025.7.34-cp310-cp310-win32.whl", hash = "sha256:3b836eb4a95526b263c2a3359308600bd95ce7848ebd3c29af0c37c4f9627cd3"}, - {file = "regex-2025.7.34-cp310-cp310-win_amd64.whl", hash = "sha256:cbfaa401d77334613cf434f723c7e8ba585df162be76474bccc53ae4e5520b3a"}, - {file = "regex-2025.7.34-cp310-cp310-win_arm64.whl", hash = "sha256:bca11d3c38a47c621769433c47f364b44e8043e0de8e482c5968b20ab90a3986"}, - {file = "regex-2025.7.34-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da304313761b8500b8e175eb2040c4394a875837d5635f6256d6fa0377ad32c8"}, - {file = "regex-2025.7.34-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:35e43ebf5b18cd751ea81455b19acfdec402e82fe0dc6143edfae4c5c4b3909a"}, - {file = "regex-2025.7.34-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96bbae4c616726f4661fe7bcad5952e10d25d3c51ddc388189d8864fbc1b3c68"}, - {file = "regex-2025.7.34-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9feab78a1ffa4f2b1e27b1bcdaad36f48c2fed4870264ce32f52a393db093c78"}, - {file = "regex-2025.7.34-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f14b36e6d4d07f1a5060f28ef3b3561c5d95eb0651741474ce4c0a4c56ba8719"}, - {file = "regex-2025.7.34-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85c3a958ef8b3d5079c763477e1f09e89d13ad22198a37e9d7b26b4b17438b33"}, - {file = "regex-2025.7.34-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37555e4ae0b93358fa7c2d240a4291d4a4227cc7c607d8f85596cdb08ec0a083"}, - {file = "regex-2025.7.34-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee38926f31f1aa61b0232a3a11b83461f7807661c062df9eb88769d86e6195c3"}, - {file = "regex-2025.7.34-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a664291c31cae9c4a30589bd8bc2ebb56ef880c9c6264cb7643633831e606a4d"}, - {file = "regex-2025.7.34-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f3e5c1e0925e77ec46ddc736b756a6da50d4df4ee3f69536ffb2373460e2dafd"}, - {file = "regex-2025.7.34-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d428fc7731dcbb4e2ffe43aeb8f90775ad155e7db4347a639768bc6cd2df881a"}, - {file = "regex-2025.7.34-cp311-cp311-win32.whl", hash = "sha256:e154a7ee7fa18333ad90b20e16ef84daaeac61877c8ef942ec8dfa50dc38b7a1"}, - {file = "regex-2025.7.34-cp311-cp311-win_amd64.whl", hash = "sha256:24257953d5c1d6d3c129ab03414c07fc1a47833c9165d49b954190b2b7f21a1a"}, - {file = "regex-2025.7.34-cp311-cp311-win_arm64.whl", hash = "sha256:3157aa512b9e606586900888cd469a444f9b898ecb7f8931996cb715f77477f0"}, - {file = "regex-2025.7.34-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7f7211a746aced993bef487de69307a38c5ddd79257d7be83f7b202cb59ddb50"}, - {file = "regex-2025.7.34-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fb31080f2bd0681484b275461b202b5ad182f52c9ec606052020fe13eb13a72f"}, - {file = "regex-2025.7.34-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0200a5150c4cf61e407038f4b4d5cdad13e86345dac29ff9dab3d75d905cf130"}, - {file = "regex-2025.7.34-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:739a74970e736df0773788377969c9fea3876c2fc13d0563f98e5503e5185f46"}, - {file = "regex-2025.7.34-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4fef81b2f7ea6a2029161ed6dea9ae13834c28eb5a95b8771828194a026621e4"}, - {file = "regex-2025.7.34-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea74cf81fe61a7e9d77989050d0089a927ab758c29dac4e8e1b6c06fccf3ebf0"}, - {file = "regex-2025.7.34-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4636a7f3b65a5f340ed9ddf53585c42e3ff37101d383ed321bfe5660481744b"}, - {file = "regex-2025.7.34-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cef962d7834437fe8d3da6f9bfc6f93f20f218266dcefec0560ed7765f5fe01"}, - {file = "regex-2025.7.34-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:cbe1698e5b80298dbce8df4d8d1182279fbdaf1044e864cbc9d53c20e4a2be77"}, - {file = "regex-2025.7.34-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:32b9f9bcf0f605eb094b08e8da72e44badabb63dde6b83bd530580b488d1c6da"}, - {file = "regex-2025.7.34-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:524c868ba527eab4e8744a9287809579f54ae8c62fbf07d62aacd89f6026b282"}, - {file = "regex-2025.7.34-cp312-cp312-win32.whl", hash = "sha256:d600e58ee6d036081c89696d2bdd55d507498a7180df2e19945c6642fac59588"}, - {file = "regex-2025.7.34-cp312-cp312-win_amd64.whl", hash = "sha256:9a9ab52a466a9b4b91564437b36417b76033e8778e5af8f36be835d8cb370d62"}, - {file = "regex-2025.7.34-cp312-cp312-win_arm64.whl", hash = "sha256:c83aec91af9c6fbf7c743274fd952272403ad9a9db05fe9bfc9df8d12b45f176"}, - {file = "regex-2025.7.34-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3c9740a77aeef3f5e3aaab92403946a8d34437db930a0280e7e81ddcada61f5"}, - {file = "regex-2025.7.34-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:69ed3bc611540f2ea70a4080f853741ec698be556b1df404599f8724690edbcd"}, - {file = "regex-2025.7.34-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d03c6f9dcd562c56527c42b8530aad93193e0b3254a588be1f2ed378cdfdea1b"}, - {file = "regex-2025.7.34-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6164b1d99dee1dfad33f301f174d8139d4368a9fb50bf0a3603b2eaf579963ad"}, - {file = "regex-2025.7.34-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1e4f4f62599b8142362f164ce776f19d79bdd21273e86920a7b604a4275b4f59"}, - {file = "regex-2025.7.34-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:72a26dcc6a59c057b292f39d41465d8233a10fd69121fa24f8f43ec6294e5415"}, - {file = "regex-2025.7.34-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5273fddf7a3e602695c92716c420c377599ed3c853ea669c1fe26218867002f"}, - {file = "regex-2025.7.34-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c1844be23cd40135b3a5a4dd298e1e0c0cb36757364dd6cdc6025770363e06c1"}, - {file = "regex-2025.7.34-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dde35e2afbbe2272f8abee3b9fe6772d9b5a07d82607b5788e8508974059925c"}, - {file = "regex-2025.7.34-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f6e8e7af516a7549412ce57613e859c3be27d55341a894aacaa11703a4c31a"}, - {file = "regex-2025.7.34-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:469142fb94a869beb25b5f18ea87646d21def10fbacb0bcb749224f3509476f0"}, - {file = "regex-2025.7.34-cp313-cp313-win32.whl", hash = "sha256:da7507d083ee33ccea1310447410c27ca11fb9ef18c95899ca57ff60a7e4d8f1"}, - {file = "regex-2025.7.34-cp313-cp313-win_amd64.whl", hash = "sha256:9d644de5520441e5f7e2db63aec2748948cc39ed4d7a87fd5db578ea4043d997"}, - {file = "regex-2025.7.34-cp313-cp313-win_arm64.whl", hash = "sha256:7bf1c5503a9f2cbd2f52d7e260acb3131b07b6273c470abb78568174fe6bde3f"}, - {file = "regex-2025.7.34-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:8283afe7042d8270cecf27cca558873168e771183d4d593e3c5fe5f12402212a"}, - {file = "regex-2025.7.34-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6c053f9647e3421dd2f5dff8172eb7b4eec129df9d1d2f7133a4386319b47435"}, - {file = "regex-2025.7.34-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a16dd56bbcb7d10e62861c3cd000290ddff28ea142ffb5eb3470f183628011ac"}, - {file = "regex-2025.7.34-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69c593ff5a24c0d5c1112b0df9b09eae42b33c014bdca7022d6523b210b69f72"}, - {file = "regex-2025.7.34-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98d0ce170fcde1a03b5df19c5650db22ab58af375aaa6ff07978a85c9f250f0e"}, - {file = "regex-2025.7.34-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d72765a4bff8c43711d5b0f5b452991a9947853dfa471972169b3cc0ba1d0751"}, - {file = "regex-2025.7.34-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4494f8fd95a77eb434039ad8460e64d57baa0434f1395b7da44015bef650d0e4"}, - {file = "regex-2025.7.34-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4f42b522259c66e918a0121a12429b2abcf696c6f967fa37bdc7b72e61469f98"}, - {file = "regex-2025.7.34-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:aaef1f056d96a0a5d53ad47d019d5b4c66fe4be2da87016e0d43b7242599ffc7"}, - {file = "regex-2025.7.34-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:656433e5b7dccc9bc0da6312da8eb897b81f5e560321ec413500e5367fcd5d47"}, - {file = "regex-2025.7.34-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e91eb2c62c39705e17b4d42d4b86c4e86c884c0d15d9c5a47d0835f8387add8e"}, - {file = "regex-2025.7.34-cp314-cp314-win32.whl", hash = "sha256:f978ddfb6216028c8f1d6b0f7ef779949498b64117fc35a939022f67f810bdcb"}, - {file = "regex-2025.7.34-cp314-cp314-win_amd64.whl", hash = "sha256:4b7dc33b9b48fb37ead12ffc7bdb846ac72f99a80373c4da48f64b373a7abeae"}, - {file = "regex-2025.7.34-cp314-cp314-win_arm64.whl", hash = "sha256:4b8c4d39f451e64809912c82392933d80fe2e4a87eeef8859fcc5380d0173c64"}, - {file = "regex-2025.7.34-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fd5edc3f453de727af267c7909d083e19f6426fc9dd149e332b6034f2a5611e6"}, - {file = "regex-2025.7.34-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa1cdfb8db96ef20137de5587954c812821966c3e8b48ffc871e22d7ec0a4938"}, - {file = "regex-2025.7.34-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:89c9504fc96268e8e74b0283e548f53a80c421182a2007e3365805b74ceef936"}, - {file = "regex-2025.7.34-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33be70d75fa05a904ee0dc43b650844e067d14c849df7e82ad673541cd465b5f"}, - {file = "regex-2025.7.34-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57d25b6732ea93eeb1d090e8399b6235ca84a651b52d52d272ed37d3d2efa0f1"}, - {file = "regex-2025.7.34-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:baf2fe122a3db1c0b9f161aa44463d8f7e33eeeda47bb0309923deb743a18276"}, - {file = "regex-2025.7.34-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a764a83128af9c1a54be81485b34dca488cbcacefe1e1d543ef11fbace191e1"}, - {file = "regex-2025.7.34-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c7f663ccc4093877f55b51477522abd7299a14c5bb7626c5238599db6a0cb95d"}, - {file = "regex-2025.7.34-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4913f52fbc7a744aaebf53acd8d3dc1b519e46ba481d4d7596de3c862e011ada"}, - {file = "regex-2025.7.34-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:efac4db9e044d47fd3b6b0d40b6708f4dfa2d8131a5ac1d604064147c0f552fd"}, - {file = "regex-2025.7.34-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:7373afae7cfb716e3b8e15d0184510d518f9d21471f2d62918dbece85f2c588f"}, - {file = "regex-2025.7.34-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9960d162f3fecf6af252534a1ae337e9c2e20d74469fed782903b24e2cc9d3d7"}, - {file = "regex-2025.7.34-cp39-cp39-win32.whl", hash = "sha256:95d538b10eb4621350a54bf14600cc80b514211d91a019dc74b8e23d2159ace5"}, - {file = "regex-2025.7.34-cp39-cp39-win_amd64.whl", hash = "sha256:f7f3071b5faa605b0ea51ec4bb3ea7257277446b053f4fd3ad02b1dcb4e64353"}, - {file = "regex-2025.7.34-cp39-cp39-win_arm64.whl", hash = "sha256:716a47515ba1d03f8e8a61c5013041c8c90f2e21f055203498105d7571b44531"}, - {file = "regex-2025.7.34.tar.gz", hash = "sha256:9ead9765217afd04a86822dfcd4ed2747dfe426e887da413b15ff0ac2457e21a"}, -] - -[[package]] -name = "requests" -version = "2.32.4" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.8" -files = [ - {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, - {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, -] - -[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 = "14.1.0" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f"}, - {file = "rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8"}, -] - -[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.1.0" -description = "rlp: A package for Recursive Length Prefix encoding and decoding" -optional = false -python-versions = "<4,>=3.8" -files = [ - {file = "rlp-4.1.0-py3-none-any.whl", hash = "sha256:8eca394c579bad34ee0b937aecb96a57052ff3716e19c7a578883e767bc5da6f"}, - {file = "rlp-4.1.0.tar.gz", hash = "sha256:be07564270a96f3e225e2c107db263de96b5bc1f27722d2855bd3459a08e95a9"}, -] - -[package.dependencies] -eth-utils = ">=2" - -[package.extras] -dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "hypothesis (>=6.22.0,<6.108.7)", "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 (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] -rust-backend = ["rusty-rlp (>=0.2.1)"] -test = ["hypothesis (>=6.22.0,<6.108.7)", "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.17.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, - {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, -] - -[[package]] -name = "snekmate" -version = "0.1.2" -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.2-py3-none-any.whl", hash = "sha256:df439ba25843f52e6d7e1b58000192f77cd0a5caa9a756075655ead2b66e6314"}, - {file = "snekmate-0.1.2.tar.gz", hash = "sha256:8ed2c6a29b90424543d1792599c5c0b5ccf6988931b43029d1ca7e520a045038"}, -] - -[[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.2.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, -] - -[[package]] -name = "tomlkit" -version = "0.13.3" -description = "Style preserving TOML library" -optional = false -python-versions = ">=3.8" -files = [ - {file = "tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0"}, - {file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"}, -] - -[[package]] -name = "toolz" -version = "1.0.0" -description = "List processing tools and functional utilities" -optional = false -python-versions = ">=3.8" -files = [ - {file = "toolz-1.0.0-py3-none-any.whl", hash = "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236"}, - {file = "toolz-1.0.0.tar.gz", hash = "sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02"}, -] - -[[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.1.0" -description = "Python implementation of the Ethereum Trie structure" -optional = false -python-versions = "<4,>=3.8" -files = [ - {file = "trie-3.1.0-py3-none-any.whl", hash = "sha256:dfc3e6ac0e76f0efa900ec1bfd082f0f1ba87f95cbfd81cc12338b03f4c679c4"}, - {file = "trie-3.1.0.tar.gz", hash = "sha256:b31fd3376d6dccfe8ad13b525e233f2c268d5c48afb90a4de09672423d4b1026"}, -] - -[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)", "bump_my_version (>=0.19.0)", "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 (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["towncrier (>=24,<25)"] -test = ["hypothesis (>=6.56.4,<7)", "pycryptodome", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] - -[[package]] -name = "trove-classifiers" -version = "2025.5.9.12" -description = "Canonical source for classifiers on PyPI (pypi.org)." -optional = false -python-versions = "*" -files = [ - {file = "trove_classifiers-2025.5.9.12-py3-none-any.whl", hash = "sha256:e381c05537adac78881c8fa345fd0e9970159f4e4a04fcc42cfd3129cca640ce"}, - {file = "trove_classifiers-2025.5.9.12.tar.gz", hash = "sha256:7ca7c8a7a76e2cd314468c677c69d12cc2357711fcab4a60f87994c1589e5cb5"}, -] - -[[package]] -name = "typing-extensions" -version = "4.14.1" -description = "Backported and Experimental Type Hints for Python 3.9+" -optional = false -python-versions = ">=3.9" -files = [ - {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, - {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, -] - -[[package]] -name = "typing-inspection" -version = "0.4.1" -description = "Runtime typing introspection tools" -optional = false -python-versions = ">=3.9" -files = [ - {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, - {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, -] - -[package.dependencies] -typing-extensions = ">=4.12.0" - -[[package]] -name = "urllib3" -version = "2.5.0" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.9" -files = [ - {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, - {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, -] - -[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.32.0" -description = "Virtual Python Environment builder" -optional = false -python-versions = ">=3.8" -files = [ - {file = "virtualenv-20.32.0-py3-none-any.whl", hash = "sha256:2c310aecb62e5aa1b06103ed7c2977b81e042695de2697d01017ff0f1034af56"}, - {file = "virtualenv-20.32.0.tar.gz", hash = "sha256:886bf75cadfdc964674e6e33eb74d787dff31ca314ceace03ca5810620f4ecf0"}, -] - -[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.3" -description = "Vyper: the Pythonic Programming Language for the EVM" -optional = false -python-versions = "<4,>=3.10" -files = [ - {file = "vyper-0.4.3-py3-none-any.whl", hash = "sha256:3b9671727c888363740dc678e60336759871487d0e4e9fdd973048fa9635c4fd"}, - {file = "vyper-0.4.3.tar.gz", hash = "sha256:22a7573657401d8a3bc690d65d6b7741680007180978c3a05f9892db31d5dcf5"}, -] - -[package.dependencies] -asttokens = ">=2.0.5,<4" -cbor2 = ">=5.4.6,<6" -immutables = "*" -lark = ">=1.0.0,<2" -packaging = ">=23.1" -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.12.1b1)", "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.12.1b1)", "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.45.1" -description = "A built-package format for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248"}, - {file = "wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729"}, -] - -[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.2.0" -description = "Python wrapper for extended filesystem attributes" -optional = false -python-versions = ">=3.8" -files = [ - {file = "xattr-1.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3df4d8d91e2996c3c72a390ec82e8544acdcb6c7df67b954f1736ff37ea4293e"}, - {file = "xattr-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f5eec248976bbfa6c23df25d4995413df57dccf4161f6cbae36f643e99dbc397"}, - {file = "xattr-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fafecfdedf7e8d455443bec2c3edab8a93d64672619cd1a4ee043a806152e19c"}, - {file = "xattr-1.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c229e245c6c9a85d2fd7d07531498f837dd34670e556b552f73350f11edf000c"}, - {file = "xattr-1.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:376631e2383918fbc3dc9bcaeb9a533e319322d2cff1c119635849edf74e1126"}, - {file = "xattr-1.2.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbae24ab22afe078d549645501ecacaa17229e0b7769c8418fad69b51ad37c9"}, - {file = "xattr-1.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a161160211081d765ac41fa056f4f9b1051f027f08188730fbc9782d0dce623e"}, - {file = "xattr-1.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a542acf6c4e8221664b51b35e0160c44bd0ed1f2fd80019476f7698f4911e560"}, - {file = "xattr-1.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:034f075fc5a9391a1597a6c9a21cb57b688680f0f18ecf73b2efc22b8d330cff"}, - {file = "xattr-1.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:00c26c14c90058338993bb2d3e1cebf562e94ec516cafba64a8f34f74b9d18b4"}, - {file = "xattr-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b4f43dc644db87d5eb9484a9518c34a864cb2e588db34cffc42139bf55302a1c"}, - {file = "xattr-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c7602583fc643ca76576498e2319c7cef0b72aef1936701678589da6371b731b"}, - {file = "xattr-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90c3ad4a9205cceb64ec54616aa90aa42d140c8ae3b9710a0aaa2843a6f1aca7"}, - {file = "xattr-1.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83d87cfe19cd606fc0709d45a4d6efc276900797deced99e239566926a5afedf"}, - {file = "xattr-1.2.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c67dabd9ddc04ead63fbc85aed459c9afcc24abfc5bb3217fff7ec9a466faacb"}, - {file = "xattr-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9a18ee82d8ba2c17f1e8414bfeb421fa763e0fb4acbc1e124988ca1584ad32d5"}, - {file = "xattr-1.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:38de598c47b85185e745986a061094d2e706e9c2d9022210d2c738066990fe91"}, - {file = "xattr-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:15e754e854bdaac366ad3f1c8fbf77f6668e8858266b4246e8c5f487eeaf1179"}, - {file = "xattr-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:daff0c1f5c5e4eaf758c56259c4f72631fa9619875e7a25554b6077dc73da964"}, - {file = "xattr-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:109b11fb3f73a0d4e199962f11230ab5f462e85a8021874f96c1732aa61148d5"}, - {file = "xattr-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c7c12968ce0bf798d8ba90194cef65de768bee9f51a684e022c74cab4218305"}, - {file = "xattr-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37989dabf25ff18773e4aaeebcb65604b9528f8645f43e02bebaa363e3ae958"}, - {file = "xattr-1.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:165de92b0f2adafb336f936931d044619b9840e35ba01079f4dd288747b73714"}, - {file = "xattr-1.2.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82191c006ae4c609b22b9aea5f38f68fff022dc6884c4c0e1dba329effd4b288"}, - {file = "xattr-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2b2e9c87dc643b09d86befad218e921f6e65b59a4668d6262b85308de5dbd1dd"}, - {file = "xattr-1.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:14edd5d47d0bb92b23222c0bb6379abbddab01fb776b2170758e666035ecf3aa"}, - {file = "xattr-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:12183d5eb104d4da787638c7dadf63b718472d92fec6dbe12994ea5d094d7863"}, - {file = "xattr-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c385ea93a18aeb6443a719eb6a6b1d7f7b143a4d1f2b08bc4fadfc429209e629"}, - {file = "xattr-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2d39d7b36842c67ab3040bead7eb6d601e35fa0d6214ed20a43df4ec30b6f9f9"}, - {file = "xattr-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:320ef856bb817f4c40213b6de956dc440d0f23cdc62da3ea02239eb5147093f8"}, - {file = "xattr-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26d306bfb3b5641726f2ee0da6f63a2656aa7fdcfd15de61c476e3ca6bc3277e"}, - {file = "xattr-1.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c67e70d5d8136d328ad13f85b887ffa97690422f1a11fb29ab2f702cf66e825a"}, - {file = "xattr-1.2.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8904d3539afe1a84fc0b7f02fa91da60d2505adf2d5951dc855bf9e75fe322b2"}, - {file = "xattr-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2520516c1d058895eae00b2b2f10833514caea6dc6802eef1e431c474b5317ad"}, - {file = "xattr-1.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:29d06abbef4024b7469fcd0d4ade6d2290582350a4df95fcc48fa48b2e83246b"}, - {file = "xattr-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:093c75f7d9190be355b8e86da3f460b9bfe3d6a176f92852d44dcc3289aa10dc"}, - {file = "xattr-1.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ee3901db48de913dcef004c5d7b477a1f4aadff997445ef62907b10fdad57de"}, - {file = "xattr-1.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b837898a5225c7f7df731783cd78bae2ed81b84bacf020821f1cd2ab2d74de58"}, - {file = "xattr-1.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cedc281811e424ecf6a14208532f7ac646866f91f88e8eadd00d8fe535e505fd"}, - {file = "xattr-1.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf60577caa248f539e4e646090b10d6ad1f54189de9a7f1854c23fdef28f574e"}, - {file = "xattr-1.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:363724f33510d2e7c7e080b389271a1241cb4929a1d9294f89721152b4410972"}, - {file = "xattr-1.2.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97db00596865845efb72f3d565a1f82b01006c5bf5a87d8854a6afac43502593"}, - {file = "xattr-1.2.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0b199ba31078f3e4181578595cd60400ee055b4399672169ceee846d33ff26de"}, - {file = "xattr-1.2.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:b19472dc38150ac09a478c71092738d86882bc9ff687a4a8f7d1a25abce20b5e"}, - {file = "xattr-1.2.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:79f7823b30ed557e0e7ffd9a6b1a821a22f485f5347e54b8d24c4a34b7545ba4"}, - {file = "xattr-1.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8eee258f5774933cb972cff5c3388166374e678980d2a1f417d7d6f61d9ae172"}, - {file = "xattr-1.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2a9de621eadf0466c391363bd6ed903b1a1bcd272422b5183fd06ef79d05347b"}, - {file = "xattr-1.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc714f236f17c57c510ae9ada9962d8e4efc9f9ea91504e2c6a09008f3918ddf"}, - {file = "xattr-1.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:545e0ad3f706724029efd23dec58fb358422ae68ab4b560b712aedeaf40446a0"}, - {file = "xattr-1.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:200bb3cdba057cb721b727607bc340a74c28274f4a628a26011f574860f5846b"}, - {file = "xattr-1.2.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b0b27c889cc9ff0dba62ac8a2eef98f4911c1621e4e8c409d5beb224c4c227c"}, - {file = "xattr-1.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ea7cf8afd717853ad78eba8ca83ff66a53484ba2bb2a4283462bc5c767518174"}, - {file = "xattr-1.2.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:02fa813db054bbb7a61c570ae025bd01c36fc20727b40f49031feb930234bc72"}, - {file = "xattr-1.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2827e23d7a1a20f31162c47ab4bd341a31e83421121978c4ab2aad5cd79ea82b"}, - {file = "xattr-1.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:29ae44247d46e63671311bf7e700826a97921278e2c0c04c2d11741888db41b8"}, - {file = "xattr-1.2.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:629c42c1dd813442d90f281f69b88ef0c9625f604989bef8411428671f70f43e"}, - {file = "xattr-1.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:549f8fbda5da48cafc81ba6ab7bb8e8e14c4b0748c37963dc504bcae505474b7"}, - {file = "xattr-1.2.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa83e677b5f92a3c5c86eaf875e9d3abbc43887ff1767178def865fa9f12a3a0"}, - {file = "xattr-1.2.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb669f01627962ce2bc556f19d421162247bc2cad0d4625d6ea5eb32af4cf29b"}, - {file = "xattr-1.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:212156aa5fb987a53211606bc09e6fea3eda3855af9f2940e40df5a2a592425a"}, - {file = "xattr-1.2.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:7dc4fa9448a513077c5ccd1ce428ff0682cdddfc71301dbbe4ee385c74517f73"}, - {file = "xattr-1.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e4b93f2e74793b61c0a7b7bdef4a3813930df9c01eda72fad706b8db7658bc2"}, - {file = "xattr-1.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dddd5f6d0bb95b099d6a3888c248bf246525647ccb8cf9e8f0fc3952e012d6fb"}, - {file = "xattr-1.2.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68fbdffebe8c398a82c84ecf5e6f6a3adde9364f891cba066e58352af404a45c"}, - {file = "xattr-1.2.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c9ee84de7cd4a6d61b0b79e2f58a6bdb13b03dbad948489ebb0b73a95caee7ae"}, - {file = "xattr-1.2.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5594fcbc38fdbb3af16a8ad18c37c81c8814955f0d636be857a67850cd556490"}, - {file = "xattr-1.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:017aac8005e1e84d5efa4b86c0896c6eb96f2331732d388600a5b999166fec1c"}, - {file = "xattr-1.2.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d27a64f695440450c119ae4bc8f54b0b726a812ebea1666fff3873236936f36"}, - {file = "xattr-1.2.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f7e7067e1a400ad4485536a9e84c3330373086b2324fafa26d07527eeb4b175"}, - {file = "xattr-1.2.0.tar.gz", hash = "sha256:a64c8e21eff1be143accf80fd3b8fde3e28a478c37da298742af647ac3e5e0a7"}, -] - -[package.dependencies] -cffi = ">=1.16.0" - -[package.extras] -test = ["pytest"] - -[[package]] -name = "zipp" -version = "3.23.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.9" -files = [ - {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, - {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - -[metadata] -lock-version = "2.0" -python-versions = ">=3.10,<4" -content-hash = "f9e74e34a08e706327695c531ab4017e35e1c68707de1a94bb7f80e656e81195" diff --git a/pyproject.toml b/pyproject.toml index 5574bb85..9df5c90e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,26 +1,23 @@ -[tool.poetry] +[project] name = "curve-stablecoin" version = "0.2.0" -description = "" -authors = ["Michael Egorov "] +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.3" -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", + "titanoboa>=0.2.7", + "hypothesis>=6.99.0", + "pytest>=8.0.0", + "pytest-xdist>=3.5", + "pytest-forked>=1.6.0", + "pytest-cov>=4.0.0", + "snekmate>=0.1.1", +] -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +[tool.uv] +package = false # This is not a Python package diff --git a/tests/lending/conftest.py b/tests/lending/conftest.py index 011746cd..8c929926 100644 --- a/tests/lending/conftest.py +++ b/tests/lending/conftest.py @@ -16,7 +16,7 @@ def amm_impl(amm_interface, admin): @pytest.fixture(scope="session") def controller_interface(): - return boa.load_partial('contracts/lending/Controller.vy') + return boa.load_partial('contracts/lending/LLController.vy') @pytest.fixture(scope="session") diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..f1882f84 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1710 @@ +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.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, +] + +[[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.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/ee/3b2fcbac3a4192e5d079aaa1850dff2f9ac625861c4c644819c2b34292ec/bitarray-3.6.0.tar.gz", hash = "sha256:20febc849a1f858e6a57a7d47b323fe9e727c579ddd526d317ad8831748a66a8", size = 147946 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/0e/52b66c2c36e938b0fc5160ba92034e949b1f7ec8c9f0b7d53188ebb3e8dc/bitarray-3.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6841c08b51417f8ffe398b2828fc0593440c99525c868f640e0302476745320b", size = 144421 }, + { url = "https://files.pythonhosted.org/packages/f4/67/904b50387cdd8caa8ad66aed03d90bf0acb38d3d1393ead5349a643b2618/bitarray-3.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a04b7a9017b8d0341ebbe77f61b74df1cf1b714f42b671a06f4912dc93d82597", size = 140919 }, + { url = "https://files.pythonhosted.org/packages/3b/d2/4371eba6e4626a663aca00bbf2a83c95498de794dd4536a7f355f177c878/bitarray-3.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:664d462a4c0783fd755fe3440f07b7e46d149859c96caacadf3f28890f19a8de", size = 314230 }, + { url = "https://files.pythonhosted.org/packages/1e/a1/e2ecc61a1441dfb2a8ba7048bbe8a7e75a394a08e4a1ffd9ec530ccbb180/bitarray-3.6.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e997d22e0d1e08c8752f61675a75d93659f7aa4dbeaee54207f8d877817b4a0c", size = 330869 }, + { url = "https://files.pythonhosted.org/packages/d7/5f/68e05c382bbbff1ff9f2c2cc343543bc755a0b7d20948a8ebb08a7e0e417/bitarray-3.6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6755cfcfa7d8966e704d580c831e39818f85e7b2b7852ad22708973176f0009e", size = 322249 }, + { url = "https://files.pythonhosted.org/packages/0f/20/ab8784e2968deb6fba2d2bac6b9190efc1360cc2a6ed57482ad82f5757cf/bitarray-3.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4798f6744fa2633666e17b4ea8ff70250781b52a25afdbf5ffb5e176c58848f1", size = 316564 }, + { url = "https://files.pythonhosted.org/packages/ac/1d/f28a148501f5fec33ad6e7f714bb4953ac3f80b8af4839c66767e874cd7a/bitarray-3.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:efa5834ba5e6c70b22afdca3894097e5a592d8d483c976359654ba990477799a", size = 304445 }, + { url = "https://files.pythonhosted.org/packages/ce/e6/175f048b2e2c26253fbae7e8fc2fdb772bb74e03b2a8b9bfde8acc2f509d/bitarray-3.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d47e2bdeba4fb1986af2ba395ce51223f4d460e6e77119439e78f2b592cafade", size = 311974 }, + { url = "https://files.pythonhosted.org/packages/b8/4b/03633ca351c8cce4902eea78cb1bbd75e4226c31da19e25df4e605372626/bitarray-3.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2a324e3007afb5c667026f5235b35efe3c4a95f1b83cd93aa9fce67b42f08e7c", size = 305521 }, + { url = "https://files.pythonhosted.org/packages/ef/cc/4a327a89b534f64f52d0ef232d3e12d179686914a0103636ddc1f77e89f7/bitarray-3.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:080a7bf55c432abdae74f25dc3dbff407418346aeae1d43e31f65e8ef114f785", size = 329032 }, + { url = "https://files.pythonhosted.org/packages/1b/ab/0000b27c58955b09c56db37fce88d7cc54fd46fee5c472002e2d08be7cb3/bitarray-3.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3eb1390a8b062fe9125e5cc4c5eba990b5d383eec54f2b996e7ce73ac43150f9", size = 327309 }, + { url = "https://files.pythonhosted.org/packages/6c/7d/0ebb9dc7a5bc7c6bb764c43b5e551234e3fdb5d4f06b8cfa4eb4dc7df27c/bitarray-3.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2020102a40edd094c0aa80e09203af71c533c41f76ce3237c99fd194a473ea33", size = 313309 }, + { url = "https://files.pythonhosted.org/packages/b7/f5/830d9f20cfb78759b2adfff01b695b40ebe26d76c256b6b228a4865ac26e/bitarray-3.6.0-cp310-cp310-win32.whl", hash = "sha256:01d6dc548e7fe5c66913c2274f44855b0f8474935acff7811e84fe1f4024c94f", size = 137611 }, + { url = "https://files.pythonhosted.org/packages/c6/65/a180faa2573711ff1d00db3ed203aa88281502da62d8088aa13d39bef56a/bitarray-3.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:8d759cecfa8aab4a1eb4e23b6420126b15c7743e85b33f389916bb98c4ecbb84", size = 144329 }, + { url = "https://files.pythonhosted.org/packages/90/bf/987f01842b3239fd5bb36b6f3aeba3070731d72f8d700a6a317f4731a0d7/bitarray-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7c20d6e6cafce5027e7092beb2ac6eec0d71045d6318b34f36e1387a8c8859a3", size = 144418 }, + { url = "https://files.pythonhosted.org/packages/ac/7e/97902cf29a46d14c2b0a938c0f83bb1de1e117a56df61a88676d278359e8/bitarray-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cf36cadeb9c989f760a13058dbc455e5406ec3d2d247c705c8d4bc6dd1b0fcc6", size = 140917 }, + { url = "https://files.pythonhosted.org/packages/a3/f6/0afe766d4bfc22403861f49a050b8ff9719a1783be25c85376cad2cbf06b/bitarray-3.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30ba4fba3de1dca653de41c879349ec6ca521d85cff6a7ca5d2fdd8f76c93781", size = 321827 }, + { url = "https://files.pythonhosted.org/packages/4d/01/a0a24b2605a8f682283a083ae2c97531446523ef67387189a9570ac5daa2/bitarray-3.6.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77d2368a06a86a18919c05a9b4b0ee9869f770e6a5f414b0fecc911870fe3974", size = 339057 }, + { url = "https://files.pythonhosted.org/packages/55/2f/0684beeaa874f321f0157206f3e625820935eb79837ca49a32aff2834097/bitarray-3.6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a39be79a7c36e9a2e20376261c30deb3cdca86b50f7462ae9ff10a755c6720b9", size = 331191 }, + { url = "https://files.pythonhosted.org/packages/42/82/ead4f1b1aa6ce4112cc59b515453dcf194d7e83cdd5e0c34eb422df9b6c1/bitarray-3.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4695fcd37478988b1d0a16d5bc0df56dcb677fd5db37f1893d993fd3ebef914b", size = 324416 }, + { url = "https://files.pythonhosted.org/packages/90/95/9558548842d249ba87caec43450cded1933e9ca0dec54bfb7cefbd146807/bitarray-3.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52328192d454ca2ddad09fbc088872b014c74b22ecdd5164717dc7e6442014fa", size = 312571 }, + { url = "https://files.pythonhosted.org/packages/d7/cb/f9aad4e436d1bcda7f348a85458b197f54b3e2f9626caba850a79d09caf3/bitarray-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96117212229905da864794df9ea7bd54987c30a5dcbab3432edc3f344231adae", size = 320005 }, + { url = "https://files.pythonhosted.org/packages/28/e4/9830c088551eb9c055871843ed15cece9917249f60f6db7093c5c58cff46/bitarray-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:68f6e64d4867ee79e25c49d7f35b2b1f04a6d6f778176dcf5b759f3b17a02b2b", size = 313190 }, + { url = "https://files.pythonhosted.org/packages/49/53/fde12dcf5eae0aabebc7b97e2226b2cdc9785ada2f38e9256b95c7402d71/bitarray-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:29ed022189a7997de46cb9bd4e2e49d6163d4f8d78dea72ac5a0e0293b856810", size = 337055 }, + { url = "https://files.pythonhosted.org/packages/41/f2/dfb6e9a9bb94ccbe5b369a839df477ab3b73e90b7ba8c92974c26e7e26ba/bitarray-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e71c9dba78671d38a549e3b2d52514f50e199f9d7e18ed9b0180adeef0d04130", size = 335649 }, + { url = "https://files.pythonhosted.org/packages/d8/30/4092ddf8ae0c566b9b157d0859d784e24f3b6d6ed90f437da304e29d3ee0/bitarray-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ddb319f869d497ef2d3d56319360b61284a9a1d8b3de3bc936748698acfda6be", size = 321512 }, + { url = "https://files.pythonhosted.org/packages/d3/a5/1cf6b545a18c71c6984f55758c2ee9595441e537ea424855fb3fdb89597e/bitarray-3.6.0-cp311-cp311-win32.whl", hash = "sha256:25060e7162e44242a449ed1a14a4e94b5aef340812754c443459f19c7954be91", size = 137767 }, + { url = "https://files.pythonhosted.org/packages/4b/66/4cd301ceb0b66281440011e539df82150b36457234ed8a6149136a8ab0ae/bitarray-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2d951002b11962b26afb31f758c18ad39771f287b100fa5adb1d09a47eaaf5b", size = 144547 }, + { url = "https://files.pythonhosted.org/packages/36/1b/91ee39eb1f52261d8734453bf4480b77edb440e86fd8d006ccf66a14b497/bitarray-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9616ea14917d06736339cf36bb9eaf4eb52110a74136b0dc5eff94e92417d22", size = 144173 }, + { url = "https://files.pythonhosted.org/packages/73/5d/edadc94cbdbfd333795d3b52aecb0106eeb163da965485363b34af9ddebe/bitarray-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7e2e1ff784c2cdfd863bad31985851427f2d2796e445cec85080c7510cba4315", size = 140917 }, + { url = "https://files.pythonhosted.org/packages/0e/cd/6a0d3d53118a148b9db13cf0d70a7fc8b61d77af1f85cb24a0c2dae62839/bitarray-3.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:911b4a16dce370657e5b8d8b6ba0fbb50dd5e2b24c4416f4b9e664503d3f0502", size = 324769 }, + { url = "https://files.pythonhosted.org/packages/3e/85/d12273fc975f40da1baf8dbc50b240782010d18ade62b28d8b03a21dfb1c/bitarray-3.6.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b47843f2f288fa746dead4394591a3432a358aaad48240283fa230d6e74b0e7", size = 341502 }, + { url = "https://files.pythonhosted.org/packages/f6/30/193980f3942280f22b2f602380c26e68d3b6e56791c27afac5497b72262b/bitarray-3.6.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8f95daf0ce2b24815ddf62667229ba5dfc0cfee43eb43b2549766170d0f24ae9", size = 333815 }, + { url = "https://files.pythonhosted.org/packages/8a/1c/a35fe5f1eda54d1db3d71b56e39a3b928f1d99546507496a1aef53dceabc/bitarray-3.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c15b9e37bbca59657e4dcc63ad068c821a4676def15f04742c406748a0a11b9c", size = 327371 }, + { url = "https://files.pythonhosted.org/packages/95/aa/b0d164489914a7c9d02f219911c05a57c7859e826fa6c0803c55339589e2/bitarray-3.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9d247fcc33c90f2758f4162693250341e3f38cd094f64390076ef33ad0887f9", size = 315020 }, + { url = "https://files.pythonhosted.org/packages/70/2c/8c69c9c85e7d3d23418ce10067f083d9e4bdf63a88244eb7d004d348d37d/bitarray-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:84bb57010a1ab76cf880424a2e0bce8dd26989849d2122ff073aa11bfc271c27", size = 322487 }, + { url = "https://files.pythonhosted.org/packages/0b/85/878fea379e568bf62175273e19b6c1a8b0b30a202f1885da693e5e037a06/bitarray-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:27d13c7b886afc5d2fc49d6e92f9c96b1f0a14dc7b5502520c29f3da7550d401", size = 316112 }, + { url = "https://files.pythonhosted.org/packages/9e/dc/68d0c84a9c26ae2a291165c7915b45611db16f7143955a5fdd5aaf481a91/bitarray-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c4e75bbf9ade3d2cdf1b607a8b353b17d9b3cf54e88b2a5a773f50ae6f1bfbc", size = 339348 }, + { url = "https://files.pythonhosted.org/packages/be/7c/cf5de46403c4176b96f064614686a584bd90ed37d63ba374a51f0313db6b/bitarray-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:975a118aa019d745f1398613b27fd8789f60a8cea057a00cdc1abedee123ffe6", size = 338601 }, + { url = "https://files.pythonhosted.org/packages/0f/8c/a725109141bc5a2e66a551d56461811d7fa506ace1a473a6455a658f5f03/bitarray-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9ed4a2852b3de7a64884afcc6936db771707943249a060aec8e551c16361d478", size = 324777 }, + { url = "https://files.pythonhosted.org/packages/69/1b/15d438b6cfbee8f01208fa0e47282fb0dcf5df501523d779a85f77584ce2/bitarray-3.6.0-cp312-cp312-win32.whl", hash = "sha256:5dd9edcab8979a50c2c4dec6d5b66789fb6f630bb52ab90a4548111075a75e48", size = 137813 }, + { url = "https://files.pythonhosted.org/packages/d3/80/14af316fe46be0d6c2624d75f138b5d29bf19846fd4e6faab4ca15c0f8b8/bitarray-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:552a93be286ca485914777461b384761519db313e0a7f3012dca424c9610a4d5", size = 144766 }, + { url = "https://files.pythonhosted.org/packages/90/2c/21066c7a97b2c88037b0fc04480fa13b0031c30c6f70452dc9c84fb2b087/bitarray-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3f96f57cea35ba19fd23a20b38fa0dfa3d87d582507129b8c8e314aa298f59b", size = 144156 }, + { url = "https://files.pythonhosted.org/packages/34/a5/9cc42ea0c440ac1c2a65375688ac5891da12b3820f4a32440791d25ed668/bitarray-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:81e84054b22babcd6c5cc1eac0de2bfc1054ecdf742720cbfb36efbe89ec6c30", size = 140916 }, + { url = "https://files.pythonhosted.org/packages/d7/66/709d259d855528213b1099facddb08d6108cb0074cf88dc357cdd07bacff/bitarray-3.6.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca643295bf5441dd38dadf7571ca4b63961820eedbffbe46ceba0893bf226203", size = 324713 }, + { url = "https://files.pythonhosted.org/packages/6c/67/831e366ea4f0d52d622482b8475f87040cbc210d8f5f383935a4cc6363fe/bitarray-3.6.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:139963494fc3dd5caee5e38c0a03783ef50be118565e94b1dbb0210770f0b32d", size = 341300 }, + { url = "https://files.pythonhosted.org/packages/66/c9/197375b63ca768ac8b1e624f27dc0eccdd451f94c6b9bf8950500d8da134/bitarray-3.6.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:243825f56b58bef28bfc602992a8c6d09bbc625628c195498d6020120d632a09", size = 333724 }, + { url = "https://files.pythonhosted.org/packages/e1/23/96c882d798b8bc9d5354ad1fba18ad3ad4f3c0a661a296c8e51ca2941e0f/bitarray-3.6.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:583b46b3ba44121de5e87e95ae379932dc5fd2e37ebdf2c11a6d7975891425c1", size = 327276 }, + { url = "https://files.pythonhosted.org/packages/20/8e/51751fe0e6f9fe7980b0467b471ba9ab8d1713a2a6576980d18143511656/bitarray-3.6.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f0be27d06732e2833b672a8fcc32fa195bdb22161eb88f8890de15e30264a01", size = 314903 }, + { url = "https://files.pythonhosted.org/packages/49/7a/e4db9876e6e8bb261e64a384d3adb4372f13099b356e559cec85d022b897/bitarray-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:507e567aee4806576e20752f22533e8b7ec61e7e75062a7ce9222a0675aa0da6", size = 322551 }, + { url = "https://files.pythonhosted.org/packages/aa/5a/9460070e6cb671067cc2e115a82da6fc9ef0958542b98b07a5ed4a05a97b/bitarray-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:22188943a29072b684cd7c99e0b2cfc0af317cea3366c583d820507e6d1f2ed4", size = 316128 }, + { url = "https://files.pythonhosted.org/packages/34/6f/f5d78c8e908750b9c3d5839eca2af5f6e99d6c7fe8a0498ef79a1af90bd8/bitarray-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f92462ea3888c99439f58f7561ecd5dd4cf8b8b1b259ccf5376667b8c46ee747", size = 339337 }, + { url = "https://files.pythonhosted.org/packages/0d/d3/f740b601eae4e28e22d8560877fe9881f1b7a96fcb23b186e8580d328929/bitarray-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3800f3c8c9780f281cf590543fd4b3278fea6988202273a260ecc58136895efb", size = 338607 }, + { url = "https://files.pythonhosted.org/packages/4e/81/b9451089eea0ef66996852d2694b0f5afc0a76b1bc45c9a4f8204ae8674d/bitarray-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a50a66fa34dd7f9dcdbc7602a1b7bf6f9ab030b4f43e892324193423d9ede180", size = 324788 }, + { url = "https://files.pythonhosted.org/packages/82/e8/80620fc60ad34bff647881a4f25c15b992c524e0f7af9c7c6c573b03556e/bitarray-3.6.0-cp313-cp313-win32.whl", hash = "sha256:afa24e5750c9b89ad5a7efef037efe49f4e339f20a94bf678c422c0c71e1207a", size = 137841 }, + { url = "https://files.pythonhosted.org/packages/3b/ee/303be88b847da29a067babc690e231d7838520dc1af57d14dad5a7ca095c/bitarray-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4c5e7edf1e7bcbde3b52058f171a411e2a24a081b3e951d685dfea4c3c383d5", size = 144820 }, + { url = "https://files.pythonhosted.org/packages/94/25/d98d86777779c422cdc1a295848dfdb04b967d5b54e6824c009bd69842cd/bitarray-3.6.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a763dd33d6e27c9b4db3f8089a5fa39179a8a3cf48ce702b24a857d7c621333c", size = 139790 }, + { url = "https://files.pythonhosted.org/packages/f7/37/4a85461ea71e0d735ec4e231a19057df156242f6b51c4d860367d8342ffc/bitarray-3.6.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8cf44b012e7493127ce7ca6e469138ac96b3295a117877d5469aabe7c8728d87", size = 136433 }, + { url = "https://files.pythonhosted.org/packages/27/f3/873ccde5d86369268bceda91056f1baa87f3920f504390c5fd7a451ac964/bitarray-3.6.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e297fd2e58afe17e33dd80c231c3a9d850279a2a8625aed1d39f9be9534809e", size = 144952 }, + { url = "https://files.pythonhosted.org/packages/cb/be/2cb43c165c6a239a9d662279f0be7ffb0903f6f889c1df3bafb3387fea44/bitarray-3.6.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fc8bc65f964c7278deb1b7a69379dab3ecc90095f252deb17365637ebb274d", size = 145693 }, + { url = "https://files.pythonhosted.org/packages/a9/86/1ffd91a77a1a5fa86bbd32d8c3c34bb066595574d3eb60c83a98f556d45f/bitarray-3.6.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa3c925502bd0b957a96a5619134bcdc0382ef73cffd40bad218ced3586bcf8d", size = 147466 }, + { url = "https://files.pythonhosted.org/packages/a9/ff/30a1d2492c1d259a17084015c7b82cc31c49dd60f47f3c7a7b3ee923f45b/bitarray-3.6.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9f7796959c9c036a115d34696563f75d4a2912d3b97c15c15f2a36bdd5496ce9", size = 143118 }, +] + +[[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.6.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/aa/ba55b47d51d27911981a18743b4d3cebfabccbb0598c09801b734cec4184/cbor2-5.6.5.tar.gz", hash = "sha256:b682820677ee1dbba45f7da11898d2720f92e06be36acec290867d5ebf3d7e09", size = 100886 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/f2/eada1d3fcaeddbd0f9a25381eed6b150f2858cf615d33ef7c14f9f86218a/cbor2-5.6.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e16c4a87fc999b4926f5c8f6c696b0d251b4745bc40f6c5aee51d69b30b15ca2", size = 66473 }, + { url = "https://files.pythonhosted.org/packages/62/d8/33ec188eeff9c5e2e8ee8e214d15a0edf8fd001eefa01730e27834cfd410/cbor2-5.6.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87026fc838370d69f23ed8572939bd71cea2b3f6c8f8bb8283f573374b4d7f33", size = 67421 }, + { url = "https://files.pythonhosted.org/packages/fa/88/9b5fc312f21a10e048c348cead933e9f891cc09a295757957d8a5726641f/cbor2-5.6.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a88f029522aec5425fc2f941b3df90da7688b6756bd3f0472ab886d21208acbd", size = 253856 }, + { url = "https://files.pythonhosted.org/packages/bb/41/debb6f35d240caa8078a4a27c03e7eed09737575c564d619e1ee0e829616/cbor2-5.6.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9d15b638539b68aa5d5eacc56099b4543a38b2d2c896055dccf7e83d24b7955", size = 242074 }, + { url = "https://files.pythonhosted.org/packages/0b/ba/5c07b001bb26bd91d96e4b3b720676a909e1acdbaf7d250a47c5d7a5a0f7/cbor2-5.6.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:47261f54a024839ec649b950013c4de5b5f521afe592a2688eebbe22430df1dc", size = 241723 }, + { url = "https://files.pythonhosted.org/packages/b2/d5/27eced2cee4725bb876875f26d659063e2b7750d0dfb7572f451b569c4d1/cbor2-5.6.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:559dcf0d897260a9e95e7b43556a62253e84550b77147a1ad4d2c389a2a30192", size = 240534 }, + { url = "https://files.pythonhosted.org/packages/20/55/bd657fdedb0d046762f9bf567b262b233175995d40302cdaa71d9aadcec8/cbor2-5.6.5-cp310-cp310-win_amd64.whl", hash = "sha256:5b856fda4c50c5bc73ed3664e64211fa4f015970ed7a15a4d6361bd48462feaf", size = 66291 }, + { url = "https://files.pythonhosted.org/packages/bd/b7/ef045245180510305648fd604244d3bb1ecf1b20de68f42ab5bc20198024/cbor2-5.6.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:863e0983989d56d5071270790e7ed8ddbda88c9e5288efdb759aba2efee670bc", size = 66452 }, + { url = "https://files.pythonhosted.org/packages/41/20/5a9d93f86b1e8fd9d9db33aff39c0e3a8459e0803ec24bd837d8b56d4a1d/cbor2-5.6.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5cff06464b8f4ca6eb9abcba67bda8f8334a058abc01005c8e616728c387ad32", size = 67421 }, + { url = "https://files.pythonhosted.org/packages/0f/1e/2010f6d02dd117df88df64baf3eeca6aa6614cc81bdd6bfabf615889cf1f/cbor2-5.6.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4c7dbcdc59ea7f5a745d3e30ee5e6b6ff5ce7ac244aa3de6786391b10027bb3", size = 260756 }, + { url = "https://files.pythonhosted.org/packages/e1/84/e177d9bef4749d14f31c513b25e341ac84e403e2ffa2bde562eac9e6184b/cbor2-5.6.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34cf5ab0dc310c3d0196caa6ae062dc09f6c242e2544bea01691fe60c0230596", size = 249210 }, + { url = "https://files.pythonhosted.org/packages/38/75/ebfdbb281104b46419fe7cb65979de9927b75acebcb6afa0af291f728cd2/cbor2-5.6.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6797b824b26a30794f2b169c0575301ca9b74ae99064e71d16e6ba0c9057de51", size = 249138 }, + { url = "https://files.pythonhosted.org/packages/b2/1e/12d887fb1a8227a16181eeec5d43057e251204626d73e1c20a77046ac1b1/cbor2-5.6.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:73b9647eed1493097db6aad61e03d8f1252080ee041a1755de18000dd2c05f37", size = 247156 }, + { url = "https://files.pythonhosted.org/packages/6f/76/478c12193de9517ce691bb8a3f7c00eafdd6a1bc3f7f23282ecdd99d02ec/cbor2-5.6.5-cp311-cp311-win_amd64.whl", hash = "sha256:6e14a1bf6269d25e02ef1d4008e0ce8880aa271d7c6b4c329dba48645764f60e", size = 66319 }, + { url = "https://files.pythonhosted.org/packages/57/af/84ced14c541451696825b7b8ccbb7668f688372ad8ee74aaca4311e79672/cbor2-5.6.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e25c2aebc9db99af7190e2261168cdde8ed3d639ca06868e4f477cf3a228a8e9", size = 67553 }, + { url = "https://files.pythonhosted.org/packages/f2/d6/f63a840c68fed4de67d5441947af2dc695152cc488bb0e57312832fb923a/cbor2-5.6.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fde21ac1cf29336a31615a2c469a9cb03cf0add3ae480672d4d38cda467d07fc", size = 67569 }, + { url = "https://files.pythonhosted.org/packages/77/ac/5fb79db6e882ec29680f4a974d35c098020a1b4709cad077667a8c3f4676/cbor2-5.6.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8947c102cac79d049eadbd5e2ffb8189952890df7cbc3ee262bbc2f95b011a9", size = 276610 }, + { url = "https://files.pythonhosted.org/packages/cf/cb/70751377d94112001d46c311b5c40b45f34863dfa78a6bc71b71f40c8c7f/cbor2-5.6.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38886c41bebcd7dca57739439455bce759f1e4c551b511f618b8e9c1295b431b", size = 270004 }, + { url = "https://files.pythonhosted.org/packages/f1/90/08800367e920aef31b93bd7b0cd6fadcb3a3f2243f4ed77a0d1c76f22b99/cbor2-5.6.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae2b49226224e92851c333b91d83292ec62eba53a19c68a79890ce35f1230d70", size = 264913 }, + { url = "https://files.pythonhosted.org/packages/a8/9c/76b11a5ea7548bccb0dfef3e8fb3ede48bfeb39348f0c217519e0c40d33a/cbor2-5.6.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2764804ffb6553283fc4afb10a280715905a4cea4d6dc7c90d3e89c4a93bc8d", size = 266751 }, + { url = "https://files.pythonhosted.org/packages/10/18/3866693a87c90cb12f7942e791d0f03a40ba44887dde7b7fc85319647efe/cbor2-5.6.5-cp312-cp312-win_amd64.whl", hash = "sha256:a3ac50485cf67dfaab170a3e7b527630e93cb0a6af8cdaa403054215dff93adf", size = 66739 }, + { url = "https://files.pythonhosted.org/packages/2b/69/77e93caae71d1baee927c9762e702c464715d88073133052c74ecc9d37d4/cbor2-5.6.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f0d0a9c5aabd48ecb17acf56004a7542a0b8d8212be52f3102b8218284bd881e", size = 67647 }, + { url = "https://files.pythonhosted.org/packages/84/83/cb941d4fd10e4696b2c0f6fb2e3056d9a296e5765b2000a69e29a507f819/cbor2-5.6.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61ceb77e6aa25c11c814d4fe8ec9e3bac0094a1f5bd8a2a8c95694596ea01e08", size = 67657 }, + { url = "https://files.pythonhosted.org/packages/5c/3f/e16a1e29994483c751b714cdf61d2956290b0b30e94690fa714a9f155c5c/cbor2-5.6.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97a7e409b864fecf68b2ace8978eb5df1738799a333ec3ea2b9597bfcdd6d7d2", size = 275863 }, + { url = "https://files.pythonhosted.org/packages/64/04/f64bda3eea649fe6644c59f13d0e1f4666d975ce305cadf13835233b2a26/cbor2-5.6.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f6d69f38f7d788b04c09ef2b06747536624b452b3c8b371ab78ad43b0296fab", size = 269131 }, + { url = "https://files.pythonhosted.org/packages/f4/8d/0d5ad3467f70578b032b3f52eb0f01f0327d5ae6b1f9e7d4d4e01a73aa95/cbor2-5.6.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f91e6d74fa6917df31f8757fdd0e154203b0dd0609ec53eb957016a2b474896a", size = 264728 }, + { url = "https://files.pythonhosted.org/packages/77/cb/9b4f7890325eaa374c21fcccfee61a099ccb9ea0bc0f606acf7495f9568c/cbor2-5.6.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5ce13a27ef8fddf643fc17a753fe34aa72b251d03c23da6a560c005dc171085b", size = 266314 }, + { url = "https://files.pythonhosted.org/packages/a8/cd/793dc041395609f5dd1edfdf0aecde504dc0fd35ed67eb3b2db79fb8ef4d/cbor2-5.6.5-cp313-cp313-win_amd64.whl", hash = "sha256:54c72a3207bb2d4480c2c39dad12d7971ce0853a99e3f9b8d559ce6eac84f66f", size = 66792 }, + { url = "https://files.pythonhosted.org/packages/9b/ef/1c4698cac96d792005ef0611832f38eaee477c275ab4b02cbfc4daba7ad3/cbor2-5.6.5-py3-none-any.whl", hash = "sha256:3038523b8fc7de312bb9cdcbbbd599987e64307c4db357cd2030c472a6c7d468", size = 23752 }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818 }, + { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649 }, + { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045 }, + { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356 }, + { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471 }, + { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317 }, + { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368 }, + { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491 }, + { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695 }, + { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849 }, + { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091 }, + { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445 }, + { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782 }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794 }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846 }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350 }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657 }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260 }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164 }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571 }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952 }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959 }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030 }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015 }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106 }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402 }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936 }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790 }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924 }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626 }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567 }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957 }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408 }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399 }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815 }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537 }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565 }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357 }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776 }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622 }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435 }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653 }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231 }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243 }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442 }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147 }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057 }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454 }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174 }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166 }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064 }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641 }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 }, +] + +[[package]] +name = "ckzg" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/55/df/f6db8e83bd4594c1ea685cd37fb81d5399e55765aae16d1a8a9502598f4e/ckzg-2.1.1.tar.gz", hash = "sha256:d6b306b7ec93a24e4346aa53d07f7f75053bc0afc7398e35fa649e5f9d48fcc4", size = 1120500 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/4b/cd25e857cdf46a752e97c530fe2582fae77c4d16c29fff5a15b7a998e2fd/ckzg-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b9825a1458219e8b4b023012b8ef027ef1f47e903f9541cbca4615f80132730", size = 116377 }, + { url = "https://files.pythonhosted.org/packages/7e/bc/5dfef36589545f797245ecacb54ed2acfa75507f63cfe12182d1277a88f1/ckzg-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e2a40a3ba65cca4b52825d26829e6f7eb464aa27a9e9efb6b8b2ce183442c741", size = 100208 }, + { url = "https://files.pythonhosted.org/packages/b7/52/96f0e3affbed321dc52b9b4ca13e0fb594da572d1f8edc47378fe48d8e9a/ckzg-2.1.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1d753fbe85be7c21602eddc2d40e0915e25fce10329f4f801a0002a4f886cc7", size = 174800 }, + { url = "https://files.pythonhosted.org/packages/dc/21/b1bc07cc8e5ed32817e89b054e2399d38775d92ff2d55e24bf233f537c02/ckzg-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d76b50527f1d12430bf118aff6fa4051e9860eada43f29177258b8d399448ea", size = 160847 }, + { url = "https://files.pythonhosted.org/packages/c9/5a/97b173d4ff9bce798031beb12b340c4f1729eaaddd07f69f368f843db28e/ckzg-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44c8603e43c021d100f355f50189183135d1df3cbbddb8881552d57fbf421dde", size = 169712 }, + { url = "https://files.pythonhosted.org/packages/7b/52/48be78c07f362438e189e2fbea7df8543290c3ee99845442549c8dc5405b/ckzg-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:38707a638c9d715b3c30b29352b969f78d8fc10faed7db5faf517f04359895c0", size = 172942 }, + { url = "https://files.pythonhosted.org/packages/13/42/3cfcd6cbdfb9030b9071d5e413a458f93883e47ad4a7d8d4c1d57608e57d/ckzg-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:52c4d257bdcbe822d20c5cd24c8154ec5aac33c49a8f5a19e716d9107a1c8785", size = 187707 }, + { url = "https://files.pythonhosted.org/packages/eb/54/d43bc3a2de486fb8be29ffedc3ec80f5726765ee4fa78beabe2ab2440f93/ckzg-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1507f7bfb9bcf51d816db5d8d0f0ed53c8289605137820d437b69daea8333e16", size = 182505 }, + { url = "https://files.pythonhosted.org/packages/21/43/5bcd2b7630732b532006572fbb8d64a29f69530c630ae4811167a2a0dc3b/ckzg-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:d02eaaf4f841910133552b3a051dea53bcfe60cd98199fc4cf80b27609d8baa2", size = 98822 }, + { url = "https://files.pythonhosted.org/packages/95/2c/44120b2d9dcb0246d67a1f28b9eaa625c499014d4d42561467e28eedd285/ckzg-2.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:465e2b71cf9dc383f66f1979269420a0da9274a3a9e98b1a4455e84927dfe491", size = 116378 }, + { url = "https://files.pythonhosted.org/packages/23/88/c5b89ba9a730fee5e089be9e0c7048fb6707c1a0e4b6c30fcf725c3eef44/ckzg-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ee2f26f17a64ad0aab833d637b276f28486b82a29e34f32cf54b237b8f8ab72d", size = 100202 }, + { url = "https://files.pythonhosted.org/packages/ee/11/b0a473e80346db52ad9a629bc9fd8f773c718ed78932ea3a70392306ffc3/ckzg-2.1.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99cc2c4e9fb8c62e3e0862c7f4df9142f07ba640da17fded5f6e0fd09f75909f", size = 175595 }, + { url = "https://files.pythonhosted.org/packages/52/fa/17a7e125d07a96dd6dce4db7262231f7583856b2be5d5b7df59e04bfa188/ckzg-2.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773dd016693d74aca1f5d7982db2bad7dde2e147563aeb16a783f7e5f69c01fe", size = 161681 }, + { url = "https://files.pythonhosted.org/packages/57/bd/46d6b90bf53da732f9adab7593d132a0834ed4f2f7659b4c7414d8f78d39/ckzg-2.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af2b2144f87ba218d8db01382a961b3ecbdde5ede4fa0d9428d35f8c8a595ba", size = 170471 }, + { url = "https://files.pythonhosted.org/packages/9d/98/113c7704749d037d75f23240ffc5c46dfe8416de574b946438587835715f/ckzg-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8f55e63d3f7c934a2cb53728ed1d815479e177aca8c84efe991c2920977cff6", size = 173595 }, + { url = "https://files.pythonhosted.org/packages/2f/d5/05fca6dcb5a19327be491157794eafc3d7498daf615c2ff5a5b745852945/ckzg-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ecb42aaa0ffa427ff14a9dde9356ba69e5ae6014650b397af55b31bdae7a9b6e", size = 188417 }, + { url = "https://files.pythonhosted.org/packages/72/36/131ae2dfc82d0fdc98fae8e3bbfe71ff14265bb434b23bd07b585afc6d61/ckzg-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5a01514239f12fb1a7ad9009c20062a4496e13b09541c1a65f97e295da648c70", size = 183286 }, + { url = "https://files.pythonhosted.org/packages/c5/6a/d371b27024422b25228fc11fa57b1ba7756a94cc9fb0c75da292c235fdaa/ckzg-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:6516b9684aae262c85cf7fddd8b585b8139ad20e08ec03994e219663abbb0916", size = 98819 }, + { url = "https://files.pythonhosted.org/packages/93/a1/9c07513dd0ea01e5db727e67bd2660f3b300a4511281cdb8d5e04afa1cfd/ckzg-2.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c60e8903344ce98ce036f0fabacce952abb714cad4607198b2f0961c28b8aa72", size = 116421 }, + { url = "https://files.pythonhosted.org/packages/27/04/b69a0dfbb2722a14c98a52973f276679151ec56a14178cb48e6f2e1697bc/ckzg-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4299149dd72448e5a8d2d1cc6cc7472c92fc9d9f00b1377f5b017c089d9cd92", size = 100216 }, + { url = "https://files.pythonhosted.org/packages/2e/24/9cc850d0b8ead395ad5064de67c7c91adacaf31b6b35292ab53fbd93270b/ckzg-2.1.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:025dd31ffdcc799f3ff842570a2a6683b6c5b01567da0109c0c05d11768729c4", size = 175764 }, + { url = "https://files.pythonhosted.org/packages/c0/c1/eb13ba399082a98b932f10b230ec08e6456051c0ce3886b3f6d8548d11ab/ckzg-2.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b42ab8385c273f40a693657c09d2bba40cb4f4666141e263906ba2e519e80bd", size = 161885 }, + { url = "https://files.pythonhosted.org/packages/57/c7/58baa64199781950c5a8c6139a46e1acff0f057a36e56769817400eb87fb/ckzg-2.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1be3890fc1543f4fcfc0063e4baf5c036eb14bcf736dabdc6171ab017e0f1671", size = 170757 }, + { url = "https://files.pythonhosted.org/packages/65/bd/4b8e1c70972c98829371b7004dc750a45268c5d3442d602e1b62f13ca867/ckzg-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b754210ded172968b201e2d7252573af6bf52d6ad127ddd13d0b9a45a51dae7b", size = 173761 }, + { url = "https://files.pythonhosted.org/packages/1f/32/c3fd1002f97ba3e0c5b1d9ab2c8fb7a6f475fa9b80ed9c4fa55975501a54/ckzg-2.1.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2f8fda87865897a269c4e951e3826c2e814427a6cdfed6731cccfe548f12b36", size = 188666 }, + { url = "https://files.pythonhosted.org/packages/e2/d9/91cf5a8169ee60c9397c975163cbca34432571f94facec5f8c0086bb47d8/ckzg-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:98e70b5923d77c7359432490145e9d1ab0bf873eb5de56ec53f4a551d7eaec79", size = 183652 }, + { url = "https://files.pythonhosted.org/packages/25/d4/8c9f6b852f99926862344b29f0c59681916ccfec2ac60a85952a369e0bca/ckzg-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:42af7bde4ca45469cd93a96c3d15d69d51d40e7f0d30e3a20711ebd639465fcb", size = 98816 }, + { url = "https://files.pythonhosted.org/packages/b7/9a/fa698b12e97452d11dd314e0335aae759725284ef6e1c1665aed56b1cd3e/ckzg-2.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e4edfdaf87825ff43b9885fabfdea408737a714f4ce5467100d9d1d0a03b673", size = 116426 }, + { url = "https://files.pythonhosted.org/packages/a1/a6/8cccd308bd11b49b40eecad6900b5769da117951cac33e880dd25e851ef7/ckzg-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:815fd2a87d6d6c57d669fda30c150bc9bf387d47e67d84535aa42b909fdc28ea", size = 100219 }, + { url = "https://files.pythonhosted.org/packages/30/0e/63573d816c1292b9a4d70eb6a7366b3593d29a977794039e926805a76ca0/ckzg-2.1.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c32466e809b1ab3ff01d3b0bb0b9912f61dcf72957885615595f75e3f7cc10e5", size = 175725 }, + { url = "https://files.pythonhosted.org/packages/86/f6/a279609516695ad3fb8b201098c669ba3b2844cbf4fa0d83a0f02b9bb29b/ckzg-2.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f11b73ccf37b12993f39a7dbace159c6d580aacacde6ee17282848476550ddbc", size = 161835 }, + { url = "https://files.pythonhosted.org/packages/39/e4/8cf7aef7dc05a777cb221e94046f947c6fe5317159a8dae2cd7090d52ef2/ckzg-2.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3b9433a1f2604bd9ac1646d3c83ad84a850d454d3ac589fe8e70c94b38a6b0", size = 170759 }, + { url = "https://files.pythonhosted.org/packages/0b/17/b34e3c08eb36bc67e338b114f289b2595e581b8bdc09a8f12299a1db5d2f/ckzg-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b7d7e1b5ea06234558cd95c483666fd785a629b720a7f1622b3cbffebdc62033", size = 173787 }, + { url = "https://files.pythonhosted.org/packages/2e/f0/aff87c3ed80713453cb6c84fe6fbb7582d86a7a5e4460fda2a497d47f489/ckzg-2.1.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9f5556e6675866040cc4335907be6c537051e7f668da289fa660fdd8a30c9ddb", size = 188722 }, + { url = "https://files.pythonhosted.org/packages/44/d9/1f08bfb8fd1cbb8c7513e7ad3fb76bbb5c3fb446238c1eba582276e4d905/ckzg-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55b2ba30c5c9daac0c55f1aac851f1b7bf1f7aa0028c2db4440e963dd5b866d6", size = 183686 }, + { url = "https://files.pythonhosted.org/packages/a3/ff/434f6d2893cbdfad00c20d17e9a52d426ca042f5e980d5c3db96bc6b6e15/ckzg-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:10d201601fc8f28c0e8cec3406676797024dd374c367bbeec5a7a9eac9147237", size = 98817 }, + { url = "https://files.pythonhosted.org/packages/30/b3/a0c7d7ba6e669cf04605dc0329173db62fc1fe3c488761755cc01e5e1b4d/ckzg-2.1.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:375918e25eafb9bafe5215ab91698504cba3fe51b4fe92f5896af6c5663f50c6", size = 113191 }, + { url = "https://files.pythonhosted.org/packages/f2/b9/a6cf403b8528d18d7d9154e28381a397bf466c86aa8e0b3327cffdde5749/ckzg-2.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:38b3b7802c76d4ad015db2b7a79a49c193babae50ee5f77e9ac2865c9e9ddb09", size = 96207 }, + { url = "https://files.pythonhosted.org/packages/63/6b/5ddd713d97886becb8450e3e13db891199125f722366d30d087ad5438390/ckzg-2.1.1-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:438a5009fd254ace0bc1ad974d524547f1a41e6aa5e778c5cd41f4ee3106bcd6", size = 126160 }, + { url = "https://files.pythonhosted.org/packages/c7/dd/e05aecc01e62108a7579f8df5e5d38536841f50e12172f8a84677edac0fa/ckzg-2.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ce11cc163a2e0dab3af7455aca7053f9d5bb8d157f231acc7665fd230565d48", size = 102811 }, + { url = "https://files.pythonhosted.org/packages/c6/81/6cdadd8626ac11290af3f58ae5dcffe38bd2c8f8c798dacee7475e244aac/ckzg-2.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b53964c07f6a076e97eaa1ef35045e935d7040aff14f80bae7e9105717702d05", size = 111328 }, + { url = "https://files.pythonhosted.org/packages/36/b7/b129ff6955cd264c6ab3dbd52dd1b2759d1b121c09c03f9991e4c722c72f/ckzg-2.1.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cf085f15ae52ab2599c9b5a3d5842794bcf5613b7f58661fbfb0c5d9eac988b9", size = 98846 }, + { url = "https://files.pythonhosted.org/packages/7f/ba/7d9c1f9cec7e0e382653c72165896194a05743e589b1dae2aa80236aa87f/ckzg-2.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4b0c850bd6cad22ac79b2a2ab884e0e7cd2b54a67d643cd616c145ebdb535a11", size = 113188 }, + { url = "https://files.pythonhosted.org/packages/2f/92/9728f5ccc1c5e87c6c5ae7941250a447b61fd5a63aadbc15249e29c21bcf/ckzg-2.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:26951f36bb60c9150bbd38110f5e1625596f9779dad54d1d492d8ec38bc84e3a", size = 96208 }, + { url = "https://files.pythonhosted.org/packages/39/63/5e27d587bd224fee70cb66b022e7c4ef95d0e091e08ee76c25ec12094b0d/ckzg-2.1.1-pp311-pypy311_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbe12445e49c4bee67746b7b958e90a973b0de116d0390749b0df351d94e9a8c", size = 126158 }, + { url = "https://files.pythonhosted.org/packages/43/98/e0a45946575a7b823d8ee0b47afb104b6017e54e1208f07da2529bc01900/ckzg-2.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71c5d4f66f09de4a99271acac74d2acb3559a77de77a366b34a91e99e8822667", size = 102812 }, + { url = "https://files.pythonhosted.org/packages/cb/50/718ca7b03e4b89b18cdf99cc3038050105b0acbf9b612c23cd513093c6de/ckzg-2.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42673c1d007372a4e8b48f6ef8f0ce31a9688a463317a98539757d1e2fb1ecc7", size = 111327 }, + { url = "https://files.pythonhosted.org/packages/29/c5/80e5a0c6967d02d801150104320484a258e5a49bd191e198643e74039320/ckzg-2.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:57a7dc41ec6b69c1d9117eb61cf001295e6b4f67a736020442e71fb4367fb1a5", size = 98847 }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215 }, +] + +[[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.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/76/17780846fc7aade1e66712e1e27dd28faa0a5d987a1f433610974959eaa8/coverage-7.10.2.tar.gz", hash = "sha256:5d6e6d84e6dd31a8ded64759626627247d676a23c1b892e1326f7c55c8d61055", size = 820754 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/5f/5ce748ab3f142593698aff5f8a0cf020775aa4e24b9d8748b5a56b64d3f8/coverage-7.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:79f0283ab5e6499fd5fe382ca3d62afa40fb50ff227676a3125d18af70eabf65", size = 215003 }, + { url = "https://files.pythonhosted.org/packages/f4/ed/507088561217b000109552139802fa99c33c16ad19999c687b601b3790d0/coverage-7.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4545e906f595ee8ab8e03e21be20d899bfc06647925bc5b224ad7e8c40e08b8", size = 215391 }, + { url = "https://files.pythonhosted.org/packages/79/1b/0f496259fe137c4c5e1e8eaff496fb95af88b71700f5e57725a4ddbe742b/coverage-7.10.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ae385e1d58fbc6a9b1c315e5510ac52281e271478b45f92ca9b5ad42cf39643f", size = 242367 }, + { url = "https://files.pythonhosted.org/packages/b9/8e/5a8835fb0122a2e2a108bf3527931693c4625fdc4d953950a480b9625852/coverage-7.10.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f0cbe5f7dd19f3a32bac2251b95d51c3b89621ac88a2648096ce40f9a5aa1e7", size = 243627 }, + { url = "https://files.pythonhosted.org/packages/c3/96/6a528429c2e0e8d85261764d0cd42e51a429510509bcc14676ee5d1bb212/coverage-7.10.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd17f427f041f6b116dc90b4049c6f3e1230524407d00daa2d8c7915037b5947", size = 245485 }, + { url = "https://files.pythonhosted.org/packages/bf/82/1fba935c4d02c33275aca319deabf1f22c0f95f2c0000bf7c5f276d6f7b4/coverage-7.10.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7f10ca4cde7b466405cce0a0e9971a13eb22e57a5ecc8b5f93a81090cc9c7eb9", size = 243429 }, + { url = "https://files.pythonhosted.org/packages/fc/a8/c8dc0a57a729fc93be33ab78f187a8f52d455fa8f79bfb379fe23b45868d/coverage-7.10.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3b990df23dd51dccce26d18fb09fd85a77ebe46368f387b0ffba7a74e470b31b", size = 242104 }, + { url = "https://files.pythonhosted.org/packages/b9/6f/0b7da1682e2557caeed299a00897b42afde99a241a01eba0197eb982b90f/coverage-7.10.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc3902584d25c7eef57fb38f440aa849a26a3a9f761a029a72b69acfca4e31f8", size = 242397 }, + { url = "https://files.pythonhosted.org/packages/2d/e4/54dc833dadccd519c04a28852f39a37e522bad35d70cfe038817cdb8f168/coverage-7.10.2-cp310-cp310-win32.whl", hash = "sha256:9dd37e9ac00d5eb72f38ed93e3cdf2280b1dbda3bb9b48c6941805f265ad8d87", size = 217502 }, + { url = "https://files.pythonhosted.org/packages/c3/e7/2f78159c4c127549172f427dff15b02176329327bf6a6a1fcf1f603b5456/coverage-7.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:99d16f15cb5baf0729354c5bd3080ae53847a4072b9ba1e10957522fb290417f", size = 218388 }, + { url = "https://files.pythonhosted.org/packages/6e/53/0125a6fc0af4f2687b4e08b0fb332cd0d5e60f3ca849e7456f995d022656/coverage-7.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c3b210d79925a476dfc8d74c7d53224888421edebf3a611f3adae923e212b27", size = 215119 }, + { url = "https://files.pythonhosted.org/packages/0e/2e/960d9871de9152dbc9ff950913c6a6e9cf2eb4cc80d5bc8f93029f9f2f9f/coverage-7.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf67d1787cd317c3f8b2e4c6ed1ae93497be7e30605a0d32237ac37a37a8a322", size = 215511 }, + { url = "https://files.pythonhosted.org/packages/3f/34/68509e44995b9cad806d81b76c22bc5181f3535bca7cd9c15791bfd8951e/coverage-7.10.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:069b779d03d458602bc0e27189876e7d8bdf6b24ac0f12900de22dd2154e6ad7", size = 245513 }, + { url = "https://files.pythonhosted.org/packages/ef/d4/9b12f357413248ce40804b0f58030b55a25b28a5c02db95fb0aa50c5d62c/coverage-7.10.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4c2de4cb80b9990e71c62c2d3e9f3ec71b804b1f9ca4784ec7e74127e0f42468", size = 247350 }, + { url = "https://files.pythonhosted.org/packages/b6/40/257945eda1f72098e4a3c350b1d68fdc5d7d032684a0aeb6c2391153ecf4/coverage-7.10.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75bf7ab2374a7eb107602f1e07310cda164016cd60968abf817b7a0b5703e288", size = 249516 }, + { url = "https://files.pythonhosted.org/packages/ff/55/8987f852ece378cecbf39a367f3f7ec53351e39a9151b130af3a3045b83f/coverage-7.10.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3f37516458ec1550815134937f73d6d15b434059cd10f64678a2068f65c62406", size = 247241 }, + { url = "https://files.pythonhosted.org/packages/df/ae/da397de7a42a18cea6062ed9c3b72c50b39e0b9e7b2893d7172d3333a9a1/coverage-7.10.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:de3c6271c482c250d3303fb5c6bdb8ca025fff20a67245e1425df04dc990ece9", size = 245274 }, + { url = "https://files.pythonhosted.org/packages/4e/64/7baa895eb55ec0e1ec35b988687ecd5d4475ababb0d7ae5ca3874dd90ee7/coverage-7.10.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:98a838101321ac3089c9bb1d4bfa967e8afed58021fda72d7880dc1997f20ae1", size = 245882 }, + { url = "https://files.pythonhosted.org/packages/24/6c/1fd76a0bd09ae75220ae9775a8290416d726f0e5ba26ea72346747161240/coverage-7.10.2-cp311-cp311-win32.whl", hash = "sha256:f2a79145a531a0e42df32d37be5af069b4a914845b6f686590739b786f2f7bce", size = 217541 }, + { url = "https://files.pythonhosted.org/packages/5f/2d/8c18fb7a6e74c79fd4661e82535bc8c68aee12f46c204eabf910b097ccc9/coverage-7.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:e4f5f1320f8ee0d7cfa421ceb257bef9d39fd614dd3ddcfcacd284d4824ed2c2", size = 218426 }, + { url = "https://files.pythonhosted.org/packages/da/40/425bb35e4ff7c7af177edf5dffd4154bc2a677b27696afe6526d75c77fec/coverage-7.10.2-cp311-cp311-win_arm64.whl", hash = "sha256:d8f2d83118f25328552c728b8e91babf93217db259ca5c2cd4dd4220b8926293", size = 217116 }, + { url = "https://files.pythonhosted.org/packages/4e/1e/2c752bdbbf6f1199c59b1a10557fbb6fb3dc96b3c0077b30bd41a5922c1f/coverage-7.10.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:890ad3a26da9ec7bf69255b9371800e2a8da9bc223ae5d86daeb940b42247c83", size = 215311 }, + { url = "https://files.pythonhosted.org/packages/68/6a/84277d73a2cafb96e24be81b7169372ba7ff28768ebbf98e55c85a491b0f/coverage-7.10.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38fd1ccfca7838c031d7a7874d4353e2f1b98eb5d2a80a2fe5732d542ae25e9c", size = 215550 }, + { url = "https://files.pythonhosted.org/packages/b5/e7/5358b73b46ac76f56cc2de921eeabd44fabd0b7ff82ea4f6b8c159c4d5dc/coverage-7.10.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:76c1ffaaf4f6f0f6e8e9ca06f24bb6454a7a5d4ced97a1bc466f0d6baf4bd518", size = 246564 }, + { url = "https://files.pythonhosted.org/packages/7c/0e/b0c901dd411cb7fc0cfcb28ef0dc6f3049030f616bfe9fc4143aecd95901/coverage-7.10.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:86da8a3a84b79ead5c7d0e960c34f580bc3b231bb546627773a3f53c532c2f21", size = 248993 }, + { url = "https://files.pythonhosted.org/packages/0e/4e/a876db272072a9e0df93f311e187ccdd5f39a190c6d1c1f0b6e255a0d08e/coverage-7.10.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99cef9731c8a39801830a604cc53c93c9e57ea8b44953d26589499eded9576e0", size = 250454 }, + { url = "https://files.pythonhosted.org/packages/64/d6/1222dc69f8dd1be208d55708a9f4a450ad582bf4fa05320617fea1eaa6d8/coverage-7.10.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ea58b112f2966a8b91eb13f5d3b1f8bb43c180d624cd3283fb33b1cedcc2dd75", size = 248365 }, + { url = "https://files.pythonhosted.org/packages/62/e3/40fd71151064fc315c922dd9a35e15b30616f00146db1d6a0b590553a75a/coverage-7.10.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:20f405188d28da9522b7232e51154e1b884fc18d0b3a10f382d54784715bbe01", size = 246562 }, + { url = "https://files.pythonhosted.org/packages/fc/14/8aa93ddcd6623ddaef5d8966268ac9545b145bce4fe7b1738fd1c3f0d957/coverage-7.10.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:64586ce42bbe0da4d9f76f97235c545d1abb9b25985a8791857690f96e23dc3b", size = 247772 }, + { url = "https://files.pythonhosted.org/packages/07/4e/dcb1c01490623c61e2f2ea85cb185fa6a524265bb70eeb897d3c193efeb9/coverage-7.10.2-cp312-cp312-win32.whl", hash = "sha256:bc2e69b795d97ee6d126e7e22e78a509438b46be6ff44f4dccbb5230f550d340", size = 217710 }, + { url = "https://files.pythonhosted.org/packages/79/16/e8aab4162b5f80ad2e5e1f54b1826e2053aa2f4db508b864af647f00c239/coverage-7.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:adda2268b8cf0d11f160fad3743b4dfe9813cd6ecf02c1d6397eceaa5b45b388", size = 218499 }, + { url = "https://files.pythonhosted.org/packages/06/7f/c112ec766e8f1131ce8ce26254be028772757b2d1e63e4f6a4b0ad9a526c/coverage-7.10.2-cp312-cp312-win_arm64.whl", hash = "sha256:164429decd0d6b39a0582eaa30c67bf482612c0330572343042d0ed9e7f15c20", size = 217154 }, + { url = "https://files.pythonhosted.org/packages/8d/04/9b7a741557f93c0ed791b854d27aa8d9fe0b0ce7bb7c52ca1b0f2619cb74/coverage-7.10.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aca7b5645afa688de6d4f8e89d30c577f62956fefb1bad021490d63173874186", size = 215337 }, + { url = "https://files.pythonhosted.org/packages/02/a4/8d1088cd644750c94bc305d3cf56082b4cdf7fb854a25abb23359e74892f/coverage-7.10.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:96e5921342574a14303dfdb73de0019e1ac041c863743c8fe1aa6c2b4a257226", size = 215596 }, + { url = "https://files.pythonhosted.org/packages/01/2f/643a8d73343f70e162d8177a3972b76e306b96239026bc0c12cfde4f7c7a/coverage-7.10.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11333094c1bff621aa811b67ed794865cbcaa99984dedea4bd9cf780ad64ecba", size = 246145 }, + { url = "https://files.pythonhosted.org/packages/1f/4a/722098d1848db4072cda71b69ede1e55730d9063bf868375264d0d302bc9/coverage-7.10.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6eb586fa7d2aee8d65d5ae1dd71414020b2f447435c57ee8de8abea0a77d5074", size = 248492 }, + { url = "https://files.pythonhosted.org/packages/3f/b0/8a6d7f326f6e3e6ed398cde27f9055e860a1e858317001835c521673fb60/coverage-7.10.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d358f259d8019d4ef25d8c5b78aca4c7af25e28bd4231312911c22a0e824a57", size = 249927 }, + { url = "https://files.pythonhosted.org/packages/bb/21/1aaadd3197b54d1e61794475379ecd0f68d8fc5c2ebd352964dc6f698a3d/coverage-7.10.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5250bda76e30382e0a2dcd68d961afcab92c3a7613606e6269855c6979a1b0bb", size = 248138 }, + { url = "https://files.pythonhosted.org/packages/48/65/be75bafb2bdd22fd8bf9bf63cd5873b91bb26ec0d68f02d4b8b09c02decb/coverage-7.10.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a91e027d66eff214d88d9afbe528e21c9ef1ecdf4956c46e366c50f3094696d0", size = 246111 }, + { url = "https://files.pythonhosted.org/packages/5e/30/a4f0c5e249c3cc60e6c6f30d8368e372f2d380eda40e0434c192ac27ccf5/coverage-7.10.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:228946da741558904e2c03ce870ba5efd9cd6e48cbc004d9a27abee08100a15a", size = 247493 }, + { url = "https://files.pythonhosted.org/packages/85/99/f09b9493e44a75cf99ca834394c12f8cb70da6c1711ee296534f97b52729/coverage-7.10.2-cp313-cp313-win32.whl", hash = "sha256:95e23987b52d02e7c413bf2d6dc6288bd5721beb518052109a13bfdc62c8033b", size = 217756 }, + { url = "https://files.pythonhosted.org/packages/2d/bb/cbcb09103be330c7d26ff0ab05c4a8861dd2e254656fdbd3eb7600af4336/coverage-7.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:f35481d42c6d146d48ec92d4e239c23f97b53a3f1fbd2302e7c64336f28641fe", size = 218526 }, + { url = "https://files.pythonhosted.org/packages/37/8f/8bfb4e0bca52c00ab680767c0dd8cfd928a2a72d69897d9b2d5d8b5f63f5/coverage-7.10.2-cp313-cp313-win_arm64.whl", hash = "sha256:65b451949cb789c346f9f9002441fc934d8ccedcc9ec09daabc2139ad13853f7", size = 217176 }, + { url = "https://files.pythonhosted.org/packages/1e/25/d458ba0bf16a8204a88d74dbb7ec5520f29937ffcbbc12371f931c11efd2/coverage-7.10.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e8415918856a3e7d57a4e0ad94651b761317de459eb74d34cc1bb51aad80f07e", size = 216058 }, + { url = "https://files.pythonhosted.org/packages/0b/1c/af4dfd2d7244dc7610fed6d59d57a23ea165681cd764445dc58d71ed01a6/coverage-7.10.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f287a25a8ca53901c613498e4a40885b19361a2fe8fbfdbb7f8ef2cad2a23f03", size = 216273 }, + { url = "https://files.pythonhosted.org/packages/8e/67/ec5095d4035c6e16368226fa9cb15f77f891194c7e3725aeefd08e7a3e5a/coverage-7.10.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:75cc1a3f8c88c69bf16a871dab1fe5a7303fdb1e9f285f204b60f1ee539b8fc0", size = 257513 }, + { url = "https://files.pythonhosted.org/packages/1c/47/be5550b57a3a8ba797de4236b0fd31031f88397b2afc84ab3c2d4cf265f6/coverage-7.10.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca07fa78cc9d26bc8c4740de1abd3489cf9c47cc06d9a8ab3d552ff5101af4c0", size = 259377 }, + { url = "https://files.pythonhosted.org/packages/37/50/b12a4da1382e672305c2d17cd3029dc16b8a0470de2191dbf26b91431378/coverage-7.10.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2e117e64c26300032755d4520cd769f2623cde1a1d1c3515b05a3b8add0ade1", size = 261516 }, + { url = "https://files.pythonhosted.org/packages/db/41/4d3296dbd33dd8da178171540ca3391af7c0184c0870fd4d4574ac290290/coverage-7.10.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:daaf98009977f577b71f8800208f4d40d4dcf5c2db53d4d822787cdc198d76e1", size = 259110 }, + { url = "https://files.pythonhosted.org/packages/ea/f1/b409959ecbc0cec0e61e65683b22bacaa4a3b11512f834e16dd8ffbc37db/coverage-7.10.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ea8d8fe546c528535c761ba424410bbeb36ba8a0f24be653e94b70c93fd8a8ca", size = 257248 }, + { url = "https://files.pythonhosted.org/packages/48/ab/7076dc1c240412e9267d36ec93e9e299d7659f6a5c1e958f87e998b0fb6d/coverage-7.10.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fe024d40ac31eb8d5aae70215b41dafa264676caa4404ae155f77d2fa95c37bb", size = 258063 }, + { url = "https://files.pythonhosted.org/packages/1e/77/f6b51a0288f8f5f7dcc7c89abdd22cf514f3bc5151284f5cd628917f8e10/coverage-7.10.2-cp313-cp313t-win32.whl", hash = "sha256:8f34b09f68bdadec122ffad312154eda965ade433559cc1eadd96cca3de5c824", size = 218433 }, + { url = "https://files.pythonhosted.org/packages/7b/6d/547a86493e25270ce8481543e77f3a0aa3aa872c1374246b7b76273d66eb/coverage-7.10.2-cp313-cp313t-win_amd64.whl", hash = "sha256:71d40b3ac0f26fa9ffa6ee16219a714fed5c6ec197cdcd2018904ab5e75bcfa3", size = 219523 }, + { url = "https://files.pythonhosted.org/packages/ff/d5/3c711e38eaf9ab587edc9bed232c0298aed84e751a9f54aaa556ceaf7da6/coverage-7.10.2-cp313-cp313t-win_arm64.whl", hash = "sha256:abb57fdd38bf6f7dcc66b38dafb7af7c5fdc31ac6029ce373a6f7f5331d6f60f", size = 217739 }, + { url = "https://files.pythonhosted.org/packages/71/53/83bafa669bb9d06d4c8c6a055d8d05677216f9480c4698fb183ba7ec5e47/coverage-7.10.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a3e853cc04987c85ec410905667eed4bf08b1d84d80dfab2684bb250ac8da4f6", size = 215328 }, + { url = "https://files.pythonhosted.org/packages/1d/6c/30827a9c5a48a813e865fbaf91e2db25cce990bd223a022650ef2293fe11/coverage-7.10.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0100b19f230df72c90fdb36db59d3f39232391e8d89616a7de30f677da4f532b", size = 215608 }, + { url = "https://files.pythonhosted.org/packages/bb/a0/c92d85948056ddc397b72a3d79d36d9579c53cb25393ed3c40db7d33b193/coverage-7.10.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9c1cd71483ea78331bdfadb8dcec4f4edfb73c7002c1206d8e0af6797853f5be", size = 246111 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/d695cf86b2559aadd072c91720a7844be4fb82cb4a3b642a2c6ce075692d/coverage-7.10.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9f75dbf4899e29a37d74f48342f29279391668ef625fdac6d2f67363518056a1", size = 248419 }, + { url = "https://files.pythonhosted.org/packages/ce/0a/03206aec4a05986e039418c038470d874045f6e00426b0c3879adc1f9251/coverage-7.10.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7df481e7508de1c38b9b8043da48d94931aefa3e32b47dd20277e4978ed5b95", size = 250038 }, + { url = "https://files.pythonhosted.org/packages/ab/9b/b3bd6bd52118c12bc4cf319f5baba65009c9beea84e665b6b9f03fa3f180/coverage-7.10.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:835f39e618099325e7612b3406f57af30ab0a0af350490eff6421e2e5f608e46", size = 248066 }, + { url = "https://files.pythonhosted.org/packages/80/cc/bfa92e261d3e055c851a073e87ba6a3bff12a1f7134233e48a8f7d855875/coverage-7.10.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:12e52b5aa00aa720097d6947d2eb9e404e7c1101ad775f9661ba165ed0a28303", size = 245909 }, + { url = "https://files.pythonhosted.org/packages/12/80/c8df15db4847710c72084164f615ae900af1ec380dce7f74a5678ccdf5e1/coverage-7.10.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:718044729bf1fe3e9eb9f31b52e44ddae07e434ec050c8c628bf5adc56fe4bdd", size = 247329 }, + { url = "https://files.pythonhosted.org/packages/04/6f/cb66e1f7124d5dd9ced69f889f02931419cb448125e44a89a13f4e036124/coverage-7.10.2-cp314-cp314-win32.whl", hash = "sha256:f256173b48cc68486299d510a3e729a96e62c889703807482dbf56946befb5c8", size = 218007 }, + { url = "https://files.pythonhosted.org/packages/8c/e1/3d4be307278ce32c1b9d95cc02ee60d54ddab784036101d053ec9e4fe7f5/coverage-7.10.2-cp314-cp314-win_amd64.whl", hash = "sha256:2e980e4179f33d9b65ac4acb86c9c0dde904098853f27f289766657ed16e07b3", size = 218802 }, + { url = "https://files.pythonhosted.org/packages/ec/66/1e43bbeb66c55a5a5efec70f1c153cf90cfc7f1662ab4ebe2d844de9122c/coverage-7.10.2-cp314-cp314-win_arm64.whl", hash = "sha256:14fb5b6641ab5b3c4161572579f0f2ea8834f9d3af2f7dd8fbaecd58ef9175cc", size = 217397 }, + { url = "https://files.pythonhosted.org/packages/81/01/ae29c129217f6110dc694a217475b8aecbb1b075d8073401f868c825fa99/coverage-7.10.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e96649ac34a3d0e6491e82a2af71098e43be2874b619547c3282fc11d3840a4b", size = 216068 }, + { url = "https://files.pythonhosted.org/packages/a2/50/6e9221d4139f357258f36dfa1d8cac4ec56d9d5acf5fdcc909bb016954d7/coverage-7.10.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1a2e934e9da26341d342d30bfe91422bbfdb3f1f069ec87f19b2909d10d8dcc4", size = 216285 }, + { url = "https://files.pythonhosted.org/packages/eb/ec/89d1d0c0ece0d296b4588e0ef4df185200456d42a47f1141335f482c2fc5/coverage-7.10.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:651015dcd5fd9b5a51ca79ece60d353cacc5beaf304db750407b29c89f72fe2b", size = 257603 }, + { url = "https://files.pythonhosted.org/packages/82/06/c830af66734671c778fc49d35b58339e8f0687fbd2ae285c3f96c94da092/coverage-7.10.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81bf6a32212f9f66da03d63ecb9cd9bd48e662050a937db7199dbf47d19831de", size = 259568 }, + { url = "https://files.pythonhosted.org/packages/60/57/f280dd6f1c556ecc744fbf39e835c33d3ae987d040d64d61c6f821e87829/coverage-7.10.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d800705f6951f75a905ea6feb03fff8f3ea3468b81e7563373ddc29aa3e5d1ca", size = 261691 }, + { url = "https://files.pythonhosted.org/packages/54/2b/c63a0acbd19d99ec32326164c23df3a4e18984fb86e902afdd66ff7b3d83/coverage-7.10.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:248b5394718e10d067354448dc406d651709c6765669679311170da18e0e9af8", size = 259166 }, + { url = "https://files.pythonhosted.org/packages/fd/c5/cd2997dcfcbf0683634da9df52d3967bc1f1741c1475dd0e4722012ba9ef/coverage-7.10.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5c61675a922b569137cf943770d7ad3edd0202d992ce53ac328c5ff68213ccf4", size = 257241 }, + { url = "https://files.pythonhosted.org/packages/16/26/c9e30f82fdad8d47aee90af4978b18c88fa74369ae0f0ba0dbf08cee3a80/coverage-7.10.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:52d708b5fd65589461381fa442d9905f5903d76c086c6a4108e8e9efdca7a7ed", size = 258139 }, + { url = "https://files.pythonhosted.org/packages/c9/99/bdb7bd00bebcd3dedfb895fa9af8e46b91422993e4a37ac634a5f1113790/coverage-7.10.2-cp314-cp314t-win32.whl", hash = "sha256:916369b3b914186b2c5e5ad2f7264b02cff5df96cdd7cdad65dccd39aa5fd9f0", size = 218809 }, + { url = "https://files.pythonhosted.org/packages/eb/5e/56a7852e38a04d1520dda4dfbfbf74a3d6dec932c20526968f7444763567/coverage-7.10.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5b9d538e8e04916a5df63052d698b30c74eb0174f2ca9cd942c981f274a18eaf", size = 219926 }, + { url = "https://files.pythonhosted.org/packages/e0/12/7fbe6b9c52bb9d627e9556f9f2edfdbe88b315e084cdecc9afead0c3b36a/coverage-7.10.2-cp314-cp314t-win_arm64.whl", hash = "sha256:04c74f9ef1f925456a9fd23a7eef1103126186d0500ef9a0acb0bd2514bdc7cc", size = 217925 }, + { url = "https://files.pythonhosted.org/packages/18/d8/9b768ac73a8ac2d10c080af23937212434a958c8d2a1c84e89b450237942/coverage-7.10.2-py3-none-any.whl", hash = "sha256:95db3750dd2e6e93d99fa2498f3a1580581e49c494bddccc6f85c5c21604921f", size = 206973 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "curve-stablecoin" +version = "0.2.0" +source = { virtual = "." } +dependencies = [ + { name = "hypothesis" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-forked" }, + { name = "pytest-xdist" }, + { name = "snekmate" }, + { name = "titanoboa" }, + { name = "vyper" }, +] + +[package.metadata] +requires-dist = [ + { name = "hypothesis", specifier = ">=6.99.0" }, + { name = "pytest", specifier = ">=8.0.0" }, + { name = "pytest-cov", specifier = ">=4.0.0" }, + { name = "pytest-forked", specifier = ">=1.6.0" }, + { name = "pytest-xdist", specifier = ">=3.5" }, + { name = "snekmate", specifier = ">=0.1.1" }, + { name = "titanoboa", specifier = ">=0.2.7" }, + { name = "vyper", specifier = "==0.4.3" }, +] + +[[package]] +name = "cytoolz" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/f9/3243eed3a6545c2a33a21f74f655e3fcb5d2192613cd3db81a93369eb339/cytoolz-1.0.1.tar.gz", hash = "sha256:89cc3161b89e1bb3ed7636f74ed2e55984fd35516904fc878cae216e42b2c7d6", size = 626652 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d9/f13d66c16cff1fa1cb6c234698029877c456f35f577ef274aba3b86e7c51/cytoolz-1.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cec9af61f71fc3853eb5dca3d42eb07d1f48a4599fa502cbe92adde85f74b042", size = 403515 }, + { url = "https://files.pythonhosted.org/packages/4b/2d/4cdf848a69300c7d44984f2ebbebb3b8576e5449c8dea157298f3bdc4da3/cytoolz-1.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:140bbd649dbda01e91add7642149a5987a7c3ccc251f2263de894b89f50b6608", size = 383936 }, + { url = "https://files.pythonhosted.org/packages/72/a4/ccfdd3f0ed9cc818f734b424261f6018fc61e3ec833bf85225a9aca0d994/cytoolz-1.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e90124bdc42ff58b88cdea1d24a6bc5f776414a314cc4d94f25c88badb3a16d1", size = 1934569 }, + { url = "https://files.pythonhosted.org/packages/50/fc/38d5344fa595683ad10dc819cfc1d8b9d2b3391ccf3e8cb7bab4899a01f5/cytoolz-1.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e74801b751e28f7c5cc3ad264c123954a051f546f2fdfe089f5aa7a12ccfa6da", size = 2015129 }, + { url = "https://files.pythonhosted.org/packages/28/29/75261748dc54a20a927f33641f4e9aac674cfc6d3fbd4f332e10d0b37639/cytoolz-1.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:582dad4545ddfb5127494ef23f3fa4855f1673a35d50c66f7638e9fb49805089", size = 2000506 }, + { url = "https://files.pythonhosted.org/packages/00/ae/e4ead004cc2698281d153c4a5388638d67cdb5544d6d6cc1e5b3db2bd2a3/cytoolz-1.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd7bd0618e16efe03bd12f19c2a26a27e6e6b75d7105adb7be1cd2a53fa755d8", size = 1957537 }, + { url = "https://files.pythonhosted.org/packages/4a/ff/4f3aa07f4f47701f7f63df60ce0a5669fa09c256c3d4a33503a9414ea5cc/cytoolz-1.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d74cca6acf1c4af58b2e4a89cc565ed61c5e201de2e434748c93e5a0f5c541a5", size = 1863331 }, + { url = "https://files.pythonhosted.org/packages/a2/29/654f57f2a9b8e9765a4ab876765f64f94530b61fc6471a07feea42ece6d4/cytoolz-1.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:823a3763828d8d457f542b2a45d75d6b4ced5e470b5c7cf2ed66a02f508ed442", size = 1849938 }, + { url = "https://files.pythonhosted.org/packages/bc/7b/11f457db6b291060a98315ab2c7198077d8bddeeebe5f7126d9dad98cc54/cytoolz-1.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:51633a14e6844c61db1d68c1ffd077cf949f5c99c60ed5f1e265b9e2966f1b52", size = 1852345 }, + { url = "https://files.pythonhosted.org/packages/6b/92/0dccc96ce0323be236d404f5084479b79b747fa0e74e43a270e95868b5f9/cytoolz-1.0.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f3ec9b01c45348f1d0d712507d54c2bfd69c62fbd7c9ef555c9d8298693c2432", size = 1989877 }, + { url = "https://files.pythonhosted.org/packages/a3/c8/1c5203a81200bae51aa8f7b5fad613f695bf1afa03f16251ca23ecb2ef9f/cytoolz-1.0.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1855022b712a9c7a5bce354517ab4727a38095f81e2d23d3eabaf1daeb6a3b3c", size = 1994492 }, + { url = "https://files.pythonhosted.org/packages/e2/8a/04bc193c4d7ced8ef6bb62cdcd0bf40b5e5eb26586ed2cfb4433ec7dfd0a/cytoolz-1.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9930f7288c4866a1dc1cc87174f0c6ff4cad1671eb1f6306808aa6c445857d78", size = 1896077 }, + { url = "https://files.pythonhosted.org/packages/21/a5/bee63a58f51d2c74856db66e6119a014464ff8cb1c9387fa4bd2d94e49b0/cytoolz-1.0.1-cp310-cp310-win32.whl", hash = "sha256:a9baad795d72fadc3445ccd0f122abfdbdf94269157e6d6d4835636dad318804", size = 322135 }, + { url = "https://files.pythonhosted.org/packages/e8/16/7abfb1685e8b7f2838264551ee33651748994813f566ac4c3d737dfe90e5/cytoolz-1.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:ad95b386a84e18e1f6136f6d343d2509d4c3aae9f5a536f3dc96808fcc56a8cf", size = 363599 }, + { url = "https://files.pythonhosted.org/packages/dc/ea/8131ae39119820b8867cddc23716fa9f681f2b3bbce6f693e68dfb36b55b/cytoolz-1.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2d958d4f04d9d7018e5c1850790d9d8e68b31c9a2deebca74b903706fdddd2b6", size = 406162 }, + { url = "https://files.pythonhosted.org/packages/26/18/3d9bd4c146f6ea6e51300c242b20cb416966b21d481dac230e1304f1e54b/cytoolz-1.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0f445b8b731fc0ecb1865b8e68a070084eb95d735d04f5b6c851db2daf3048ab", size = 384961 }, + { url = "https://files.pythonhosted.org/packages/e4/73/9034827907c7f85c7c484c9494e905d022fb8174526004e9ef332570349e/cytoolz-1.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f546a96460a7e28eb2ec439f4664fa646c9b3e51c6ebad9a59d3922bbe65e30", size = 2091698 }, + { url = "https://files.pythonhosted.org/packages/74/af/d5c2733b0fde1a08254ff1a8a8d567874040c9eb1606363cfebc0713c73f/cytoolz-1.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0317681dd065532d21836f860b0563b199ee716f55d0c1f10de3ce7100c78a3b", size = 2188452 }, + { url = "https://files.pythonhosted.org/packages/6a/bb/77c71fa9c217260b4056a732d754748903423c2cdd82a673d6064741e375/cytoolz-1.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c0ef52febd5a7821a3fd8d10f21d460d1a3d2992f724ba9c91fbd7a96745d41", size = 2174203 }, + { url = "https://files.pythonhosted.org/packages/fc/a9/a5b4a3ff5d22faa1b60293bfe97362e2caf4a830c26d37ab5557f60d04b2/cytoolz-1.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5ebaf419acf2de73b643cf96108702b8aef8e825cf4f63209ceb078d5fbbbfd", size = 2099831 }, + { url = "https://files.pythonhosted.org/packages/35/08/7f6869ea1ff31ce5289a7d58d0e7090acfe7058baa2764473048ff61ea3c/cytoolz-1.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f7f04eeb4088947585c92d6185a618b25ad4a0f8f66ea30c8db83cf94a425e3", size = 1996744 }, + { url = "https://files.pythonhosted.org/packages/46/b4/9ac424c994b51763fd1bbed62d95f8fba8fa0e45c8c3c583904fdaf8f51d/cytoolz-1.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f61928803bb501c17914b82d457c6f50fe838b173fb40d39c38d5961185bd6c7", size = 2013733 }, + { url = "https://files.pythonhosted.org/packages/3e/99/03009765c4b87d742d5b5a8670abb56a8c7ede033c2cdaa4be8662d3b001/cytoolz-1.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d2960cb4fa01ccb985ad1280db41f90dc97a80b397af970a15d5a5de403c8c61", size = 1994850 }, + { url = "https://files.pythonhosted.org/packages/40/9a/8458af9a5557e177ea42f8cf7e477bede518b0bbef564e28c4151feaa52c/cytoolz-1.0.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2b407cc3e9defa8df5eb46644f6f136586f70ba49eba96f43de67b9a0984fd3", size = 2155352 }, + { url = "https://files.pythonhosted.org/packages/5e/5c/2a701423e001fcbec288b4f3fc2bf67557d114c2388237fc1ae67e1e2686/cytoolz-1.0.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8245f929144d4d3bd7b972c9593300195c6cea246b81b4c46053c48b3f044580", size = 2163515 }, + { url = "https://files.pythonhosted.org/packages/36/16/ee2e06e65d9d533bc05cd52a0b355ba9072fc8f60d77289e529c6d2e3750/cytoolz-1.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e37385db03af65763933befe89fa70faf25301effc3b0485fec1c15d4ce4f052", size = 2054431 }, + { url = "https://files.pythonhosted.org/packages/d8/d5/2fac8315f210fa1bc7106e27c19e1211580aa25bb7fa17dfd79505e5baf2/cytoolz-1.0.1-cp311-cp311-win32.whl", hash = "sha256:50f9c530f83e3e574fc95c264c3350adde8145f4f8fc8099f65f00cc595e5ead", size = 322004 }, + { url = "https://files.pythonhosted.org/packages/a9/9e/0b70b641850a95f9ff90adde9d094a4b1d81ec54dadfd97fec0a2aaf440e/cytoolz-1.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:b7f6b617454b4326af7bd3c7c49b0fc80767f134eb9fd6449917a058d17a0e3c", size = 365358 }, + { url = "https://files.pythonhosted.org/packages/d8/e8/218098344ed2cb5f8441fade9b2428e435e7073962374a9c71e59ac141a7/cytoolz-1.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fcb8f7d0d65db1269022e7e0428471edee8c937bc288ebdcb72f13eaa67c2fe4", size = 414121 }, + { url = "https://files.pythonhosted.org/packages/de/27/4d729a5653718109262b758fec1a959aa9facb74c15460d9074dc76d6635/cytoolz-1.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:207d4e4b445e087e65556196ff472ff134370d9a275d591724142e255f384662", size = 390904 }, + { url = "https://files.pythonhosted.org/packages/72/c0/cbabfa788bab9c6038953bf9478adaec06e88903a726946ea7c88092f5c4/cytoolz-1.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21cdf6bac6fd843f3b20280a66fd8df20dea4c58eb7214a2cd8957ec176f0bb3", size = 2090734 }, + { url = "https://files.pythonhosted.org/packages/c3/66/369262c60f9423c2da82a60864a259c852f1aa122aced4acd2c679af58c0/cytoolz-1.0.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a55ec098036c0dea9f3bdc021f8acd9d105a945227d0811589f0573f21c9ce1", size = 2155933 }, + { url = "https://files.pythonhosted.org/packages/aa/4e/ee55186802f8d24b5fbf9a11405ccd1203b30eded07cc17750618219b94e/cytoolz-1.0.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a13ab79ff4ce202e03ab646a2134696988b554b6dc4b71451e948403db1331d8", size = 2171903 }, + { url = "https://files.pythonhosted.org/packages/a1/96/bd1a9f3396e9b7f618db8cd08d15630769ce3c8b7d0534f92cd639c977ae/cytoolz-1.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e2d944799026e1ff08a83241f1027a2d9276c41f7a74224cd98b7df6e03957d", size = 2125270 }, + { url = "https://files.pythonhosted.org/packages/28/48/2a3762873091c88a69e161111cfbc6c222ff145d57ff011a642b169f04f1/cytoolz-1.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88ba85834cd523b91fdf10325e1e6d71c798de36ea9bdc187ca7bd146420de6f", size = 1973967 }, + { url = "https://files.pythonhosted.org/packages/e4/50/500bd69774bdc49a4d78ec8779eb6ac7c1a9d706bfd91cf2a1dba604373a/cytoolz-1.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a750b1af7e8bf6727f588940b690d69e25dc47cce5ce467925a76561317eaf7", size = 2021695 }, + { url = "https://files.pythonhosted.org/packages/e4/4e/ba5a0ce34869495eb50653de8d676847490cf13a2cac1760fc4d313e78de/cytoolz-1.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:44a71870f7eae31d263d08b87da7c2bf1176f78892ed8bdade2c2850478cb126", size = 2010177 }, + { url = "https://files.pythonhosted.org/packages/87/57/615c630b3089a13adb15351d958d227430cf624f03b1dd39eb52c34c1f59/cytoolz-1.0.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8231b9abbd8e368e036f4cc2e16902c9482d4cf9e02a6147ed0e9a3cd4a9ab0", size = 2154321 }, + { url = "https://files.pythonhosted.org/packages/7f/0f/fe1aa2d931e3b35ecc05215bd75da945ea7346095b3b6f6027164e602d5a/cytoolz-1.0.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:aa87599ccc755de5a096a4d6c34984de6cd9dc928a0c5eaa7607457317aeaf9b", size = 2188374 }, + { url = "https://files.pythonhosted.org/packages/de/fa/fd363d97a641b6d0e2fd1d5c35b8fd41d9ccaeb4df56302f53bf23a58e3a/cytoolz-1.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:67cd16537df51baabde3baa770ab7b8d16839c4d21219d5b96ac59fb012ebd2d", size = 2077911 }, + { url = "https://files.pythonhosted.org/packages/d9/68/0a22946b98ae5201b54ccb4e651295285c0fb79406022b6ee8b2f791940c/cytoolz-1.0.1-cp312-cp312-win32.whl", hash = "sha256:fb988c333f05ee30ad4693fe4da55d95ec0bb05775d2b60191236493ea2e01f9", size = 321903 }, + { url = "https://files.pythonhosted.org/packages/62/1a/f3903197956055032f8cb297342e2dff07e50f83991aebfe5b4c4fcb55e4/cytoolz-1.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:8f89c48d8e5aec55ffd566a8ec858706d70ed0c6a50228eca30986bfa5b4da8b", size = 364490 }, + { url = "https://files.pythonhosted.org/packages/aa/2e/a9f069db0107749e9e72baf6c21abe3f006841a3bcfdc9b8420e22ef31eb/cytoolz-1.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6944bb93b287032a4c5ca6879b69bcd07df46f3079cf8393958cf0b0454f50c0", size = 407365 }, + { url = "https://files.pythonhosted.org/packages/a9/9b/5e87dd0e31f54c778b4f9f34cc14c1162d3096c8d746b0f8be97d70dd73c/cytoolz-1.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e027260fd2fc5cb041277158ac294fc13dca640714527219f702fb459a59823a", size = 385233 }, + { url = "https://files.pythonhosted.org/packages/63/00/2fd32b16284cdb97cfe092822179bc0c3bcdd5e927dd39f986169a517642/cytoolz-1.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88662c0e07250d26f5af9bc95911e6137e124a5c1ec2ce4a5d74de96718ab242", size = 2062903 }, + { url = "https://files.pythonhosted.org/packages/85/39/b3cbb5a9847ba59584a263772ad4f8ca2dbfd2a0e11efd09211d1219804c/cytoolz-1.0.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:309dffa78b0961b4c0cf55674b828fbbc793cf2d816277a5c8293c0c16155296", size = 2139517 }, + { url = "https://files.pythonhosted.org/packages/ea/39/bfcab4a46d50c467e36fe704f19d8904efead417787806ee210327f68390/cytoolz-1.0.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:edb34246e6eb40343c5860fc51b24937698e4fa1ee415917a73ad772a9a1746b", size = 2154849 }, + { url = "https://files.pythonhosted.org/packages/fd/42/3bc6ee61b0aa47e1cb40819adc1a456d7efa809f0dea9faddacb43fdde8f/cytoolz-1.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a54da7a8e4348a18d45d4d5bc84af6c716d7f131113a4f1cc45569d37edff1b", size = 2102302 }, + { url = "https://files.pythonhosted.org/packages/00/66/3f636c6ddea7b18026b90a8c238af472e423b86e427b11df02213689b012/cytoolz-1.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:241c679c3b1913c0f7259cf1d9639bed5084c86d0051641d537a0980548aa266", size = 1960872 }, + { url = "https://files.pythonhosted.org/packages/40/36/cb3b7cdd651007b69f9c48e9d104cec7cb8dc53afa1d6a720e5ad08022fa/cytoolz-1.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5bfc860251a8f280ac79696fc3343cfc3a7c30b94199e0240b6c9e5b6b01a2a5", size = 2014430 }, + { url = "https://files.pythonhosted.org/packages/88/3f/2e9bd2a16cfd269808922147551dcb2d8b68ba54a2c4deca2fa6a6cd0d5f/cytoolz-1.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c8edd1547014050c1bdad3ff85d25c82bd1c2a3c96830c6181521eb78b9a42b3", size = 2003127 }, + { url = "https://files.pythonhosted.org/packages/c4/7d/08604ff940aa784df8343c387fdf2489b948b714a6afb587775ae94da912/cytoolz-1.0.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b349bf6162e8de215403d7f35f8a9b4b1853dc2a48e6e1a609a5b1a16868b296", size = 2142369 }, + { url = "https://files.pythonhosted.org/packages/d2/c6/39919a0645bdbdf720e97cae107f959ea9d1267fbc3b0d94fc6e1d12ac8f/cytoolz-1.0.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1b18b35256219b6c3dd0fa037741b85d0bea39c552eab0775816e85a52834140", size = 2180427 }, + { url = "https://files.pythonhosted.org/packages/d8/03/dbb9d47556ee54337e7e0ac209d17ceff2d2a197c34de08005abc7a7449b/cytoolz-1.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:738b2350f340ff8af883eb301054eb724997f795d20d90daec7911c389d61581", size = 2069785 }, + { url = "https://files.pythonhosted.org/packages/ea/f8/11bb7b8947002231faae3ec2342df5896afbc19eb783a332cce6d219ff79/cytoolz-1.0.1-cp313-cp313-win32.whl", hash = "sha256:9cbd9c103df54fcca42be55ef40e7baea624ac30ee0b8bf1149f21146d1078d9", size = 320685 }, + { url = "https://files.pythonhosted.org/packages/40/eb/dde173cf2357084ca9423950be1f2f11ab11d65d8bd30165bfb8fd4213e9/cytoolz-1.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:90e577e08d3a4308186d9e1ec06876d4756b1e8164b92971c69739ea17e15297", size = 362898 }, + { url = "https://files.pythonhosted.org/packages/d9/f7/ef2a10daaec5c0f7d781d50758c6187eee484256e356ae8ef178d6c48497/cytoolz-1.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:83d19d55738ad9c60763b94f3f6d3c6e4de979aeb8d76841c1401081e0e58d96", size = 345702 }, + { url = "https://files.pythonhosted.org/packages/c8/14/53c84adddedb67ff1546abb86fea04d26e24298c3ceab8436d20122ed0b9/cytoolz-1.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f112a71fad6ea824578e6393765ce5c054603afe1471a5c753ff6c67fd872d10", size = 385695 }, + { url = "https://files.pythonhosted.org/packages/bd/80/3ae356c5e7b8d7dc7d1adb52f6932fee85cd748ed4e1217c269d2dfd610f/cytoolz-1.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a515df8f8aa6e1eaaf397761a6e4aff2eef73b5f920aedf271416d5471ae5ee", size = 406261 }, + { url = "https://files.pythonhosted.org/packages/0c/31/8e43761ffc82d90bf9cab7e0959712eedcd1e33c211397e143dd42d7af57/cytoolz-1.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92c398e7b7023460bea2edffe5fcd0a76029580f06c3f6938ac3d198b47156f3", size = 397207 }, + { url = "https://files.pythonhosted.org/packages/d1/b9/fe9da37090b6444c65f848a83e390f87d8cb43d6a4df46de1556ad7e5ceb/cytoolz-1.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3237e56211e03b13df47435b2369f5df281e02b04ad80a948ebd199b7bc10a47", size = 343358 }, +] + +[[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.0" +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/0d/49/bee95f16d2ef068097afeeffbd6c67738107001ee57ad7bcdd4fc4d3c6a7/eth_utils-5.3.0.tar.gz", hash = "sha256:1f096867ac6be895f456fa3acb26e9573ae66e753abad9208f316d24d6178156", size = 123753 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/c6/0417a92e6a3fc9b85f5a8380d9f9d43b69ba836a90e45f79f9ae74d41e53/eth_utils-5.3.0-py3-none-any.whl", hash = "sha256:ac184883ab299d923428bbe25dae5e356979a3993e0ef695a864db0a20bc262d", size = 102531 }, +] + +[[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 = "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 = "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.137.1" +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/65/92/70f29b04e4d7acf7f9a0c3dd52619853715ad9ae092a8e5d89bc7bdc39ec/hypothesis-6.137.1.tar.gz", hash = "sha256:b086e644456da79ad460fdaf8fbf90a41a661e8a4076232dd4ea64cfbc0d0529", size = 460593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/13/92753f97f70f3584a70ccbd2a678878ea43d5880c4e009664c3fe9fe7e50/hypothesis-6.137.1-py3-none-any.whl", hash = "sha256:7cbda6a98ed4d32aad31a5fc5bff5e119b9275fe2579a7b08863cba313a4b9be", size = 527566 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[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.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, +] + +[[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.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/60/bc7622aefb2aee1c0b4ba23c1446d3e30225c8770b38d7aedbfb65ca9d5a/lark-1.2.2.tar.gz", hash = "sha256:ca807d0162cd16cef15a8feecb862d7319e7a09bdb13aef927968e45040fed80", size = 252132 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/00/d90b10b962b4277f5e64a78b6609968859ff86889f5b898c1a778c06ec00/lark-1.2.2-py3-none-any.whl", hash = "sha256:c2276486b02f0f1b90be155f2c8ba4a8e194d42775786db622faccd652d8e80c", size = 111036 }, +] + +[[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.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[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 = "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.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 }, +] + +[[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 = "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.11.7" +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/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782 }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817 }, + { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357 }, + { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011 }, + { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730 }, + { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178 }, + { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462 }, + { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652 }, + { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306 }, + { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720 }, + { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915 }, + { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884 }, + { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496 }, + { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019 }, + { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584 }, + { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071 }, + { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823 }, + { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792 }, + { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338 }, + { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998 }, + { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200 }, + { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890 }, + { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359 }, + { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883 }, + { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074 }, + { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538 }, + { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909 }, + { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786 }, + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000 }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996 }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957 }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199 }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296 }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109 }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028 }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044 }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881 }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034 }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187 }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628 }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866 }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894 }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688 }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808 }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580 }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859 }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810 }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498 }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611 }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924 }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196 }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389 }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223 }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473 }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269 }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921 }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162 }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560 }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777 }, + { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982 }, + { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412 }, + { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749 }, + { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527 }, + { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225 }, + { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490 }, + { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525 }, + { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446 }, + { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678 }, + { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200 }, + { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123 }, + { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852 }, + { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484 }, + { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896 }, + { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475 }, + { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013 }, + { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715 }, + { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757 }, +] + +[[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.1" +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/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474 }, +] + +[[package]] +name = "pytest-cov" +version = "6.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644 }, +] + +[[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-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.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[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.7.34" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/de/e13fa6dc61d78b30ba47481f99933a3b49a57779d625c392d8036770a60d/regex-2025.7.34.tar.gz", hash = "sha256:9ead9765217afd04a86822dfcd4ed2747dfe426e887da413b15ff0ac2457e21a", size = 400714 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/d2/0a44a9d92370e5e105f16669acf801b215107efea9dea4317fe96e9aad67/regex-2025.7.34-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d856164d25e2b3b07b779bfed813eb4b6b6ce73c2fd818d46f47c1eb5cd79bd6", size = 484591 }, + { url = "https://files.pythonhosted.org/packages/2e/b1/00c4f83aa902f1048495de9f2f33638ce970ce1cf9447b477d272a0e22bb/regex-2025.7.34-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d15a9da5fad793e35fb7be74eec450d968e05d2e294f3e0e77ab03fa7234a83", size = 289293 }, + { url = "https://files.pythonhosted.org/packages/f3/b0/5bc5c8ddc418e8be5530b43ae1f7c9303f43aeff5f40185c4287cf6732f2/regex-2025.7.34-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:95b4639c77d414efa93c8de14ce3f7965a94d007e068a94f9d4997bb9bd9c81f", size = 285932 }, + { url = "https://files.pythonhosted.org/packages/46/c7/a1a28d050b23665a5e1eeb4d7f13b83ea86f0bc018da7b8f89f86ff7f094/regex-2025.7.34-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7de1ceed5a5f84f342ba4a9f4ae589524adf9744b2ee61b5da884b5b659834", size = 780361 }, + { url = "https://files.pythonhosted.org/packages/cb/0d/82e7afe7b2c9fe3d488a6ab6145d1d97e55f822dfb9b4569aba2497e3d09/regex-2025.7.34-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02e5860a250cd350c4933cf376c3bc9cb28948e2c96a8bc042aee7b985cfa26f", size = 849176 }, + { url = "https://files.pythonhosted.org/packages/bf/16/3036e16903d8194f1490af457a7e33b06d9e9edd9576b1fe6c7ac660e9ed/regex-2025.7.34-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a5966220b9a1a88691282b7e4350e9599cf65780ca60d914a798cb791aa1177", size = 897222 }, + { url = "https://files.pythonhosted.org/packages/5a/c2/010e089ae00d31418e7d2c6601760eea1957cde12be719730c7133b8c165/regex-2025.7.34-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48fb045bbd4aab2418dc1ba2088a5e32de4bfe64e1457b948bb328a8dc2f1c2e", size = 789831 }, + { url = "https://files.pythonhosted.org/packages/dd/86/b312b7bf5c46d21dbd9a3fdc4a80fde56ea93c9c0b89cf401879635e094d/regex-2025.7.34-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:20ff8433fa45e131f7316594efe24d4679c5449c0ca69d91c2f9d21846fdf064", size = 780665 }, + { url = "https://files.pythonhosted.org/packages/40/e5/674b82bfff112c820b09e3c86a423d4a568143ede7f8440fdcbce259e895/regex-2025.7.34-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c436fd1e95c04c19039668cfb548450a37c13f051e8659f40aed426e36b3765f", size = 773511 }, + { url = "https://files.pythonhosted.org/packages/2d/18/39e7c578eb6cf1454db2b64e4733d7e4f179714867a75d84492ec44fa9b2/regex-2025.7.34-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0b85241d3cfb9f8a13cefdfbd58a2843f208f2ed2c88181bf84e22e0c7fc066d", size = 843990 }, + { url = "https://files.pythonhosted.org/packages/b6/d9/522a6715aefe2f463dc60c68924abeeb8ab6893f01adf5720359d94ede8c/regex-2025.7.34-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:075641c94126b064c65ab86e7e71fc3d63e7ff1bea1fb794f0773c97cdad3a03", size = 834676 }, + { url = "https://files.pythonhosted.org/packages/59/53/c4d5284cb40543566542e24f1badc9f72af68d01db21e89e36e02292eee0/regex-2025.7.34-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:70645cad3407d103d1dbcb4841839d2946f7d36cf38acbd40120fee1682151e5", size = 778420 }, + { url = "https://files.pythonhosted.org/packages/ea/4a/b779a7707d4a44a7e6ee9d0d98e40b2a4de74d622966080e9c95e25e2d24/regex-2025.7.34-cp310-cp310-win32.whl", hash = "sha256:3b836eb4a95526b263c2a3359308600bd95ce7848ebd3c29af0c37c4f9627cd3", size = 263999 }, + { url = "https://files.pythonhosted.org/packages/ef/6e/33c7583f5427aa039c28bff7f4103c2de5b6aa5b9edc330c61ec576b1960/regex-2025.7.34-cp310-cp310-win_amd64.whl", hash = "sha256:cbfaa401d77334613cf434f723c7e8ba585df162be76474bccc53ae4e5520b3a", size = 276023 }, + { url = "https://files.pythonhosted.org/packages/9f/fc/00b32e0ac14213d76d806d952826402b49fd06d42bfabacdf5d5d016bc47/regex-2025.7.34-cp310-cp310-win_arm64.whl", hash = "sha256:bca11d3c38a47c621769433c47f364b44e8043e0de8e482c5968b20ab90a3986", size = 268357 }, + { url = "https://files.pythonhosted.org/packages/0d/85/f497b91577169472f7c1dc262a5ecc65e39e146fc3a52c571e5daaae4b7d/regex-2025.7.34-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da304313761b8500b8e175eb2040c4394a875837d5635f6256d6fa0377ad32c8", size = 484594 }, + { url = "https://files.pythonhosted.org/packages/1c/c5/ad2a5c11ce9e6257fcbfd6cd965d07502f6054aaa19d50a3d7fd991ec5d1/regex-2025.7.34-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:35e43ebf5b18cd751ea81455b19acfdec402e82fe0dc6143edfae4c5c4b3909a", size = 289294 }, + { url = "https://files.pythonhosted.org/packages/8e/01/83ffd9641fcf5e018f9b51aa922c3e538ac9439424fda3df540b643ecf4f/regex-2025.7.34-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96bbae4c616726f4661fe7bcad5952e10d25d3c51ddc388189d8864fbc1b3c68", size = 285933 }, + { url = "https://files.pythonhosted.org/packages/77/20/5edab2e5766f0259bc1da7381b07ce6eb4401b17b2254d02f492cd8a81a8/regex-2025.7.34-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9feab78a1ffa4f2b1e27b1bcdaad36f48c2fed4870264ce32f52a393db093c78", size = 792335 }, + { url = "https://files.pythonhosted.org/packages/30/bd/744d3ed8777dce8487b2606b94925e207e7c5931d5870f47f5b643a4580a/regex-2025.7.34-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f14b36e6d4d07f1a5060f28ef3b3561c5d95eb0651741474ce4c0a4c56ba8719", size = 858605 }, + { url = "https://files.pythonhosted.org/packages/99/3d/93754176289718d7578c31d151047e7b8acc7a8c20e7706716f23c49e45e/regex-2025.7.34-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85c3a958ef8b3d5079c763477e1f09e89d13ad22198a37e9d7b26b4b17438b33", size = 905780 }, + { url = "https://files.pythonhosted.org/packages/ee/2e/c689f274a92deffa03999a430505ff2aeace408fd681a90eafa92fdd6930/regex-2025.7.34-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37555e4ae0b93358fa7c2d240a4291d4a4227cc7c607d8f85596cdb08ec0a083", size = 798868 }, + { url = "https://files.pythonhosted.org/packages/0d/9e/39673688805d139b33b4a24851a71b9978d61915c4d72b5ffda324d0668a/regex-2025.7.34-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee38926f31f1aa61b0232a3a11b83461f7807661c062df9eb88769d86e6195c3", size = 781784 }, + { url = "https://files.pythonhosted.org/packages/18/bd/4c1cab12cfabe14beaa076523056b8ab0c882a8feaf0a6f48b0a75dab9ed/regex-2025.7.34-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a664291c31cae9c4a30589bd8bc2ebb56ef880c9c6264cb7643633831e606a4d", size = 852837 }, + { url = "https://files.pythonhosted.org/packages/cb/21/663d983cbb3bba537fc213a579abbd0f263fb28271c514123f3c547ab917/regex-2025.7.34-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f3e5c1e0925e77ec46ddc736b756a6da50d4df4ee3f69536ffb2373460e2dafd", size = 844240 }, + { url = "https://files.pythonhosted.org/packages/8e/2d/9beeeb913bc5d32faa913cf8c47e968da936af61ec20af5d269d0f84a100/regex-2025.7.34-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d428fc7731dcbb4e2ffe43aeb8f90775ad155e7db4347a639768bc6cd2df881a", size = 787139 }, + { url = "https://files.pythonhosted.org/packages/eb/f5/9b9384415fdc533551be2ba805dd8c4621873e5df69c958f403bfd3b2b6e/regex-2025.7.34-cp311-cp311-win32.whl", hash = "sha256:e154a7ee7fa18333ad90b20e16ef84daaeac61877c8ef942ec8dfa50dc38b7a1", size = 264019 }, + { url = "https://files.pythonhosted.org/packages/18/9d/e069ed94debcf4cc9626d652a48040b079ce34c7e4fb174f16874958d485/regex-2025.7.34-cp311-cp311-win_amd64.whl", hash = "sha256:24257953d5c1d6d3c129ab03414c07fc1a47833c9165d49b954190b2b7f21a1a", size = 276047 }, + { url = "https://files.pythonhosted.org/packages/fd/cf/3bafbe9d1fd1db77355e7fbbbf0d0cfb34501a8b8e334deca14f94c7b315/regex-2025.7.34-cp311-cp311-win_arm64.whl", hash = "sha256:3157aa512b9e606586900888cd469a444f9b898ecb7f8931996cb715f77477f0", size = 268362 }, + { url = "https://files.pythonhosted.org/packages/ff/f0/31d62596c75a33f979317658e8d261574785c6cd8672c06741ce2e2e2070/regex-2025.7.34-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7f7211a746aced993bef487de69307a38c5ddd79257d7be83f7b202cb59ddb50", size = 485492 }, + { url = "https://files.pythonhosted.org/packages/d8/16/b818d223f1c9758c3434be89aa1a01aae798e0e0df36c1f143d1963dd1ee/regex-2025.7.34-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fb31080f2bd0681484b275461b202b5ad182f52c9ec606052020fe13eb13a72f", size = 290000 }, + { url = "https://files.pythonhosted.org/packages/cd/70/69506d53397b4bd6954061bae75677ad34deb7f6ca3ba199660d6f728ff5/regex-2025.7.34-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0200a5150c4cf61e407038f4b4d5cdad13e86345dac29ff9dab3d75d905cf130", size = 286072 }, + { url = "https://files.pythonhosted.org/packages/b0/73/536a216d5f66084fb577bb0543b5cb7de3272eb70a157f0c3a542f1c2551/regex-2025.7.34-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:739a74970e736df0773788377969c9fea3876c2fc13d0563f98e5503e5185f46", size = 797341 }, + { url = "https://files.pythonhosted.org/packages/26/af/733f8168449e56e8f404bb807ea7189f59507cbea1b67a7bbcd92f8bf844/regex-2025.7.34-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4fef81b2f7ea6a2029161ed6dea9ae13834c28eb5a95b8771828194a026621e4", size = 862556 }, + { url = "https://files.pythonhosted.org/packages/19/dd/59c464d58c06c4f7d87de4ab1f590e430821345a40c5d345d449a636d15f/regex-2025.7.34-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea74cf81fe61a7e9d77989050d0089a927ab758c29dac4e8e1b6c06fccf3ebf0", size = 910762 }, + { url = "https://files.pythonhosted.org/packages/37/a8/b05ccf33ceca0815a1e253693b2c86544932ebcc0049c16b0fbdf18b688b/regex-2025.7.34-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4636a7f3b65a5f340ed9ddf53585c42e3ff37101d383ed321bfe5660481744b", size = 801892 }, + { url = "https://files.pythonhosted.org/packages/5f/9a/b993cb2e634cc22810afd1652dba0cae156c40d4864285ff486c73cd1996/regex-2025.7.34-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cef962d7834437fe8d3da6f9bfc6f93f20f218266dcefec0560ed7765f5fe01", size = 786551 }, + { url = "https://files.pythonhosted.org/packages/2d/79/7849d67910a0de4e26834b5bb816e028e35473f3d7ae563552ea04f58ca2/regex-2025.7.34-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:cbe1698e5b80298dbce8df4d8d1182279fbdaf1044e864cbc9d53c20e4a2be77", size = 856457 }, + { url = "https://files.pythonhosted.org/packages/91/c6/de516bc082524b27e45cb4f54e28bd800c01efb26d15646a65b87b13a91e/regex-2025.7.34-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:32b9f9bcf0f605eb094b08e8da72e44badabb63dde6b83bd530580b488d1c6da", size = 848902 }, + { url = "https://files.pythonhosted.org/packages/7d/22/519ff8ba15f732db099b126f039586bd372da6cd4efb810d5d66a5daeda1/regex-2025.7.34-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:524c868ba527eab4e8744a9287809579f54ae8c62fbf07d62aacd89f6026b282", size = 788038 }, + { url = "https://files.pythonhosted.org/packages/3f/7d/aabb467d8f57d8149895d133c88eb809a1a6a0fe262c1d508eb9dfabb6f9/regex-2025.7.34-cp312-cp312-win32.whl", hash = "sha256:d600e58ee6d036081c89696d2bdd55d507498a7180df2e19945c6642fac59588", size = 264417 }, + { url = "https://files.pythonhosted.org/packages/3b/39/bd922b55a4fc5ad5c13753274e5b536f5b06ec8eb9747675668491c7ab7a/regex-2025.7.34-cp312-cp312-win_amd64.whl", hash = "sha256:9a9ab52a466a9b4b91564437b36417b76033e8778e5af8f36be835d8cb370d62", size = 275387 }, + { url = "https://files.pythonhosted.org/packages/f7/3c/c61d2fdcecb754a40475a3d1ef9a000911d3e3fc75c096acf44b0dfb786a/regex-2025.7.34-cp312-cp312-win_arm64.whl", hash = "sha256:c83aec91af9c6fbf7c743274fd952272403ad9a9db05fe9bfc9df8d12b45f176", size = 268482 }, + { url = "https://files.pythonhosted.org/packages/15/16/b709b2119975035169a25aa8e4940ca177b1a2e25e14f8d996d09130368e/regex-2025.7.34-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3c9740a77aeef3f5e3aaab92403946a8d34437db930a0280e7e81ddcada61f5", size = 485334 }, + { url = "https://files.pythonhosted.org/packages/94/a6/c09136046be0595f0331bc58a0e5f89c2d324cf734e0b0ec53cf4b12a636/regex-2025.7.34-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:69ed3bc611540f2ea70a4080f853741ec698be556b1df404599f8724690edbcd", size = 289942 }, + { url = "https://files.pythonhosted.org/packages/36/91/08fc0fd0f40bdfb0e0df4134ee37cfb16e66a1044ac56d36911fd01c69d2/regex-2025.7.34-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d03c6f9dcd562c56527c42b8530aad93193e0b3254a588be1f2ed378cdfdea1b", size = 285991 }, + { url = "https://files.pythonhosted.org/packages/be/2f/99dc8f6f756606f0c214d14c7b6c17270b6bbe26d5c1f05cde9dbb1c551f/regex-2025.7.34-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6164b1d99dee1dfad33f301f174d8139d4368a9fb50bf0a3603b2eaf579963ad", size = 797415 }, + { url = "https://files.pythonhosted.org/packages/62/cf/2fcdca1110495458ba4e95c52ce73b361cf1cafd8a53b5c31542cde9a15b/regex-2025.7.34-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1e4f4f62599b8142362f164ce776f19d79bdd21273e86920a7b604a4275b4f59", size = 862487 }, + { url = "https://files.pythonhosted.org/packages/90/38/899105dd27fed394e3fae45607c1983e138273ec167e47882fc401f112b9/regex-2025.7.34-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:72a26dcc6a59c057b292f39d41465d8233a10fd69121fa24f8f43ec6294e5415", size = 910717 }, + { url = "https://files.pythonhosted.org/packages/ee/f6/4716198dbd0bcc9c45625ac4c81a435d1c4d8ad662e8576dac06bab35b17/regex-2025.7.34-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5273fddf7a3e602695c92716c420c377599ed3c853ea669c1fe26218867002f", size = 801943 }, + { url = "https://files.pythonhosted.org/packages/40/5d/cff8896d27e4e3dd11dd72ac78797c7987eb50fe4debc2c0f2f1682eb06d/regex-2025.7.34-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c1844be23cd40135b3a5a4dd298e1e0c0cb36757364dd6cdc6025770363e06c1", size = 786664 }, + { url = "https://files.pythonhosted.org/packages/10/29/758bf83cf7b4c34f07ac3423ea03cee3eb3176941641e4ccc05620f6c0b8/regex-2025.7.34-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dde35e2afbbe2272f8abee3b9fe6772d9b5a07d82607b5788e8508974059925c", size = 856457 }, + { url = "https://files.pythonhosted.org/packages/d7/30/c19d212b619963c5b460bfed0ea69a092c6a43cba52a973d46c27b3e2975/regex-2025.7.34-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f6e8e7af516a7549412ce57613e859c3be27d55341a894aacaa11703a4c31a", size = 849008 }, + { url = "https://files.pythonhosted.org/packages/9e/b8/3c35da3b12c87e3cc00010ef6c3a4ae787cff0bc381aa3d251def219969a/regex-2025.7.34-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:469142fb94a869beb25b5f18ea87646d21def10fbacb0bcb749224f3509476f0", size = 788101 }, + { url = "https://files.pythonhosted.org/packages/47/80/2f46677c0b3c2b723b2c358d19f9346e714113865da0f5f736ca1a883bde/regex-2025.7.34-cp313-cp313-win32.whl", hash = "sha256:da7507d083ee33ccea1310447410c27ca11fb9ef18c95899ca57ff60a7e4d8f1", size = 264401 }, + { url = "https://files.pythonhosted.org/packages/be/fa/917d64dd074682606a003cba33585c28138c77d848ef72fc77cbb1183849/regex-2025.7.34-cp313-cp313-win_amd64.whl", hash = "sha256:9d644de5520441e5f7e2db63aec2748948cc39ed4d7a87fd5db578ea4043d997", size = 275368 }, + { url = "https://files.pythonhosted.org/packages/65/cd/f94383666704170a2154a5df7b16be28f0c27a266bffcd843e58bc84120f/regex-2025.7.34-cp313-cp313-win_arm64.whl", hash = "sha256:7bf1c5503a9f2cbd2f52d7e260acb3131b07b6273c470abb78568174fe6bde3f", size = 268482 }, + { url = "https://files.pythonhosted.org/packages/ac/23/6376f3a23cf2f3c00514b1cdd8c990afb4dfbac3cb4a68b633c6b7e2e307/regex-2025.7.34-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:8283afe7042d8270cecf27cca558873168e771183d4d593e3c5fe5f12402212a", size = 485385 }, + { url = "https://files.pythonhosted.org/packages/73/5b/6d4d3a0b4d312adbfd6d5694c8dddcf1396708976dd87e4d00af439d962b/regex-2025.7.34-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6c053f9647e3421dd2f5dff8172eb7b4eec129df9d1d2f7133a4386319b47435", size = 289788 }, + { url = "https://files.pythonhosted.org/packages/92/71/5862ac9913746e5054d01cb9fb8125b3d0802c0706ef547cae1e7f4428fa/regex-2025.7.34-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a16dd56bbcb7d10e62861c3cd000290ddff28ea142ffb5eb3470f183628011ac", size = 286136 }, + { url = "https://files.pythonhosted.org/packages/27/df/5b505dc447eb71278eba10d5ec940769ca89c1af70f0468bfbcb98035dc2/regex-2025.7.34-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69c593ff5a24c0d5c1112b0df9b09eae42b33c014bdca7022d6523b210b69f72", size = 797753 }, + { url = "https://files.pythonhosted.org/packages/86/38/3e3dc953d13998fa047e9a2414b556201dbd7147034fbac129392363253b/regex-2025.7.34-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98d0ce170fcde1a03b5df19c5650db22ab58af375aaa6ff07978a85c9f250f0e", size = 863263 }, + { url = "https://files.pythonhosted.org/packages/68/e5/3ff66b29dde12f5b874dda2d9dec7245c2051f2528d8c2a797901497f140/regex-2025.7.34-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d72765a4bff8c43711d5b0f5b452991a9947853dfa471972169b3cc0ba1d0751", size = 910103 }, + { url = "https://files.pythonhosted.org/packages/9e/fe/14176f2182125977fba3711adea73f472a11f3f9288c1317c59cd16ad5e6/regex-2025.7.34-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4494f8fd95a77eb434039ad8460e64d57baa0434f1395b7da44015bef650d0e4", size = 801709 }, + { url = "https://files.pythonhosted.org/packages/5a/0d/80d4e66ed24f1ba876a9e8e31b709f9fd22d5c266bf5f3ab3c1afe683d7d/regex-2025.7.34-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4f42b522259c66e918a0121a12429b2abcf696c6f967fa37bdc7b72e61469f98", size = 786726 }, + { url = "https://files.pythonhosted.org/packages/12/75/c3ebb30e04a56c046f5c85179dc173818551037daae2c0c940c7b19152cb/regex-2025.7.34-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:aaef1f056d96a0a5d53ad47d019d5b4c66fe4be2da87016e0d43b7242599ffc7", size = 857306 }, + { url = "https://files.pythonhosted.org/packages/b1/b2/a4dc5d8b14f90924f27f0ac4c4c4f5e195b723be98adecc884f6716614b6/regex-2025.7.34-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:656433e5b7dccc9bc0da6312da8eb897b81f5e560321ec413500e5367fcd5d47", size = 848494 }, + { url = "https://files.pythonhosted.org/packages/0d/21/9ac6e07a4c5e8646a90b56b61f7e9dac11ae0747c857f91d3d2bc7c241d9/regex-2025.7.34-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e91eb2c62c39705e17b4d42d4b86c4e86c884c0d15d9c5a47d0835f8387add8e", size = 787850 }, + { url = "https://files.pythonhosted.org/packages/be/6c/d51204e28e7bc54f9a03bb799b04730d7e54ff2718862b8d4e09e7110a6a/regex-2025.7.34-cp314-cp314-win32.whl", hash = "sha256:f978ddfb6216028c8f1d6b0f7ef779949498b64117fc35a939022f67f810bdcb", size = 269730 }, + { url = "https://files.pythonhosted.org/packages/74/52/a7e92d02fa1fdef59d113098cb9f02c5d03289a0e9f9e5d4d6acccd10677/regex-2025.7.34-cp314-cp314-win_amd64.whl", hash = "sha256:4b7dc33b9b48fb37ead12ffc7bdb846ac72f99a80373c4da48f64b373a7abeae", size = 278640 }, + { url = "https://files.pythonhosted.org/packages/d1/78/a815529b559b1771080faa90c3ab401730661f99d495ab0071649f139ebd/regex-2025.7.34-cp314-cp314-win_arm64.whl", hash = "sha256:4b8c4d39f451e64809912c82392933d80fe2e4a87eeef8859fcc5380d0173c64", size = 271757 }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847 }, +] + +[[package]] +name = "rich" +version = "14.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368 }, +] + +[[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 = { registry = "https://pypi.org/simple" } +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" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/85/2cfd05b649cf173714ea7a2b3597228b7a577cab7fe4696e67879b8784b7/titanoboa-0.2.7.tar.gz", hash = "sha256:4fb931747bf9beb05ea39b149878bb7d89525bcfacf1a903e4d14da91f7c49de", size = 99921 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/fd/25ccd329bb68c2251b1fbb30aa799cfaca67ab262b2da05c5997e6b73e38/titanoboa-0.2.7-py3-none-any.whl", hash = "sha256:75771f5e8183c073f2c37d8925f583abd2c6b56eae4d087dbf1e33f809ea7ca5", size = 111582 }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + +[[package]] +name = "toolz" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/0b/d80dfa675bf592f636d1ea0b835eab4ec8df6e9415d8cfd766df54456123/toolz-1.0.0.tar.gz", hash = "sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02", size = 66790 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl", hash = "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236", size = 56383 }, +] + +[[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.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906 }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 }, +] + +[[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 = "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 }, +] From dd0890c522747260f93f48af80596fa531fa142f Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 6 Aug 2025 15:07:30 +0200 Subject: [PATCH 077/413] chore: bump Vault and Factory to Vyper 0.4.1 --- contracts/interfaces/ILendingFactory.vyi | 28 ++ contracts/interfaces/ILlamalendController.vyi | 355 ++++++++++++++++++ contracts/interfaces/IPool.vyi | 9 + contracts/interfaces/IVault.vyi | 24 ++ contracts/lending/LendingFactory.vy | 192 ++++------ contracts/lending/Vault.vy | 203 ++++------ 6 files changed, 578 insertions(+), 233 deletions(-) create mode 100644 contracts/interfaces/ILendingFactory.vyi create mode 100644 contracts/interfaces/IPool.vyi diff --git a/contracts/interfaces/ILendingFactory.vyi b/contracts/interfaces/ILendingFactory.vyi new file mode 100644 index 00000000..660ac92a --- /dev/null +++ b/contracts/interfaces/ILendingFactory.vyi @@ -0,0 +1,28 @@ +event SetImplementations: + amm: address + controller: address + vault: address + price_oracle: address + monetary_policy: address + +event SetDefaultRates: + min_rate: uint256 + max_rate: uint256 + +event SetAdmin: + admin: address + +event SetFeeReceiver: + fee_receiver: 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 + +# TODO populate this \ No newline at end of file diff --git a/contracts/interfaces/ILlamalendController.vyi b/contracts/interfaces/ILlamalendController.vyi index 59f7277a..88c3c5ae 100644 --- a/contracts/interfaces/ILlamalendController.vyi +++ b/contracts/interfaces/ILlamalendController.vyi @@ -1,7 +1,357 @@ from contracts.interfaces import IVault +from ethereum.ercs 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 + stablecoin_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 + + +# 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 + stablecoins: uint256 + collateral: uint256 + +# Functions + +@view +@external +def monetary_policy() -> IMonetaryPolicy: + ... + + +@view +@external +def minted() -> uint256: + ... + + +@view +@external +def redeemed() -> uint256: + ... + + +@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, max_active_band: int256, callbacker: address, calldata: Bytes[10000]): + ... + + +@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 repaid() -> uint256: + ... + + +@view +@external +def processed() -> uint256: + ... + + + +# EXTRA THINGS FOR LLAMALEND CONTROLLER + event SetBorrowCap: borrow_cap: uint256 @@ -40,3 +390,8 @@ def admin_fee() -> uint256: ... +@view +@external +def borrowed_balance() -> uint256: + ... + diff --git a/contracts/interfaces/IPool.vyi b/contracts/interfaces/IPool.vyi new file mode 100644 index 00000000..c754fb90 --- /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: + ... \ No newline at end of file diff --git a/contracts/interfaces/IVault.vyi b/contracts/interfaces/IVault.vyi index bb4500ea..83695e84 100644 --- a/contracts/interfaces/IVault.vyi +++ b/contracts/interfaces/IVault.vyi @@ -1,4 +1,6 @@ from ethereum.ercs import IERC20 +from contracts.interfaces import IAMM +# from contracts.interfaces import ILlamalendController as IController @view def collateral_token() -> IERC20: @@ -23,4 +25,26 @@ 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() -> address: + ... + + +@view +@external +def amm() -> IAMM: + ... + + +@external +def set_max_supply(max_supply: uint256): + ... + +# TODO populate + implement diff --git a/contracts/lending/LendingFactory.vy b/contracts/lending/LendingFactory.vy index bad3dfa1..1b35a1db 100644 --- a/contracts/lending/LendingFactory.vy +++ b/contracts/lending/LendingFactory.vy @@ -1,4 +1,4 @@ -# @version 0.3.10 +# pragma version 0.4.3 # pragma optimize codesize # pragma evm-version shanghai """ @@ -8,67 +8,23 @@ @license Copyright (c) Curve.Fi, 2020-2025 - all rights reserved """ -from vyper.interfaces import ERC20Detailed as ERC20 - - -interface Vault: - def initialize(amm: address, controller: address, borrowed_token: address, collateral_token: address): nonpayable - def amm() -> address: view - def controller() -> address: view - def borrowed_token() -> address: view - def collateral_token() -> address: view - def set_max_supply(_value: uint256): nonpayable - -interface Controller: - def monetary_policy() -> address: view - -interface AMM: - def set_admin(_admin: address): nonpayable - def price_oracle_contract() -> address: view - -interface Pool: - def price_oracle(i: uint256 = 0) -> uint256: view # Universal method! - def coins(i: uint256) -> address: view - -interface PriceOracle: - def price() -> uint256: view - def price_w() -> uint256: nonpayable - - -event SetImplementations: - amm: address - controller: address - vault: address - price_oracle: address - monetary_policy: address - -event SetDefaultRates: - min_rate: uint256 - max_rate: uint256 - -event SetAdmin: - admin: address - -event SetFeeReceiver: - fee_receiver: 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 +from ethereum.ercs import IERC20 +from ethereum.ercs import IERC20Detailed +from contracts.interfaces import IVault +from contracts.interfaces import ILlamalendController as IController +from contracts.interfaces import IAMM +from contracts.interfaces import IPool +from contracts.interfaces import IPriceOracle +from contracts.interfaces import ILendingFactory +implements: ILendingFactory 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% +MIN_RATE: public(constant(uint256)) = 10**15 // (365 * 86400) # 0.1% +MAX_RATE: public(constant(uint256)) = 10**19 // (365 * 86400) # 1000% MIN_A: constant(uint256) = 2 MAX_A: constant(uint256) = 10000 MIN_FEE: constant(uint256) = 10**6 # 1e-12, still needs to be above 0 @@ -83,7 +39,7 @@ vault_impl: public(address) pool_price_oracle_impl: public(address) monetary_policy_impl: public(address) -# Actual min/max borrow rates when creating new markets +# 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) @@ -93,18 +49,18 @@ admin: public(address) fee_receiver: public(address) # Vaults can only be created but not removed -vaults: public(Vault[10**18]) -_vaults_index: HashMap[Vault, uint256] +vaults: public(IVault[10**18]) +_vaults_index: HashMap[IVault, uint256] market_count: public(uint256) # Index to find vaults by a non-crvUSD token -token_to_vaults: public(HashMap[address, Vault[10**18]]) +token_to_vaults: public(HashMap[address, IVault[10**18]]) token_market_count: public(HashMap[address, uint256]) names: public(HashMap[uint256, String[64]]) -@external +@deploy def __init__( stablecoin: address, amm: address, @@ -132,8 +88,8 @@ def __init__( self.pool_price_oracle_impl = pool_price_oracle self.monetary_policy_impl = monetary_policy - self.min_default_borrow_rate = 5 * 10**15 / (365 * 86400) - self.max_default_borrow_rate = 50 * 10**16 / (365 * 86400) + self.min_default_borrow_rate = 5 * 10**15 // (365 * 86400) + self.max_default_borrow_rate = 50 * 10**16 // (365 * 86400) self.admin = admin self.fee_receiver = fee_receiver @@ -145,27 +101,27 @@ 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 + # 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): + for i: uint256 in range(8): t: uint256 = 2**(7 - i) p: uint256 = 2**t if x >= p * 10**18: - x /= p + x //= p res += t * 10**18 d: uint256 = 10**18 - for i in range(59): # 18 decimals: math.log2(10**10) == 59.7 + for i: uint256 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 + 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) + # ln(x) = log2(x) // log2(e) + return convert(res * 10**18 // 1442695040888963328, int256) @internal @@ -203,16 +159,16 @@ def _create( monetary_policy: address = create_from_blueprint( self.monetary_policy_impl, borrowed_token, min_rate, max_rate, code_offset=3) - A_ratio: uint256 = 10**18 * A / (A - 1) - p: uint256 = PriceOracle(price_oracle).price() # This also validates price oracle ABI + A_ratio: uint256 = 10**18 * A // (A - 1) + p: uint256 = staticcall IPriceOracle(price_oracle).price() # This also validates price oracle ABI assert p > 0 - assert PriceOracle(price_oracle).price_w() == p + assert extcall IPriceOracle(price_oracle).price_w() == p - vault: Vault = Vault(create_minimal_proxy_to(self.vault_impl)) + vault: IVault = IVault(create_minimal_proxy_to(self.vault_impl)) amm: address = create_from_blueprint( self.amm_impl, - borrowed_token, 10**convert(18 - ERC20(borrowed_token).decimals(), uint256), - collateral_token, 10**convert(18 - ERC20(collateral_token).decimals(), uint256), + borrowed_token, 10**convert(18 - staticcall IERC20Detailed(borrowed_token).decimals(), uint256), + collateral_token, 10**convert(18 - staticcall IERC20Detailed(collateral_token).decimals(), uint256), A, isqrt(A_ratio * 10**18), self.ln_int(A_ratio), p, fee, convert(0, uint256), price_oracle, code_offset=3) @@ -222,13 +178,21 @@ def _create( borrowed_token, collateral_token, monetary_policy, loan_discount, liquidation_discount, code_offset=3) - AMM(amm).set_admin(controller) + extcall IAMM(amm).set_admin(controller) - vault.initialize(amm, controller, borrowed_token, collateral_token) + extcall vault.initialize(IAMM(amm), controller, IERC20(borrowed_token), IERC20(collateral_token)) market_count: uint256 = self.market_count - log NewVault(market_count, collateral_token, borrowed_token, - vault.address, controller, amm, price_oracle, monetary_policy) + log ILendingFactory.NewVault( + id=market_count, + collateral_token=collateral_token, + borrowed_token=borrowed_token, + vault=vault.address, + controller=controller, + amm=amm, + price_oracle=price_oracle, + monetary_policy=monetary_policy + ) self.vaults[market_count] = vault self._vaults_index[vault] = market_count + 2**128 self.names[market_count] = name @@ -246,7 +210,7 @@ def _create( @external -@nonreentrant('lock') +@nonreentrant def create( borrowed_token: address, collateral_token: address, @@ -264,10 +228,10 @@ def create( @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 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 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) @@ -277,13 +241,13 @@ def create( res: address[3] = 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(res[0]).set_max_supply(supply_limit) + extcall IVault(res[0]).set_max_supply(supply_limit) return res @external -@nonreentrant('lock') +@nonreentrant def create_from_pool( borrowed_token: address, collateral_token: address, @@ -301,10 +265,10 @@ def create_from_pool( @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 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 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 @@ -316,12 +280,12 @@ def create_from_pool( borrowed_ix: uint256 = 100 collateral_ix: uint256 = 100 N: uint256 = 0 - for i in range(10): + 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)")), + 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): @@ -341,7 +305,7 @@ def create_from_pool( price_oracle, name, min_borrow_rate, max_borrow_rate, ) if supply_limit < max_value(uint256): - Vault(res[0]).set_max_supply(supply_limit) + extcall IVault(res[0]).set_max_supply(supply_limit) return res @@ -349,47 +313,47 @@ def create_from_pool( @view @external def controllers(n: uint256) -> address: - return self.vaults[n].controller() + return staticcall self.vaults[n].controller() @view @external def amms(n: uint256) -> address: - return self.vaults[n].amm() + return (staticcall self.vaults[n].amm()).address @view @external def borrowed_tokens(n: uint256) -> address: - return self.vaults[n].borrowed_token() + return (staticcall self.vaults[n].borrowed_token()).address @view @external def collateral_tokens(n: uint256) -> address: - return self.vaults[n].collateral_token() + return (staticcall self.vaults[n].collateral_token()).address @view @external def price_oracles(n: uint256) -> address: - return AMM(self.vaults[n].amm()).price_oracle_contract() + return (staticcall (staticcall self.vaults[n].amm()).price_oracle_contract()).address @view @external def monetary_policies(n: uint256) -> address: - return Controller(self.vaults[n].controller()).monetary_policy() + return (staticcall IController(staticcall self.vaults[n].controller()).monetary_policy()).address @view @external -def vaults_index(vault: Vault) -> uint256: +def vaults_index(vault: IVault) -> uint256: return self._vaults_index[vault] - 2**128 @external -@nonreentrant('lock') +@nonreentrant def set_implementations(controller: address, amm: address, vault: address, pool_price_oracle: address, monetary_policy: address): """ @@ -414,11 +378,17 @@ def set_implementations(controller: address, amm: address, vault: address, if monetary_policy != empty(address): self.monetary_policy_impl = monetary_policy - log SetImplementations(amm, controller, vault, pool_price_oracle, monetary_policy) + log ILendingFactory.SetImplementations( + amm=amm, + controller=controller, + vault=vault, + price_oracle=pool_price_oracle, + monetary_policy=monetary_policy + ) @external -@nonreentrant('lock') +@nonreentrant def set_default_rates(min_rate: uint256, max_rate: uint256): """ @notice Change min and max default borrow rates for creating new markets @@ -434,11 +404,11 @@ def set_default_rates(min_rate: uint256, max_rate: uint256): self.min_default_borrow_rate = min_rate self.max_default_borrow_rate = max_rate - log SetDefaultRates(min_rate, max_rate) + log ILendingFactory.SetDefaultRates(min_rate=min_rate, max_rate=max_rate) @external -@nonreentrant('lock') +@nonreentrant def set_admin(admin: address): """ @notice Set admin of the factory (should end up with DAO) @@ -446,11 +416,11 @@ def set_admin(admin: address): """ assert msg.sender == self.admin self.admin = admin - log SetAdmin(admin) + log ILendingFactory.SetAdmin(admin=admin) @external -@nonreentrant('lock') +@nonreentrant def set_fee_receiver(fee_receiver: address): """ @notice Set fee receiver who earns interest (DAO) @@ -459,11 +429,11 @@ def set_fee_receiver(fee_receiver: address): assert msg.sender == self.admin assert fee_receiver != empty(address) self.fee_receiver = fee_receiver - log SetFeeReceiver(fee_receiver) + log ILendingFactory.SetFeeReceiver(fee_receiver=fee_receiver) @external @view def coins(vault_id: uint256) -> address[2]: - vault: Vault = self.vaults[vault_id] - return [vault.borrowed_token(), vault.collateral_token()] + vault: IVault = self.vaults[vault_id] + return [(staticcall vault.borrowed_token()).address, (staticcall vault.collateral_token()).address] diff --git a/contracts/lending/Vault.vy b/contracts/lending/Vault.vy index b2afdf74..7723a9b0 100644 --- a/contracts/lending/Vault.vy +++ b/contracts/lending/Vault.vy @@ -1,4 +1,4 @@ -# @version 0.3.10 +# pragma version 0.4.3 # pragma optimize codesize # pragma evm-version shanghai """ @@ -8,61 +8,20 @@ @license Copyright (c) Curve.Fi, 2020-2025 - all rights reserved """ -from vyper.interfaces import ERC20 as ERC20Spec -from vyper.interfaces import ERC20Detailed +from ethereum.ercs import IERC20 +from ethereum.ercs import IERC20Detailed +from ethereum.ercs 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 +implements: IERC20 +implements: IERC20Detailed +# 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 rate() -> uint256: view - -interface Controller: - def total_debt() -> uint256: view - def borrowed_balance() -> uint256: view - def check_lock() -> bool: view - def save_rate(): 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 event SetMaxSupply: max_supply: uint256 @@ -71,15 +30,15 @@ event SetMaxSupply: # 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) -amm: public(AMM) -controller: public(Controller) -factory: public(Factory) +amm: public(IAMM) +controller: public(IController) +factory: public(IFactory) maxSupply: public(uint256) @@ -102,7 +61,7 @@ totalSupply: public(uint256) precision: uint256 -@external +@deploy def __init__(): """ @notice Template for Vault implementation @@ -110,15 +69,15 @@ def __init__(): # 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) + self.borrowed_token = IERC20(0x0000000000000000000000000000000000000001) @external def initialize( - amm: AMM, - controller: Controller, - borrowed_token: ERC20, - collateral_token: ERC20, + amm: IAMM, + controller: IController, + borrowed_token: IERC20, + collateral_token: IERC20, ): """ @notice Initializer for vaults @@ -130,16 +89,16 @@ def initialize( assert self.borrowed_token.address == empty(address) self.borrowed_token = borrowed_token - borrowed_precision: uint256 = 10**(18 - borrowed_token.decimals()) + borrowed_precision: uint256 = 10**(18 - convert(staticcall IERC20Detailed(borrowed_token.address).decimals(), uint256)) self.collateral_token = collateral_token - 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 IERC20Detailed(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 @@ -153,38 +112,38 @@ 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 """ @@ -195,12 +154,12 @@ def asset() -> ERC20: @view def _total_assets() -> uint256: # admin fee should be accounted for here when enabled - return self.controller.borrowed_balance() + self.controller.total_debt() + return staticcall self.controller.borrowed_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 @@ -219,9 +178,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 @@ -235,37 +194,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 @@ -275,7 +234,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 @@ -299,7 +258,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 @@ -308,23 +267,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) + assert extcall self.borrowed_token.transferFrom(msg.sender, controller.address, assets, default_return_value=True) self.deposited += assets 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 @@ -344,7 +303,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 @@ -353,51 +312,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) + assert extcall self.borrowed_token.transferFrom(msg.sender, controller.address, assets, default_return_value=True) self.deposited += assets 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.controller.borrowed_balance()) + staticcall self.controller.borrowed_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.controller.borrowed_balance() + assert assets <= staticcall self.controller.borrowed_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 @@ -413,30 +372,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) + assert extcall self.borrowed_token.transferFrom(controller.address, receiver, assets, default_return_value=True) self.withdrawn += assets - controller.save_rate() - log Withdraw(msg.sender, receiver, owner, assets, shares) + 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.controller.borrowed_balance(), False), + self._convert_to_shares(staticcall self.controller.borrowed_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 @@ -447,12 +406,12 @@ def previewRedeem(shares: uint256) -> uint256: else: assets_to_redeem: uint256 = self._convert_to_assets(shares) - assert assets_to_redeem <= self.controller.borrowed_balance() + assert assets_to_redeem <= staticcall self.controller.borrowed_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 @@ -474,11 +433,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: IController = self.controller + assert extcall self.borrowed_token.transferFrom(controller.address, receiver, assets_to_redeem, default_return_value=True) self.withdrawn += assets_to_redeem - controller.save_rate() - log Withdraw(msg.sender, receiver, owner, assets_to_redeem, shares) + extcall controller.save_rate() + log IERC4626.Withdraw(sender=msg.sender, receiver=receiver, owner=owner, assets=assets_to_redeem, shares=shares) return assets_to_redeem @@ -488,7 +447,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 @@ -496,7 +455,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 @@ -504,7 +463,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 @@ -514,7 +473,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 @@ -609,4 +568,4 @@ def decreaseAllowance(_spender: address, _sub_value: uint256) -> bool: @external @view def admin() -> address: - return self.factory.admin() + return staticcall self.factory.admin() From 0d6d93d8f47eda39e48cae72101b37bd1c833007 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 6 Aug 2025 16:34:10 +0200 Subject: [PATCH 078/413] fix: reuse structs from MintController --- contracts/interfaces/ILlamalendController.vyi | 23 ++----------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/contracts/interfaces/ILlamalendController.vyi b/contracts/interfaces/ILlamalendController.vyi index 88c3c5ae..a6a6bee5 100644 --- a/contracts/interfaces/ILlamalendController.vyi +++ b/contracts/interfaces/ILlamalendController.vyi @@ -5,6 +5,7 @@ from contracts.interfaces import ILMGauge from contracts.interfaces import IAMM from contracts.interfaces import IMonetaryPolicy from contracts.interfaces import IFactory +from contracts.interfaces import IMintController # Events @@ -71,26 +72,6 @@ event CollectFees: new_supply: uint256 -# 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 - stablecoins: uint256 - collateral: uint256 - # Functions @view @@ -273,7 +254,7 @@ def health(user: address, full: bool) -> int256: @view @external -def users_to_liquidate(_from: uint256, _limit: uint256) -> DynArray[Position, 1000]: +def users_to_liquidate(_from: uint256, _limit: uint256) -> DynArray[IMintController.Position, 1000]: ... From a704ce8f4ef4d2dc748fd5a7048be00232ed44df Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 7 Aug 2025 11:12:13 +0400 Subject: [PATCH 079/413] fix: args order for LLController init --- contracts/lending/LLController.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy index 5af4f456..99adcb3d 100644 --- a/contracts/lending/LLController.vy +++ b/contracts/lending/LLController.vy @@ -94,12 +94,12 @@ MAX_ADMIN_FEE: constant(uint256) = 2 * 10**17 # 20% @deploy def __init__( vault: IVault, + amm: IAMM, collateral_token: IERC20, borrowed_token: IERC20, monetary_policy: IMonetaryPolicy, loan_discount: uint256, liquidation_discount: uint256, - amm: IAMM, ): """ @notice Controller constructor deployed by the factory from blueprint From 0ffc963100de8b301a14d684be73beb68860c4ad Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 7 Aug 2025 11:12:48 +0400 Subject: [PATCH 080/413] fix: ERC20 interface for Vault --- contracts/lending/Vault.vy | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/contracts/lending/Vault.vy b/contracts/lending/Vault.vy index 7723a9b0..172a451d 100644 --- a/contracts/lending/Vault.vy +++ b/contracts/lending/Vault.vy @@ -9,7 +9,7 @@ """ from ethereum.ercs import IERC20 -from ethereum.ercs import IERC20Detailed +from ethereum.ercs import IERC20Detailed from ethereum.ercs import IERC4626 from contracts.interfaces import IAMM @@ -23,6 +23,11 @@ implements: IERC20Detailed # implements: IERC4626 +interface IERC20Custom: + def decimals() -> uint256: view + def symbol() -> String[32]: view + + event SetMaxSupply: max_supply: uint256 @@ -89,7 +94,7 @@ def initialize( assert self.borrowed_token.address == empty(address) self.borrowed_token = borrowed_token - borrowed_precision: uint256 = 10**(18 - convert(staticcall IERC20Detailed(borrowed_token.address).decimals(), uint256)) + borrowed_precision: uint256 = 10**(18 - (staticcall IERC20Custom(borrowed_token.address).decimals())) self.collateral_token = collateral_token self.factory = IFactory(msg.sender) @@ -98,7 +103,7 @@ def initialize( # ERC20 set up self.precision = borrowed_precision - borrowed_symbol: String[32] = staticcall IERC20Detailed(borrowed_token.address).symbol() + borrowed_symbol: String[32] = staticcall IERC20Custom(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 From 0ccea7a2a8fc8020e93aea00691504273799ccd3 Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 7 Aug 2025 13:21:45 +0400 Subject: [PATCH 081/413] fix: give approval to vault for LLController --- contracts/Controller.vy | 5 ----- contracts/MintController.vy | 2 ++ contracts/lending/LLController.vy | 4 +++- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 45b5708b..ce94d9d0 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -159,11 +159,6 @@ def __init__( self.loan_discount = loan_discount self._total_debt.rate_mul = 10**18 - # TODO check what this is needed for - assert extcall BORROWED_TOKEN.approve( - msg.sender, max_value(uint256), default_return_value=True - ) - @view @external diff --git a/contracts/MintController.vy b/contracts/MintController.vy index 9d36afc0..2cadb997 100644 --- a/contracts/MintController.vy +++ b/contracts/MintController.vy @@ -34,3 +34,5 @@ def __init__( liquidation_discount, amm, ) + + assert extcall core.BORROWED_TOKEN.approve(core.FACTORY, max_value(uint256), default_return_value=True) diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy index 99adcb3d..2cdd4b91 100644 --- a/contracts/lending/LLController.vy +++ b/contracts/lending/LLController.vy @@ -95,8 +95,8 @@ MAX_ADMIN_FEE: constant(uint256) = 2 * 10**17 # 20% def __init__( vault: IVault, amm: IAMM, - collateral_token: IERC20, borrowed_token: IERC20, + collateral_token: IERC20, monetary_policy: IMonetaryPolicy, loan_discount: uint256, liquidation_discount: uint256, @@ -121,6 +121,8 @@ def __init__( amm, ) + assert extcall core.BORROWED_TOKEN.approve(VAULT.address, max_value(uint256), default_return_value=True) + @external @view From d9e6b001dc92f5c410ee80e94eecaa11794057fc Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 7 Aug 2025 13:23:10 +0400 Subject: [PATCH 082/413] fix: p_oracle_up can touch price_oracle --- contracts/Controller.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index ce94d9d0..0caf1474 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -423,7 +423,7 @@ def _calculate_debt_n1( assert staticcall AMM.can_skip_bands(n1 - 1), "Debt too high" assert ( - staticcall AMM.p_oracle_up(n1) < staticcall AMM.price_oracle() + staticcall AMM.p_oracle_up(n1) <= staticcall AMM.price_oracle() ), "Debt too high" return n1 From c5a74415db9d0ec23d12ef430b605b7f4182170b Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 7 Aug 2025 13:28:30 +0400 Subject: [PATCH 083/413] fix: take from wallet no more than required amount in repay --- contracts/Controller.vy | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 0caf1474..dbfe563f 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -964,14 +964,19 @@ def repay( if callbacker == empty(address): xy = extcall AMM.withdraw(_for, 10**18) + total_stablecoins = 0 if xy[0] > 0: # Only allow full repayment when underwater for the sender to do assert approval self.transferFrom(BORROWED_TOKEN, AMM.address, self, xy[0]) + total_stablecoins += xy[0] if cb.stablecoins > 0: self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins) - if _d_debt > 0: - self.transferFrom(BORROWED_TOKEN, msg.sender, self, _d_debt) + total_stablecoins += cb.stablecoins + if total_stablecoins < d_debt: + _d_debt_effective: uint256 = unsafe_sub(d_debt, xy[0] + cb.stablecoins) # <= _d_debt + self.transferFrom(BORROWED_TOKEN, msg.sender, self, _d_debt_effective) + total_stablecoins += _d_debt_effective if total_stablecoins > d_debt: self.transfer( From 717030617b89595d45e3b0f8cf79faacaed07008 Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 7 Aug 2025 13:39:21 +0400 Subject: [PATCH 084/413] fix: default liquidate args --- contracts/Controller.vy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index dbfe563f..8e760e49 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -1209,9 +1209,9 @@ def _get_f_remove(frac: uint256, health_limit: uint256) -> uint256: def liquidate( user: address, min_x: uint256, - _frac: uint256, - callbacker: address, - calldata: Bytes[10**4], + _frac: uint256 = 10 ** 18, + callbacker: address = empty(address), + calldata: Bytes[10 ** 4] = b"" ): """ @notice Perform a bad liquidation (or self-liquidation) of user if health is not good From ad11b85fecfd5c27fb956779948b3dd78578daad Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 7 Aug 2025 15:02:44 +0400 Subject: [PATCH 085/413] fix: LOGN_A_RATIO --- contracts/Controller.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 8e760e49..d0f7b41a 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -136,7 +136,7 @@ def __init__( Aminus1 = A - 1 # TODO check math (removed unsafe) - LOGN_A_RATIO = math._wad_ln(convert(A * WAD // A - 1, int256)) + LOGN_A_RATIO = math._wad_ln(convert(A * WAD // (A - 1), int256)) # TODO check math SQRT_BAND_RATIO = isqrt(10**36 * A // (A - 1)) From 87ae004c7c6bd7c11ae239b8d87f78ed7f310ad5 Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 8 Aug 2025 15:37:19 +0200 Subject: [PATCH 086/413] feat: make price oracle settable --- .gitignore | 2 +- .python-version | 1 + contracts/AMM.vy | 39 ++++++++++++++++++++++------------- contracts/Controller.vy | 33 +++++++++++++++++++++++++---- contracts/interfaces/IAMM.vyi | 9 ++++++++ 5 files changed, 65 insertions(+), 19 deletions(-) create mode 100644 .python-version diff --git a/.gitignore b/.gitignore index 44ca38f9..9cfa3794 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ venv /hardhat.config.js .idea -.python-version .env /.env-* /.py312 @@ -21,3 +20,4 @@ venv .coverage .claude .vscode +.venv 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/contracts/AMM.vy b/contracts/AMM.vy index 9a554c3a..91506c11 100644 --- a/contracts/AMM.vy +++ b/contracts/AMM.vy @@ -1,4 +1,4 @@ -# pragma version 0.4.1 +# pragma version 0.4.3 """ @title LLAMMA - crvUSD AMM @author Curve.Fi @@ -77,14 +77,13 @@ active_band: public(int256) min_band: public(int256) max_band: public(int256) -# TODO setter -_price_oracle_contract: immutable(IPriceOracle) +_price_oracle_contract: IPriceOracle -# TODO This is a workaround for a compiler bug +# https://github.com/vyperlang/vyper/issues/4721 @view @external def price_oracle_contract() -> IPriceOracle: - return _price_oracle_contract + return self._price_oracle_contract old_p_o: uint256 @@ -112,9 +111,9 @@ def liquidity_mining_callback() -> ILMGauge: @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, @@ -122,7 +121,7 @@ def __init__( _base_price: uint256, fee: uint256, admin_fee: uint256, - price_oracle_contract: address, + price_oracle_contract: IPriceOracle, ): """ @notice LLAMMA constructor @@ -138,9 +137,9 @@ def __init__( @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 = IERC20(_borrowed_token) + BORROWED_TOKEN = _borrowed_token BORROWED_PRECISION = _borrowed_precision - COLLATERAL_TOKEN = IERC20(_collateral_token) + COLLATERAL_TOKEN = _collateral_token COLLATERAL_PRECISION = _collateral_precision A = _A BASE_PRICE = _base_price @@ -150,9 +149,9 @@ def __init__( Aminus12 = pow_mod256(unsafe_sub(A, 1), 2) self.fee = fee - _price_oracle_contract = IPriceOracle(price_oracle_contract) + self._price_oracle_contract = price_oracle_contract self.prev_p_o_time = block.timestamp - self.old_p_o = staticcall _price_oracle_contract.price() + self.old_p_o = staticcall self._price_oracle_contract.price() self.rate_mul = 10**18 @@ -275,12 +274,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(staticcall _price_oracle_contract.price()) + return self.limit_p_o(staticcall self._price_oracle_contract.price()) @internal def _price_oracle_w() -> uint256[2]: - p: uint256[2] = self.limit_p_o(extcall _price_oracle_contract.price_w()) + p: uint256[2] = self.limit_p_o(extcall self._price_oracle_contract.price_w()) self.prev_p_o_time = block.timestamp self.old_p_o = p[0] self.old_dfee = p[1] @@ -1702,3 +1701,15 @@ def set_callback(liquidity_mining_callback: ILMGauge): """ assert msg.sender == self.admin self._liquidity_mining_callback = liquidity_mining_callback + + +@external +@nonreentrant +def set_price_oracle(_price_oracle: IPriceOracle): + """ + @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._price_oracle_contract = _price_oracle + log IAMM.SetPriceOracle(price_oracle=_price_oracle) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index d0f7b41a..49f52ae3 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -11,6 +11,7 @@ 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 ethereum.ercs import IERC20 from ethereum.ercs import IERC20Detailed @@ -75,6 +76,7 @@ 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 ################################################################ # STORAGE # @@ -83,8 +85,8 @@ MAX_RATE: constant(uint256) = 43959106799 # 300% APY liquidation_discount: public(uint256) loan_discount: public(uint256) _monetary_policy: IMonetaryPolicy -# TODO can't mark it as public, likely a compiler bug -# TODO make an issue + +# https://github.com/vyperlang/vyper/issues/4721 @external @view def monetary_policy() -> IMonetaryPolicy: @@ -135,9 +137,7 @@ def __init__( A = staticcall AMM.A() Aminus1 = A - 1 - # TODO check math (removed unsafe) LOGN_A_RATIO = math._wad_ln(convert(A * WAD // (A - 1), int256)) - # TODO check math SQRT_BAND_RATIO = isqrt(10**36 * A // (A - 1)) MAX_AMM_FEE = min(WAD * MIN_TICKS_UINT // A, 10**17) @@ -213,6 +213,31 @@ def _update_total_debt( return loan +@external +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 + new_price: uint256 = extcall price_oracle.price_w() + + # 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, "deviation>max" + + extcall AMM.set_price_oracle(price_oracle) + + @external @view def factory() -> IFactory: diff --git a/contracts/interfaces/IAMM.vyi b/contracts/interfaces/IAMM.vyi index 54f4d854..59cf3678 100644 --- a/contracts/interfaces/IAMM.vyi +++ b/contracts/interfaces/IAMM.vyi @@ -49,6 +49,10 @@ event SetFee: fee: uint256 +event SetPriceOracle: + price_oracle: IPriceOracle + + # Functions @external @@ -229,6 +233,11 @@ def set_callback(liquidity_mining_callback: ILMGauge): ... +@external +def set_price_oracle(_price_oracle: IPriceOracle): + ... + + @view @external def admin() -> address: From ed92cfc3d3abd0f9a535a90b3972fe14b6eebf8e Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 8 Aug 2025 17:41:02 +0200 Subject: [PATCH 087/413] test: improve unit tests architecture --- .gitignore | 2 + .../lp-oracles/LPOracleCrypto.vy | 2 +- .../lp-oracles/LPOracleFactory.vy | 2 +- .../lp-oracles/LPOracleStable.vy | 2 +- .../price_oracles/lp-oracles/lp_oracle_lib.vy | 3 +- contracts/price_oracles/proxy/ProxyOracle.vy | 2 +- .../price_oracles/proxy/ProxyOracleFactory.vy | 2 +- pyproject.toml | 12 +- tests/amm/conftest.py | 3 +- tests/amm/test_price_oracles.py | 3 +- tests/boosted_lm_callback/conftest.py | 41 ++++--- tests/conftest.py | 5 +- tests/flashloan/conftest.py | 22 ++-- tests/lending/conftest.py | 28 +++-- tests/lending/test_bigfuzz.py | 4 +- tests/lending/test_oracle_attack.py | 3 +- tests/lending/test_pool_price_oracle.py | 11 +- tests/lending/test_secondary_mpolicy.py | 14 ++- tests/lending/test_vault.py | 1 + tests/lm_callback/conftest.py | 38 +++++-- tests/lm_callback/test_add_new_lm_callback.py | 6 +- tests/lm_callback/test_lm_callback_reverts.py | 6 +- tests/price_oracles/lp-oracles/conftest.py | 36 ++++-- .../lp-oracles/test_lp_oracle.py | 2 +- .../lp-oracles/test_lp_oracle_factory.py | 12 +- tests/price_oracles/proxy/conftest.py | 14 ++- tests/price_oracles/proxy/test_proxy.py | 6 +- tests/stableborrow/conftest.py | 23 ++-- tests/stableborrow/stabilize/conftest.py | 63 ++++++----- .../stateful/test_agg_monetary_policy.py | 21 ++-- .../unitary/test_agg_monetary_policy_3.py | 19 ++-- .../unitary/test_pk_admin_functions.py | 3 +- .../stabilize/unitary/test_pk_regulator.py | 9 +- tests/stableborrow/test_bigfuzz.py | 3 +- tests/stableborrow/test_liquidate.py | 3 +- tests/stableborrow/test_lm_callback.py | 3 +- tests/swap/conftest.py | 16 ++- tests/test_math.py | 3 +- tests/test_packing.py | 3 +- tests/utils/__init__.py | 0 tests/utils/constants.py | 3 + tests/utils/deployers.py | 104 ++++++++++++++++++ uv.lock | 49 +++++++-- 43 files changed, 438 insertions(+), 169 deletions(-) create mode 100644 tests/utils/__init__.py create mode 100644 tests/utils/constants.py create mode 100644 tests/utils/deployers.py diff --git a/.gitignore b/.gitignore index 9cfa3794..1d71aec1 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ venv .claude .vscode .venv +prof +.prof \ No newline at end of file diff --git a/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy b/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy index dd285c93..7c4eeabc 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy @@ -1,4 +1,4 @@ -# @version 0.4.1 +# @version 0.4.3 #pragma optimize gas #pragma evm-version shanghai diff --git a/contracts/price_oracles/lp-oracles/LPOracleFactory.vy b/contracts/price_oracles/lp-oracles/LPOracleFactory.vy index 84f74d75..acf2ac5b 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleFactory.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleFactory.vy @@ -1,4 +1,4 @@ -# @version 0.4.1 +# @version 0.4.3 #pragma optimize gas #pragma evm-version shanghai diff --git a/contracts/price_oracles/lp-oracles/LPOracleStable.vy b/contracts/price_oracles/lp-oracles/LPOracleStable.vy index fb744e95..4dc14906 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleStable.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleStable.vy @@ -1,4 +1,4 @@ -# @version 0.4.1 +# @version 0.4.3 #pragma optimize gas #pragma evm-version shanghai diff --git a/contracts/price_oracles/lp-oracles/lp_oracle_lib.vy b/contracts/price_oracles/lp-oracles/lp_oracle_lib.vy index 4e281f57..2882bd88 100644 --- a/contracts/price_oracles/lp-oracles/lp_oracle_lib.vy +++ b/contracts/price_oracles/lp-oracles/lp_oracle_lib.vy @@ -1,8 +1,9 @@ -# @version 0.4.1 +# @version 0.4.3 #pragma optimize gas #pragma evm-version shanghai +# TODO use vyi interfaces interface PriceOracle: def price() -> uint256: view def price_w() -> uint256: nonpayable diff --git a/contracts/price_oracles/proxy/ProxyOracle.vy b/contracts/price_oracles/proxy/ProxyOracle.vy index b24220c6..ede9cc52 100644 --- a/contracts/price_oracles/proxy/ProxyOracle.vy +++ b/contracts/price_oracles/proxy/ProxyOracle.vy @@ -1,4 +1,4 @@ -# @version 0.4.1 +# @version 0.4.3 #pragma optimize gas #pragma evm-version shanghai diff --git a/contracts/price_oracles/proxy/ProxyOracleFactory.vy b/contracts/price_oracles/proxy/ProxyOracleFactory.vy index af7d9c9b..126f076d 100644 --- a/contracts/price_oracles/proxy/ProxyOracleFactory.vy +++ b/contracts/price_oracles/proxy/ProxyOracleFactory.vy @@ -1,4 +1,4 @@ -# @version 0.4.1 +# @version 0.4.3 #pragma optimize gas #pragma evm-version shanghai diff --git a/pyproject.toml b/pyproject.toml index 9df5c90e..0f781f30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,14 +10,22 @@ authors = [ dependencies = [ "vyper==0.4.3", - "titanoboa>=0.2.7", + "snekmate>=0.1.1", +] + +[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", - "snekmate>=0.1.1", + "pytest-profiling>=1.8.1", ] +[tool.uv.sources] +titanoboa = { git = "https://github.com/vyperlang/titanoboa", rev = "a508f2adac3c05a5bfcc09b7d64f9be46c0bab17" } + [tool.uv] package = false # This is not a Python package diff --git a/tests/amm/conftest.py b/tests/amm/conftest.py index 647912a0..eec74352 100644 --- a/tests/amm/conftest.py +++ b/tests/amm/conftest.py @@ -1,6 +1,7 @@ import boa import pytest from math import sqrt, log +from tests.utils.deployers import AMM_DEPLOYER PRICE = 3000 @@ -14,7 +15,7 @@ def borrowed_token(get_borrowed_token): def get_amm(price_oracle, admin, accounts): def f(collateral_token, borrowed_token): with boa.env.prank(admin): - amm = boa.load('contracts/AMM.vy', + 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), diff --git a/tests/amm/test_price_oracles.py b/tests/amm/test_price_oracles.py index aab50dec..1acd877a 100644 --- a/tests/amm/test_price_oracles.py +++ b/tests/amm/test_price_oracles.py @@ -1,6 +1,7 @@ import boa import pytest from ..conftest import PRICE, approx +from tests.utils.deployers import EMA_PRICE_ORACLE_DEPLOYER @pytest.fixture(scope="module") @@ -8,7 +9,7 @@ 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) + return EMA_PRICE_ORACLE_DEPLOYER.deploy(10000, price_oracle.address, signature) def test_price_oracle(price_oracle, amm): diff --git a/tests/boosted_lm_callback/conftest.py b/tests/boosted_lm_callback/conftest.py index 715a69ce..6abc7ef6 100644 --- a/tests/boosted_lm_callback/conftest.py +++ b/tests/boosted_lm_callback/conftest.py @@ -1,35 +1,50 @@ import boa import pytest +from tests.utils.deployers import ( + ERC20_CRV_DEPLOYER, + VOTING_ESCROW_DEPLOYER, + VE_DELEGATION_MOCK_DEPLOYER, + GAUGE_CONTROLLER_DEPLOYER, + MINTER_DEPLOYER, + STABLECOIN_DEPLOYER, + WETH_DEPLOYER, + CONTROLLER_FACTORY_DEPLOYER, + CONTROLLER_DEPLOYER, + AMM_DEPLOYER, + CONSTANT_MONETARY_POLICY_DEPLOYER, + BOOSTED_LM_CALLBACK_DEPLOYER, + BLOCK_COUNTER_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 voting_escrow_delegation_mock(admin, voting_escrow): with boa.env.prank(admin): - return boa.load('contracts/testing/VEDelegationMock.vy', voting_escrow) + return VE_DELEGATION_MOCK_DEPLOYER.deploy(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) + return GAUGE_CONTROLLER_DEPLOYER.deploy(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) + return MINTER_DEPLOYER.deploy(crv, gauge_controller) # Trader @@ -44,7 +59,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,18 +68,18 @@ 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, accounts[0], weth.address) + return CONTROLLER_FACTORY_DEPLOYER.deploy(stablecoin.address, admin, accounts[0], weth.address) @pytest.fixture(scope="module") def controller_interface(): - return boa.load_partial('contracts/Controller.vy') + return CONTROLLER_DEPLOYER @pytest.fixture(scope="module") @@ -75,7 +90,7 @@ def controller_impl(controller_prefactory, controller_interface, admin): @pytest.fixture(scope="module") def amm_interface(): - return boa.load_partial('contracts/AMM.vy') + return AMM_DEPLOYER @pytest.fixture(scope="module") @@ -95,7 +110,7 @@ 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 @@ -144,7 +159,7 @@ def market_controller(market, stablecoin, collateral_token, controller_interface 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, + cb = BOOSTED_LM_CALLBACK_DEPLOYER.deploy(market_amm, crv, voting_escrow, voting_escrow_delegation_mock, gauge_controller, minter) market_controller.set_callback(cb) @@ -154,4 +169,4 @@ def boosted_lm_callback(admin, market_amm, crv, voting_escrow, voting_escrow_del @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/conftest.py b/tests/conftest.py index 004dc28e..4628cfff 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,7 @@ import boa import pytest from hypothesis import settings +from tests.utils.deployers import ERC20_MOCK_DEPLOYER, DUMMY_PRICE_ORACLE_DEPLOYER boa.env.enable_fast_mode() @@ -45,7 +46,7 @@ def admin(): @pytest.fixture(scope="session") def token_mock(): - return boa.load_partial('contracts/testing/ERC20Mock.vy') + return ERC20_MOCK_DEPLOYER @pytest.fixture(scope="session") @@ -72,5 +73,5 @@ def collateral_token(get_collateral_token): @pytest.fixture(scope="session") def price_oracle(admin): with boa.env.prank(admin): - oracle = boa.load('contracts/testing/DummyPriceOracle.vy', admin, PRICE * 10**18) + oracle = DUMMY_PRICE_ORACLE_DEPLOYER.deploy(admin, PRICE * 10**18) return oracle diff --git a/tests/flashloan/conftest.py b/tests/flashloan/conftest.py index 70946669..27ad1c52 100644 --- a/tests/flashloan/conftest.py +++ b/tests/flashloan/conftest.py @@ -1,10 +1,18 @@ import boa import pytest +from tests.utils.deployers import ( + STABLECOIN_DEPLOYER, + WETH_DEPLOYER, + CONTROLLER_FACTORY_DEPLOYER, + FLASH_LENDER_DEPLOYER, + AMM_DEPLOYER, + DUMMY_FLASH_BORROWER_DEPLOYER +) @pytest.fixture(scope="session") def stablecoin_pre(): - return boa.load_partial('contracts/Stablecoin.vy') + return STABLECOIN_DEPLOYER @pytest.fixture(scope="module") @@ -18,12 +26,12 @@ def stablecoin(stablecoin_pre, admin): @pytest.fixture(scope="session") 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") @@ -34,7 +42,7 @@ def controller_prefactory(controller_factory_impl, stablecoin, weth, admin, acco @pytest.fixture(scope="session") def controller_interface(): - return boa.load_partial('contracts/flashloan/FlashLender.vy') + return FLASH_LENDER_DEPLOYER @pytest.fixture(scope="session") @@ -45,7 +53,7 @@ def controller_impl(controller_interface, admin): @pytest.fixture(scope="session") def amm_interface(): - return boa.load_partial('contracts/AMM.vy') + return AMM_DEPLOYER @pytest.fixture(scope="session") @@ -75,7 +83,7 @@ def max_flash_loan(): @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 @@ -84,4 +92,4 @@ def flash_lender(controller_factory, admin, max_flash_loan): @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/lending/conftest.py b/tests/lending/conftest.py index 8c929926..e842225c 100644 --- a/tests/lending/conftest.py +++ b/tests/lending/conftest.py @@ -1,11 +1,21 @@ import boa import pytest from itertools import product +from tests.utils.deployers import ( + AMM_DEPLOYER, + LL_CONTROLLER_DEPLOYER, + VAULT_DEPLOYER, + CRYPTO_FROM_POOL_DEPLOYER, + SEMILOG_MONETARY_POLICY_DEPLOYER, + LENDING_FACTORY_DEPLOYER, + ERC20_MOCK_DEPLOYER, + FAKE_LEVERAGE_DEPLOYER +) @pytest.fixture(scope="session") def amm_interface(): - return boa.load_partial('contracts/AMM.vy') + return AMM_DEPLOYER @pytest.fixture(scope="session") @@ -16,7 +26,7 @@ def amm_impl(amm_interface, admin): @pytest.fixture(scope="session") def controller_interface(): - return boa.load_partial('contracts/lending/LLController.vy') + return LL_CONTROLLER_DEPLOYER @pytest.fixture(scope="session") @@ -32,18 +42,18 @@ def stablecoin(get_borrowed_token): @pytest.fixture(scope="session") def vault_interface(): - return boa.load_partial('contracts/lending/Vault.vy') + return VAULT_DEPLOYER @pytest.fixture(scope="session") def vault_impl(admin): with boa.env.prank(admin): - return boa.load('contracts/lending/Vault.vy') + return VAULT_DEPLOYER.deploy() @pytest.fixture(scope="session") def price_oracle_interface(): - return boa.load_partial('contracts/price_oracles/CryptoFromPool.vy') + return CRYPTO_FROM_POOL_DEPLOYER @pytest.fixture(scope="session") @@ -54,7 +64,7 @@ def price_oracle_impl(price_oracle_interface, admin): @pytest.fixture(scope="session") def mpolicy_interface(): - return boa.load_partial('contracts/mpolicies/SemilogMonetaryPolicy.vy') + return SEMILOG_MONETARY_POLICY_DEPLOYER @pytest.fixture(scope="session") @@ -65,7 +75,7 @@ def mpolicy_impl(mpolicy_interface, admin): @pytest.fixture(scope="session") def factory_partial(): - return boa.load_partial('contracts/lending/LendingFactory.vy') + return LENDING_FACTORY_DEPLOYER @pytest.fixture(scope="module") @@ -136,7 +146,7 @@ def market_mpolicy(market_controller, mpolicy_interface): @pytest.fixture(scope="session") def mock_token_interface(): - return boa.load_partial('contracts/testing/ERC20Mock.vy') + return ERC20_MOCK_DEPLOYER @pytest.fixture(scope="module") @@ -152,7 +162,7 @@ def filled_controller(vault, borrowed_token, market_controller, admin): @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, + leverage = FAKE_LEVERAGE_DEPLOYER.deploy(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 diff --git a/tests/lending/test_bigfuzz.py b/tests/lending/test_bigfuzz.py index 5a8c2c13..b27484b4 100644 --- a/tests/lending/test_bigfuzz.py +++ b/tests/lending/test_bigfuzz.py @@ -5,6 +5,8 @@ from hypothesis import strategies as st from hypothesis.stateful import RuleBasedStateMachine, run_state_machine_as_test, rule, invariant +from tests.utils.constants import ZERO_ADDRESS + # Variables and methods to check # * A @@ -15,8 +17,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% diff --git a/tests/lending/test_oracle_attack.py b/tests/lending/test_oracle_attack.py index 0f8fd8de..0d3c1fcb 100644 --- a/tests/lending/test_oracle_attack.py +++ b/tests/lending/test_oracle_attack.py @@ -12,6 +12,7 @@ from hypothesis import given, settings from hypothesis import strategies as st +from tests.utils.deployers import OLD_AMM_DEPLOYER MAX = 2**256 - 1 @@ -49,7 +50,7 @@ def factory_new(factory_partial, stablecoin, amm_impl, controller_impl, vault_im @pytest.fixture(scope="module") def amm_old_interface(): - return boa.load_partial('contracts/testing/OldAMM.vy') + return OLD_AMM_DEPLOYER @pytest.fixture(scope="module") diff --git a/tests/lending/test_pool_price_oracle.py b/tests/lending/test_pool_price_oracle.py index 9addc500..4c503c3f 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..cd3c7833 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,17 +21,17 @@ @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) + return MOCK_RATE_SETTER_DEPLOYER.deploy(RATE0) @pytest.fixture(scope="module") @@ -35,7 +41,7 @@ def borrowed_token(get_borrowed_token): @pytest.fixture(scope="module") def mp(factory, amm, borrowed_token): - return boa.load('contracts/mpolicies/SecondaryMonetaryPolicy.vy', factory, amm, borrowed_token, + return SECONDARY_MONETARY_POLICY_DEPLOYER.deploy(factory, amm, borrowed_token, int(0.85 * 1e18), int(0.5 * 1e18), int(3 * 1e18), 0) diff --git a/tests/lending/test_vault.py b/tests/lending/test_vault.py index 1b16caa4..e2f2f5ee 100644 --- a/tests/lending/test_vault.py +++ b/tests/lending/test_vault.py @@ -6,6 +6,7 @@ from ..conftest import approx +# TODO get this from contract directly DEAD_SHARES = 1000 diff --git a/tests/lm_callback/conftest.py b/tests/lm_callback/conftest.py index cf9daf05..6f7c04af 100644 --- a/tests/lm_callback/conftest.py +++ b/tests/lm_callback/conftest.py @@ -1,23 +1,37 @@ 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, + CONTROLLER_DEPLOYER, + AMM_DEPLOYER, + CONSTANT_MONETARY_POLICY_DEPLOYER, + LM_CALLBACK_DEPLOYER, + BLOCK_COUNTER_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) @@ -27,7 +41,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 @@ -44,7 +58,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,18 +67,18 @@ 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') + return CONTROLLER_DEPLOYER @pytest.fixture(scope="module") @@ -75,7 +89,7 @@ def controller_impl(controller_interface, admin): @pytest.fixture(scope="module") def amm_interface(): - return boa.load_partial('contracts/AMM.vy') + return AMM_DEPLOYER @pytest.fixture(scope="module") @@ -95,7 +109,7 @@ 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 @@ -143,7 +157,7 @@ def market_controller(market, stablecoin, collateral_token, controller_interface @pytest.fixture(scope="module") 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) @@ -154,4 +168,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..c5b71b01 100644 --- a/tests/lm_callback/test_add_new_lm_callback.py +++ b/tests/lm_callback/test_add_new_lm_callback.py @@ -1,4 +1,6 @@ import boa +from tests.utils.deployers import LM_CALLBACK_DEPLOYER +from tests.utils.constants import ZERO_ADDRESS WEEK = 7 * 86400 @@ -17,7 +19,7 @@ def test_add_new_lm_callback( 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) @@ -29,7 +31,7 @@ def test_add_new_lm_callback( # 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) diff --git a/tests/lm_callback/test_lm_callback_reverts.py b/tests/lm_callback/test_lm_callback_reverts.py index 26ed10a8..8d651fd0 100644 --- a/tests/lm_callback/test_lm_callback_reverts.py +++ b/tests/lm_callback/test_lm_callback_reverts.py @@ -1,4 +1,6 @@ import boa +from tests.utils.deployers import LM_CALLBACK_WITH_REVERTS_DEPLOYER +from tests.utils.constants import ZERO_ADDRESS WEEK = 7 * 86400 @@ -16,7 +18,7 @@ def test_add_new_lm_callback( 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) @@ -39,7 +41,7 @@ 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) diff --git a/tests/price_oracles/lp-oracles/conftest.py b/tests/price_oracles/lp-oracles/conftest.py index 7e7947df..af227a9d 100644 --- a/tests/price_oracles/lp-oracles/conftest.py +++ b/tests/price_oracles/lp-oracles/conftest.py @@ -1,6 +1,18 @@ 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): @@ -10,13 +22,13 @@ def user(accounts): @pytest.fixture(scope="module") def broken_contract(admin): with boa.env.prank(admin): - return boa.load('contracts/testing/WETH.vy') + return WETH_DEPLOYER.deploy() @pytest.fixture(scope="module") def coin0_oracle(admin): with boa.env.prank(admin): - return boa.load('contracts/testing/DummyPriceOracle.vy', admin, random.randint(99 * 10**17, 101 * 10**17)) + return DUMMY_PRICE_ORACLE_DEPLOYER.deploy(admin, random.randint(99 * 10**17, 101 * 10**17)) @pytest.fixture(scope="module") @@ -24,7 +36,7 @@ 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 boa.load('contracts/price_oracles/lp-oracles/testing/MockStableSwap.vy', admin, prices) + return MOCK_STABLE_SWAP_DEPLOYER.deploy(admin, prices) return f @@ -32,50 +44,50 @@ def f(N): @pytest.fixture(scope="module") def crypto_swap(admin): with boa.env.prank(admin): - return boa.load('contracts/price_oracles/lp-oracles/testing/MockCryptoSwap.vy', admin, random.randint(10**16, 10**23)) + 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 boa.load('contracts/price_oracles/lp-oracles/testing/MockStableSwapNoArgument.vy', admin, random.randint(10**16, 10**23)) + 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 boa.load('contracts/price_oracles/proxy/ProxyOracle.vy') + return PROXY_ORACLE_DEPLOYER.deploy() @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) + 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 boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleStable.vy').deploy_as_blueprint() + return LP_ORACLE_STABLE_DEPLOYER.deploy_as_blueprint() @pytest.fixture(scope="module") def lp_oracle_crypto_impl(admin): with boa.env.prank(admin): - return boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleCrypto.vy').deploy_as_blueprint() + 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 boa.load('contracts/price_oracles/lp-oracles/LPOracleFactory.vy', admin, lp_oracle_stable_impl, lp_oracle_crypto_impl, proxy_factory) + 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 boa.load('contracts/price_oracles/lp-oracles/LPOracleStable.vy', pool, coin0_oracle) + return LP_ORACLE_STABLE_DEPLOYER.deploy(pool, coin0_oracle) return f @@ -84,6 +96,6 @@ def f(pool, coin0_oracle): def get_lp_oracle_crypto(admin): def f(pool, coin0_oracle): with boa.env.prank(admin): - return boa.load('contracts/price_oracles/lp-oracles/LPOracleCrypto.vy', pool, coin0_oracle) + 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 index af6b707e..e69ca061 100644 --- a/tests/price_oracles/lp-oracles/test_lp_oracle.py +++ b/tests/price_oracles/lp-oracles/test_lp_oracle.py @@ -1,6 +1,6 @@ import boa -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" +from tests.utils.constants import ZERO_ADDRESS def _get_lp_stable_price(stable_swap): diff --git a/tests/price_oracles/lp-oracles/test_lp_oracle_factory.py b/tests/price_oracles/lp-oracles/test_lp_oracle_factory.py index 7022241e..c74dbdde 100644 --- a/tests/price_oracles/lp-oracles/test_lp_oracle_factory.py +++ b/tests/price_oracles/lp-oracles/test_lp_oracle_factory.py @@ -1,6 +1,6 @@ import boa - -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" +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): @@ -33,7 +33,7 @@ def test_lp_oracle_stable_factory(lp_oracle_factory, proxy_factory, get_stable_s with boa.env.anchor(): oracle, proxy = lp_oracle_factory.deploy_oracle(stable_swap, coin0_oracle, False) - oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleStable.vy').at(oracle) + 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 @@ -41,7 +41,7 @@ def test_lp_oracle_stable_factory(lp_oracle_factory, proxy_factory, get_stable_s 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 = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleStable.vy').at(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 @@ -70,7 +70,7 @@ def test_lp_oracle_crypto_factory(lp_oracle_factory, proxy_factory, crypto_swap, with boa.env.anchor(): oracle, proxy = lp_oracle_factory.deploy_oracle(crypto_swap, coin0_oracle, False) - oracle = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleCrypto.vy').at(oracle) + 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 @@ -78,7 +78,7 @@ def test_lp_oracle_crypto_factory(lp_oracle_factory, proxy_factory, crypto_swap, 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 = boa.load_partial('contracts/price_oracles/lp-oracles/LPOracleCrypto.vy').at(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 diff --git a/tests/price_oracles/proxy/conftest.py b/tests/price_oracles/proxy/conftest.py index 2d22e38f..71538eb7 100644 --- a/tests/price_oracles/proxy/conftest.py +++ b/tests/price_oracles/proxy/conftest.py @@ -1,5 +1,11 @@ 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") @@ -11,7 +17,7 @@ def user(accounts): def get_price_oracle(admin): def f(price): with boa.env.prank(admin): - oracle = boa.load('contracts/testing/DummyPriceOracle.vy', admin, price) + oracle = DUMMY_PRICE_ORACLE_DEPLOYER.deploy(admin, price) return oracle return f @@ -20,17 +26,17 @@ def f(price): @pytest.fixture(scope="session") def broken_price_oracle(admin): with boa.env.prank(admin): - oracle = boa.load('contracts/testing/WETH.vy') + oracle = WETH_DEPLOYER.deploy() return oracle @pytest.fixture(scope="module") def proxy_impl(admin): with boa.env.prank(admin): - return boa.load('contracts/price_oracles/proxy/ProxyOracle.vy') + return PROXY_ORACLE_DEPLOYER.deploy() @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) + 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 index 4190cdfa..5d39b6b0 100644 --- a/tests/price_oracles/proxy/test_proxy.py +++ b/tests/price_oracles/proxy/test_proxy.py @@ -1,6 +1,6 @@ import boa - -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" +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): @@ -12,7 +12,7 @@ def test_proxy(proxy_factory, get_price_oracle, user, admin, 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 = boa.load_partial('contracts/price_oracles/proxy/ProxyOracle.vy').at(proxy_address) + proxy = PROXY_ORACLE_DEPLOYER.at(proxy_address) assert proxy.factory() == proxy_factory.address assert proxy.oracle() == oracle1.address diff --git a/tests/stableborrow/conftest.py b/tests/stableborrow/conftest.py index 34f40ce2..4f2f13f0 100644 --- a/tests/stableborrow/conftest.py +++ b/tests/stableborrow/conftest.py @@ -1,6 +1,15 @@ import boa import pytest from vyper.utils import method_id +from tests.utils.deployers import ( + STABLECOIN_DEPLOYER, + WETH_DEPLOYER, + CONTROLLER_FACTORY_DEPLOYER, + CONTROLLER_DEPLOYER, + AMM_DEPLOYER, + CONSTANT_MONETARY_POLICY_DEPLOYER, + FAKE_LEVERAGE_DEPLOYER +) def get_method_id(desc): @@ -9,7 +18,7 @@ def get_method_id(desc): @pytest.fixture(scope="session") def stablecoin_pre(): - return boa.load_partial('contracts/Stablecoin.vy') + return STABLECOIN_DEPLOYER @pytest.fixture(scope="module") @@ -21,12 +30,12 @@ def stablecoin(stablecoin_pre, admin): @pytest.fixture(scope="session") 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") @@ -37,7 +46,7 @@ def controller_prefactory(controller_factory_impl, stablecoin, weth, admin, acco @pytest.fixture(scope="session") def controller_interface(): - return boa.load_partial('contracts/Controller.vy') + return CONTROLLER_DEPLOYER @pytest.fixture(scope="session") @@ -48,7 +57,7 @@ def controller_impl(controller_interface, admin): @pytest.fixture(scope="session") def amm_interface(): - return boa.load_partial('contracts/AMM.vy') + return AMM_DEPLOYER @pytest.fixture(scope="session") @@ -68,7 +77,7 @@ def controller_factory(controller_prefactory, amm_impl, controller_impl, stablec @pytest.fixture(scope="session") 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 @@ -115,7 +124,7 @@ 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, + leverage = FAKE_LEVERAGE_DEPLOYER.deploy(stablecoin.address, collateral_token.address, market_controller.address, 3000 * 10**18) collateral_token._mint_for_testing(leverage.address, 1000 * 10**collateral_token.decimals()) return leverage diff --git a/tests/stableborrow/stabilize/conftest.py b/tests/stableborrow/stabilize/conftest.py index e654eec6..50f3221f 100644 --- a/tests/stableborrow/stabilize/conftest.py +++ b/tests/stableborrow/stabilize/conftest.py @@ -1,8 +1,26 @@ import boa import pytest from ...conftest import approx - -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" +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 +) +from tests.utils.constants import ZERO_ADDRESS BASE_AMOUNT = 10**6 @@ -35,31 +53,31 @@ def collateral_token(get_collateral_token): @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("USDa", "USDa", 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("USDb", "USDb", 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") @@ -67,17 +85,17 @@ 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) + 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 @@ -129,7 +147,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,7 +156,7 @@ 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', + pool = TRICRYPTO_MOCK_DEPLOYER.deploy( [stablecoin_a.address, "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"]) @@ -167,8 +185,7 @@ 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', + crypto_agg = CRYPTO_WITH_STABLE_PRICE_DEPLOYER.deploy( dummy_tricrypto.address, 0, stableswap_a, @@ -182,8 +199,7 @@ def crypto_agg(dummy_tricrypto, agg, stableswap_a, admin): @pytest.fixture(scope="module") 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, @@ -200,14 +216,13 @@ def crypto_agg_with_external_oracle(dummy_tricrypto, agg, stableswap_a, chainlin def mock_peg_keepers(stablecoin): """ 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): @@ -223,8 +238,7 @@ def peg_keepers(stablecoin_a, stablecoin_b, stableswap_a, stableswap_b, controll with boa.env.prank(admin): for (coin, pool) in [(stablecoin_a, stableswap_a), (stablecoin_b, stableswap_b)]: pks.append( - boa.load( - 'contracts/stabilizer/PegKeeperV2.vy', + PEG_KEEPER_V2_DEPLOYER.deploy( pool.address, 2 * 10**4, controller_factory.address, reg.address, admin) ) @@ -235,8 +249,7 @@ 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', + mp = AGG_MONETARY_POLICY2_DEPLOYER.deploy( admin, agg.address, controller_factory.address, @@ -398,4 +411,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/test_agg_monetary_policy.py b/tests/stableborrow/stabilize/stateful/test_agg_monetary_policy.py index c371847f..f524a3af 100644 --- a/tests/stableborrow/stabilize/stateful/test_agg_monetary_policy.py +++ b/tests/stableborrow/stabilize/stateful/test_agg_monetary_policy.py @@ -3,9 +3,14 @@ from hypothesis.stateful import RuleBasedStateMachine, run_state_machine_as_test, initialize, rule, invariant from boa.interpret import VyperContract import boa - - -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" +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 RATE0 = 634195839 # 2% @@ -18,9 +23,9 @@ class AggMonetaryPolicyCreation(RuleBasedStateMachine): 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 +37,8 @@ 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): 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..59c71a15 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,7 +27,7 @@ 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 @@ -32,8 +38,7 @@ 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, @@ -109,7 +114,7 @@ def test_add_controllers(mp, mock_factory, admin): 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) diff --git a/tests/stableborrow/stabilize/unitary/test_pk_admin_functions.py b/tests/stableborrow/stabilize/unitary/test_pk_admin_functions.py index 1a45211c..5d187b6e 100644 --- a/tests/stableborrow/stabilize/unitary/test_pk_admin_functions.py +++ b/tests/stableborrow/stabilize/unitary/test_pk_admin_functions.py @@ -1,7 +1,6 @@ import boa - -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" +from tests.utils.constants import ZERO_ADDRESS ADMIN_ACTIONS_DEADLINE = 3 * 86400 diff --git a/tests/stableborrow/stabilize/unitary/test_pk_regulator.py b/tests/stableborrow/stabilize/unitary/test_pk_regulator.py index ef932b62..1cf549fd 100644 --- a/tests/stableborrow/stabilize/unitary/test_pk_regulator.py +++ b/tests/stableborrow/stabilize/unitary/test_pk_regulator.py @@ -1,9 +1,8 @@ import boa import pytest from hypothesis import strategies as st, given - - -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" +from tests.utils.deployers import MOCK_PEG_KEEPER_DEPLOYER +from tests.utils.constants import ZERO_ADDRESS ADMIN_ACTIONS_DEADLINE = 3 * 86400 @@ -62,7 +61,7 @@ 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}") with boa.env.prank(admin): @@ -225,7 +224,7 @@ 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) ] diff --git a/tests/stableborrow/test_bigfuzz.py b/tests/stableborrow/test_bigfuzz.py index c0e14442..652674d6 100644 --- a/tests/stableborrow/test_bigfuzz.py +++ b/tests/stableborrow/test_bigfuzz.py @@ -6,6 +6,7 @@ from hypothesis import strategies as st from hypothesis.stateful import RuleBasedStateMachine, run_state_machine_as_test, rule, invariant +from tests.utils.constants import ZERO_ADDRESS # Variables and methods to check # * A @@ -16,8 +17,6 @@ # * set_debt_ceiling # * set_borrowing_discounts # * collect AMM fees - -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" USE_FRACTION = 1 USE_CALLBACKS = 2 diff --git a/tests/stableborrow/test_liquidate.py b/tests/stableborrow/test_liquidate.py index 7124da03..476c762b 100644 --- a/tests/stableborrow/test_liquidate.py +++ b/tests/stableborrow/test_liquidate.py @@ -4,6 +4,7 @@ from hypothesis import given, settings from hypothesis import strategies as st from ..conftest import approx +from tests.utils.constants import ZERO_ADDRESS N = 5 @@ -180,7 +181,7 @@ def test_tokens_to_liquidate(accounts, admin, controller_for_liquidation, market stablecoin.transfer(fee_receiver, 10**10) with boa.env.prank(fee_receiver): - controller.liquidate_extended(user, 0, frac, "0x0000000000000000000000000000000000000000", []) + controller.liquidate_extended(user, 0, frac, ZERO_ADDRESS, []) balance = stablecoin.balanceOf(fee_receiver) diff --git a/tests/stableborrow/test_lm_callback.py b/tests/stableborrow/test_lm_callback.py index ead4f74e..a5e719e6 100644 --- a/tests/stableborrow/test_lm_callback.py +++ b/tests/stableborrow/test_lm_callback.py @@ -2,12 +2,13 @@ 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 diff --git a/tests/swap/conftest.py b/tests/swap/conftest.py index a1932484..f8ee20ee 100644 --- a/tests/swap/conftest.py +++ b/tests/swap/conftest.py @@ -1,33 +1,37 @@ import boa import pytest from boa.interpret import VyperContract - -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" +from tests.utils.deployers import ( + STABLESWAP_DEPLOYER, + SWAP_FACTORY_DEPLOYER, + ERC20_MOCK_DEPLOYER +) +from tests.utils.constants import ZERO_ADDRESS @pytest.fixture(scope="session") def swap_impl(admin): with boa.env.prank(admin): - return boa.load('contracts/Stableswap.vy') + return STABLESWAP_DEPLOYER.deploy() @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) + deployer = SWAP_FACTORY_DEPLOYER.deploy(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) + return ERC20_MOCK_DEPLOYER.deploy("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) + return ERC20_MOCK_DEPLOYER.deploy("Volatile USD", "vUSD", 18) @pytest.fixture(scope="session") diff --git a/tests/test_math.py b/tests/test_math.py index 27488df0..eadb3251 100644 --- a/tests/test_math.py +++ b/tests/test_math.py @@ -3,6 +3,7 @@ from math import log2, sqrt, exp, log from hypothesis import given, settings from hypothesis import strategies as st +from tests.utils.deployers import OPTIMIZE_MATH_DEPLOYER SETTINGS = dict(max_examples=2000) @@ -10,7 +11,7 @@ @pytest.fixture(scope="module") def optimized_math(admin): with boa.env.prank(admin): - return boa.load('contracts/testing/OptimizeMath.vy') + return OPTIMIZE_MATH_DEPLOYER.deploy() @given(st.integers(min_value=0, max_value=2**256-1)) diff --git a/tests/test_packing.py b/tests/test_packing.py index 55c4b14f..a16a2e79 100644 --- a/tests/test_packing.py +++ b/tests/test_packing.py @@ -2,6 +2,7 @@ import pytest from hypothesis import strategies as st from hypothesis import given, settings +from tests.utils.deployers import TEST_PACKING_DEPLOYER MAX_N = 2**127 - 1 MIN_N = -2**127 + 1 # <- not -2**127! @@ -10,7 +11,7 @@ @pytest.fixture(scope="module") def packing(admin): with boa.env.prank(admin): - return boa.load('contracts/testing/TestPacking.vy') + return TEST_PACKING_DEPLOYER.deploy() @given( diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/utils/constants.py b/tests/utils/constants.py new file mode 100644 index 00000000..e5d7e4cb --- /dev/null +++ b/tests/utils/constants.py @@ -0,0 +1,3 @@ +import boa + +ZERO_ADDRESS = boa.eval("empty(address)") \ No newline at end of file diff --git a/tests/utils/deployers.py b/tests/utils/deployers.py new file mode 100644 index 00000000..b36a014c --- /dev/null +++ b/tests/utils/deployers.py @@ -0,0 +1,104 @@ +""" +Centralized deployers for all contracts used in tests. +Each deployer is a VyperDeployer object returned using boa.load_partial(). +""" + +import boa + +# Compiler args if needed +compiler_args = {} + +# 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/" + +# Core contracts +AMM_DEPLOYER = boa.load_partial(BASE_CONTRACT_PATH + "AMM.vy", compiler_args=compiler_args) +CONTROLLER_DEPLOYER = boa.load_partial(BASE_CONTRACT_PATH + "Controller.vy", compiler_args=compiler_args) +CONTROLLER_FACTORY_DEPLOYER = boa.load_partial(BASE_CONTRACT_PATH + "ControllerFactory.vy", compiler_args=compiler_args) +STABLECOIN_DEPLOYER = boa.load_partial(BASE_CONTRACT_PATH + "Stablecoin.vy", compiler_args=compiler_args) +STABLESWAP_DEPLOYER = boa.load_partial(BASE_CONTRACT_PATH + "Stableswap.vy", compiler_args=compiler_args) + +# Lending contracts +VAULT_DEPLOYER = boa.load_partial(LENDING_CONTRACT_PATH + "Vault.vy", compiler_args=compiler_args) +LL_CONTROLLER_DEPLOYER = boa.load_partial(LENDING_CONTRACT_PATH + "LLController.vy", compiler_args=compiler_args) +LENDING_FACTORY_DEPLOYER = boa.load_partial(LENDING_CONTRACT_PATH + "LendingFactory.vy", compiler_args=compiler_args) + +# Flashloan contracts +FLASH_LENDER_DEPLOYER = boa.load_partial(FLASHLOAN_CONTRACT_PATH + "FlashLender.vy", compiler_args=compiler_args) + +# Monetary policies +CONSTANT_MONETARY_POLICY_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "ConstantMonetaryPolicy.vy", compiler_args=compiler_args) +SEMILOG_MONETARY_POLICY_DEPLOYER = boa.load_partial(MPOLICIES_CONTRACT_PATH + "SemilogMonetaryPolicy.vy", compiler_args=compiler_args) +SECONDARY_MONETARY_POLICY_DEPLOYER = boa.load_partial(MPOLICIES_CONTRACT_PATH + "SecondaryMonetaryPolicy.vy", compiler_args=compiler_args) +AGG_MONETARY_POLICY2_DEPLOYER = boa.load_partial(MPOLICIES_CONTRACT_PATH + "AggMonetaryPolicy2.vy", compiler_args=compiler_args) +AGG_MONETARY_POLICY3_DEPLOYER = boa.load_partial(MPOLICIES_CONTRACT_PATH + "AggMonetaryPolicy3.vy", compiler_args=compiler_args) + +# Price oracles +DUMMY_PRICE_ORACLE_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "DummyPriceOracle.vy", compiler_args=compiler_args) +CRYPTO_FROM_POOL_DEPLOYER = boa.load_partial(PRICE_ORACLES_CONTRACT_PATH + "CryptoFromPool.vy", compiler_args=compiler_args) +EMA_PRICE_ORACLE_DEPLOYER = boa.load_partial(PRICE_ORACLES_CONTRACT_PATH + "EmaPriceOracle.vy", compiler_args=compiler_args) +AGGREGATE_STABLE_PRICE3_DEPLOYER = boa.load_partial(PRICE_ORACLES_CONTRACT_PATH + "AggregateStablePrice3.vy", compiler_args=compiler_args) +CRYPTO_WITH_STABLE_PRICE_DEPLOYER = boa.load_partial(PRICE_ORACLES_CONTRACT_PATH + "CryptoWithStablePrice.vy", compiler_args=compiler_args) +CRYPTO_WITH_STABLE_PRICE_AND_CHAINLINK_DEPLOYER = boa.load_partial(PRICE_ORACLES_CONTRACT_PATH + "CryptoWithStablePriceAndChainlink.vy", compiler_args=compiler_args) + +# Proxy oracle contracts +PROXY_ORACLE_DEPLOYER = boa.load_partial(PRICE_ORACLES_CONTRACT_PATH + "proxy/ProxyOracle.vy", compiler_args=compiler_args) +PROXY_ORACLE_FACTORY_DEPLOYER = boa.load_partial(PRICE_ORACLES_CONTRACT_PATH + "proxy/ProxyOracleFactory.vy", compiler_args=compiler_args) + +# LP oracle contracts +LP_ORACLE_STABLE_DEPLOYER = boa.load_partial(PRICE_ORACLES_CONTRACT_PATH + "lp-oracles/LPOracleStable.vy", compiler_args=compiler_args) +LP_ORACLE_CRYPTO_DEPLOYER = boa.load_partial(PRICE_ORACLES_CONTRACT_PATH + "lp-oracles/LPOracleCrypto.vy", compiler_args=compiler_args) +LP_ORACLE_FACTORY_DEPLOYER = boa.load_partial(PRICE_ORACLES_CONTRACT_PATH + "lp-oracles/LPOracleFactory.vy", compiler_args=compiler_args) + +# Stabilizer contracts +PEG_KEEPER_V2_DEPLOYER = boa.load_partial(STABILIZER_CONTRACT_PATH + "PegKeeperV2.vy", compiler_args=compiler_args) +PEG_KEEPER_REGULATOR_DEPLOYER = boa.load_partial(STABILIZER_CONTRACT_PATH + "PegKeeperRegulator.vy", compiler_args=compiler_args) + +# Callback contracts +LM_CALLBACK_DEPLOYER = boa.load_partial(BASE_CONTRACT_PATH + "LMCallback.vy", compiler_args=compiler_args) +BOOSTED_LM_CALLBACK_DEPLOYER = boa.load_partial(BASE_CONTRACT_PATH + "BoostedLMCallback.vy", compiler_args=compiler_args) + +# Testing/Mock contracts +ERC20_MOCK_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "ERC20Mock.vy", compiler_args=compiler_args) +ERC20_CRV_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "ERC20CRV.vy", compiler_args=compiler_args) +WETH_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "WETH.vy", compiler_args=compiler_args) +VOTING_ESCROW_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "VotingEscrow.vy", compiler_args=compiler_args) +VE_DELEGATION_MOCK_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "VEDelegationMock.vy", compiler_args=compiler_args) +GAUGE_CONTROLLER_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "GaugeController.vy", compiler_args=compiler_args) +MINTER_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "Minter.vy", compiler_args=compiler_args) +FAKE_LEVERAGE_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "FakeLeverage.vy", compiler_args=compiler_args) +BLOCK_COUNTER_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "BlockCounter.vy", compiler_args=compiler_args) +DUMMY_FLASH_BORROWER_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "DummyFlashBorrower.vy", compiler_args=compiler_args) +DUMMY_LM_CALLBACK_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "DummyLMCallback.vy", compiler_args=compiler_args) +LM_CALLBACK_WITH_REVERTS_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "LMCallbackWithReverts.vy", compiler_args=compiler_args) +MOCK_FACTORY_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "MockFactory.vy", compiler_args=compiler_args) +MOCK_MARKET_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "MockMarket.vy", compiler_args=compiler_args) +MOCK_RATE_SETTER_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "MockRateSetter.vy", compiler_args=compiler_args) +MOCK_PEG_KEEPER_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "MockPegKeeper.vy", compiler_args=compiler_args) +MOCK_RATE_ORACLE_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "MockRateOracle.vy", compiler_args=compiler_args) +CHAINLINK_AGGREGATOR_MOCK_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "ChainlinkAggregatorMock.vy", compiler_args=compiler_args) +TRICRYPTO_MOCK_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "TricryptoMock.vy", compiler_args=compiler_args) +MOCK_SWAP2_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "MockSwap2.vy", compiler_args=compiler_args) +MOCK_SWAP3_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "MockSwap3.vy", compiler_args=compiler_args) +SWAP_FACTORY_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "SwapFactory.vy", compiler_args=compiler_args) +OPTIMIZE_MATH_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "OptimizeMath.vy", compiler_args=compiler_args) +TEST_PACKING_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "TestPacking.vy", compiler_args=compiler_args) +OLD_AMM_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "OldAMM.vy", compiler_args=compiler_args) + +# LP oracle testing contracts +MOCK_STABLE_SWAP_DEPLOYER = boa.load_partial(PRICE_ORACLES_CONTRACT_PATH + "lp-oracles/testing/MockStableSwap.vy", compiler_args=compiler_args) +MOCK_CRYPTO_SWAP_DEPLOYER = boa.load_partial(PRICE_ORACLES_CONTRACT_PATH + "lp-oracles/testing/MockCryptoSwap.vy", compiler_args=compiler_args) +MOCK_STABLE_SWAP_NO_ARGUMENT_DEPLOYER = boa.load_partial(PRICE_ORACLES_CONTRACT_PATH + "lp-oracles/testing/MockStableSwapNoArgument.vy", compiler_args=compiler_args) + +# Stableswap NG contracts +CURVE_STABLESWAP_FACTORY_NG_DEPLOYER = boa.load_partial(STABLESWAP_NG_PATH + "CurveStableSwapFactoryNG.vy", compiler_args=compiler_args) +CURVE_STABLESWAP_NG_DEPLOYER = boa.load_partial(STABLESWAP_NG_PATH + "CurveStableSwapNG.vy", compiler_args=compiler_args) +CURVE_STABLESWAP_NG_MATH_DEPLOYER = boa.load_partial(STABLESWAP_NG_PATH + "CurveStableSwapNGMath.vy", compiler_args=compiler_args) +CURVE_STABLESWAP_NG_VIEWS_DEPLOYER = boa.load_partial(STABLESWAP_NG_PATH + "CurveStableSwapNGViews.vy", compiler_args=compiler_args) \ No newline at end of file diff --git a/uv.lock b/uv.lock index f1882f84..ead77f28 100644 --- a/uv.lock +++ b/uv.lock @@ -395,26 +395,36 @@ name = "curve-stablecoin" version = "0.2.0" source = { virtual = "." } dependencies = [ + { name = "snekmate" }, + { name = "vyper" }, +] + +[package.dev-dependencies] +dev = [ { name = "hypothesis" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-forked" }, + { name = "pytest-profiling" }, { name = "pytest-xdist" }, - { name = "snekmate" }, { name = "titanoboa" }, - { name = "vyper" }, ] [package.metadata] requires-dist = [ + { name = "snekmate", specifier = ">=0.1.1" }, + { name = "vyper", specifier = "==0.4.3" }, +] + +[package.metadata.requires-dev] +dev = [ { name = "hypothesis", specifier = ">=6.99.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 = "snekmate", specifier = ">=0.1.1" }, - { name = "titanoboa", specifier = ">=0.2.7" }, - { name = "vyper", specifier = "==0.4.3" }, + { name = "titanoboa", git = "https://github.com/vyperlang/titanoboa?rev=a508f2adac3c05a5bfcc09b7d64f9be46c0bab17" }, ] [[package]] @@ -665,6 +675,15 @@ 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" @@ -1291,6 +1310,20 @@ 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" @@ -1521,7 +1554,7 @@ wheels = [ [[package]] name = "titanoboa" version = "0.2.7" -source = { registry = "https://pypi.org/simple" } +source = { git = "https://github.com/vyperlang/titanoboa?rev=a508f2adac3c05a5bfcc09b7d64f9be46c0bab17#a508f2adac3c05a5bfcc09b7d64f9be46c0bab17" } dependencies = [ { name = "eth-abi" }, { name = "eth-account" }, @@ -1538,10 +1571,6 @@ dependencies = [ { name = "vvm" }, { name = "vyper" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/85/2cfd05b649cf173714ea7a2b3597228b7a577cab7fe4696e67879b8784b7/titanoboa-0.2.7.tar.gz", hash = "sha256:4fb931747bf9beb05ea39b149878bb7d89525bcfacf1a903e4d14da91f7c49de", size = 99921 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/fd/25ccd329bb68c2251b1fbb30aa799cfaca67ab262b2da05c5997e6b73e38/titanoboa-0.2.7-py3-none-any.whl", hash = "sha256:75771f5e8183c073f2c37d8925f583abd2c6b56eae4d087dbf1e33f809ea7ca5", size = 111582 }, -] [[package]] name = "tomli" From 921b2dbc05316829a90a25e94cdd9b4f352c6ae0 Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 8 Aug 2025 17:54:05 +0200 Subject: [PATCH 088/413] feat: more idiomatic vyper for price oracles --- contracts/interfaces/ICryptoPool.vyi | 3 +++ contracts/interfaces/IStablePool.vyi | 11 +++++++++ .../lp-oracles/LPOracleCrypto.vy | 20 +++++++--------- .../lp-oracles/LPOracleStable.vy | 24 ++++++++----------- .../price_oracles/lp-oracles/lp_oracle_lib.vy | 20 ++++++---------- 5 files changed, 39 insertions(+), 39 deletions(-) create mode 100644 contracts/interfaces/ICryptoPool.vyi create mode 100644 contracts/interfaces/IStablePool.vyi diff --git a/contracts/interfaces/ICryptoPool.vyi b/contracts/interfaces/ICryptoPool.vyi new file mode 100644 index 00000000..93b3d75e --- /dev/null +++ b/contracts/interfaces/ICryptoPool.vyi @@ -0,0 +1,3 @@ +@view +def lp_price() -> uint256: + ... \ No newline at end of file diff --git a/contracts/interfaces/IStablePool.vyi b/contracts/interfaces/IStablePool.vyi new file mode 100644 index 00000000..beca05bf --- /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: + ... \ No newline at end of file diff --git a/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy b/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy index 7c4eeabc..4eae8944 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy @@ -1,7 +1,4 @@ -# @version 0.4.3 -#pragma optimize gas -#pragma evm-version shanghai - +# pragma version 0.4.3 """ @title LPOracleCrypto @author Curve.Fi @@ -9,6 +6,9 @@ @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 @@ -16,15 +16,11 @@ initializes: lp_oracle_lib exports: lp_oracle_lib.COIN0_ORACLE -interface CryptoPool: - def lp_price() -> uint256: view # Exists only for cryptopools - - -POOL: public(immutable(CryptoPool)) +POOL: public(immutable(ICryptoPool)) @deploy -def __init__(_pool: CryptoPool, _coin0_oracle: lp_oracle_lib.PriceOracle): +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" @@ -42,9 +38,9 @@ def _price_in_coin0() -> uint256: @external @view def price() -> uint256: - return self._price_in_coin0() * lp_oracle_lib._coin0_oracle_price() // 10 ** 18 + 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() // 10 ** 18 + return self._price_in_coin0() * lp_oracle_lib._coin0_oracle_price_w() // c.WAD diff --git a/contracts/price_oracles/lp-oracles/LPOracleStable.vy b/contracts/price_oracles/lp-oracles/LPOracleStable.vy index 4dc14906..62b893ca 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleStable.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleStable.vy @@ -1,7 +1,4 @@ -# @version 0.4.3 -#pragma optimize gas -#pragma evm-version shanghai - +# pragma version 0.4.3 """ @title LPOracleStable @author Curve.Fi @@ -10,11 +7,10 @@ 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 -interface StablePool: - def coins(i: uint256) -> address: view - def price_oracle(i: uint256 = 0) -> uint256: view # Universal method! - def get_virtual_price() -> uint256: view import lp_oracle_lib initializes: lp_oracle_lib @@ -22,13 +18,13 @@ exports: lp_oracle_lib.COIN0_ORACLE MAX_COINS: constant(uint256) = 8 -POOL: public(immutable(StablePool)) +POOL: public(immutable(IStablePool)) NO_ARGUMENT: public(immutable(bool)) N_COINS: public(immutable(uint256)) @deploy -def __init__(_pool: StablePool, _coin0_oracle: lp_oracle_lib.PriceOracle): +def __init__(_pool: IStablePool, _coin0_oracle: IPriceOracle): no_argument: bool = False # Init variables for raw calls @@ -73,7 +69,7 @@ def __init__(_pool: StablePool, _coin0_oracle: lp_oracle_lib.PriceOracle): def _price_in_coin0() -> uint256: min_p: uint256 = max_value(uint256) for i: uint256 in range(N_COINS, bound=MAX_COINS): - p_oracle: uint256 = 10 ** 18 + p_oracle: uint256 = c.WAD if i > 0: if NO_ARGUMENT: p_oracle = staticcall POOL.price_oracle() @@ -83,15 +79,15 @@ def _price_in_coin0() -> uint256: if p_oracle < min_p: min_p = p_oracle - return min_p * (staticcall POOL.get_virtual_price()) // 10**18 + 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() // 10 ** 18 + 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() // 10 ** 18 + 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 index 2882bd88..4a8edbe7 100644 --- a/contracts/price_oracles/lp-oracles/lp_oracle_lib.vy +++ b/contracts/price_oracles/lp-oracles/lp_oracle_lib.vy @@ -1,19 +1,13 @@ -# @version 0.4.3 -#pragma optimize gas -#pragma evm-version shanghai +# pragma version 0.4.3 +from contracts.interfaces import IPriceOracle +from contracts import constants as c -# TODO use vyi interfaces -interface PriceOracle: - def price() -> uint256: view - def price_w() -> uint256: nonpayable - - -COIN0_ORACLE: public(immutable(PriceOracle)) +COIN0_ORACLE: public(immutable(IPriceOracle)) @deploy -def __init__(coin0_oracle: PriceOracle): +def __init__(coin0_oracle: IPriceOracle): COIN0_ORACLE = coin0_oracle @@ -23,7 +17,7 @@ def _coin0_oracle_price() -> uint256: if COIN0_ORACLE.address != empty(address): return staticcall COIN0_ORACLE.price() else: - return 10**18 + return c.WAD @internal @@ -31,4 +25,4 @@ def _coin0_oracle_price_w() -> uint256: if COIN0_ORACLE.address != empty(address): return extcall COIN0_ORACLE.price_w() else: - return 10**18 + return c.WAD From e7fa08ee2f9778b6fcc9967b3c25d539b0f709ef Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 8 Aug 2025 18:18:03 +0200 Subject: [PATCH 089/413] chore: tackling TODOs --- contracts/AMM.vy | 5 +-- contracts/Controller.vy | 82 ++++++++++++++++++++--------------------- 2 files changed, 43 insertions(+), 44 deletions(-) diff --git a/contracts/AMM.vy b/contracts/AMM.vy index 91506c11..b8e63fed 100644 --- a/contracts/AMM.vy +++ b/contracts/AMM.vy @@ -50,7 +50,7 @@ MAX_TICKS: constant(int256) = 50 MAX_TICKS_UINT: constant(uint256) = c.MAX_TICKS_UINT MAX_SKIP_TICKS: constant(int256) = 1024 MAX_SKIP_TICKS_UINT: constant(uint256) = 1024 -# TODO create vyper issue +# https://github.com/vyperlang/vyper/issues/4723 DEAD_SHARES: constant(uint256) = c.DEAD_SHARES @@ -101,8 +101,7 @@ user_shares: public(HashMap[address, IAMM.UserTicks]) _liquidity_mining_callback: ILMGauge -# TODO compiler bug workaround -# TODO report issue +# https://github.com/vyperlang/vyper/issues/4721 @view @external def liquidity_mining_callback() -> ILMGauge: diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 49f52ae3..242dea70 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -66,6 +66,7 @@ CALLBACK_LIQUIDATE: constant(bytes4) = method_id( "callback_liquidate(address,uint256,uint256,uint256,bytes)", output_type=bytes4, ) +CALLDATA_MAX_SIZE: constant(uint256) = 10**4 MAX_LOAN_DISCOUNT: constant(uint256) = 5 * 10**17 MIN_LIQUIDATION_DISCOUNT: constant(uint256) = 10**16 @@ -157,7 +158,7 @@ def __init__( self._monetary_policy = monetary_policy self.liquidation_discount = liquidation_discount self.loan_discount = loan_discount - self._total_debt.rate_mul = 10**18 + self._total_debt.rate_mul = WAD @view @@ -374,14 +375,14 @@ def get_y_effective( d_y_effective: uint256 = unsafe_div( collateral * unsafe_sub( - 10**18, + WAD, min( discount + unsafe_div( - (DEAD_SHARES * 10**18), + (DEAD_SHARES * WAD), max(unsafe_div(collateral, N), DEAD_SHARES), ), - 10**18, + WAD, ), ), unsafe_mul(SQRT_BAND_RATIO, N), @@ -463,7 +464,7 @@ def max_p_base() -> uint256: 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() * 10**18 // p_oracle, int256) + convert(staticcall AMM.get_base_price() * WAD // p_oracle, int256) ) if n1 < 0: n1 -= ( @@ -546,10 +547,10 @@ def _max_borrowable( ) x: uint256 = unsafe_sub( - max(unsafe_div(y_effective * self.max_p_base(), 10**18), 1), 1 + max(unsafe_div(y_effective * self.max_p_base(), WAD), 1), 1 ) x = unsafe_div( - x * (10**18 - 10**14), unsafe_mul(10**18, BORROWED_PRECISION) + x * (WAD - 10**14), unsafe_mul(WAD, BORROWED_PRECISION) ) # Make it a bit smaller return min(x, cap) @@ -572,10 +573,10 @@ def min_collateral( return unsafe_div( unsafe_div( debt - * unsafe_mul(10**18, BORROWED_PRECISION) // self.max_p_base() + * unsafe_mul(WAD, BORROWED_PRECISION) // self.max_p_base() * 10 ** 18 // self.get_y_effective( - 10**18, N, self.loan_discount + self.extra_health[user] + WAD, N, self.loan_discount + self.extra_health[user] ) + unsafe_add( unsafe_mul(N, unsafe_add(N, 2 * DEAD_SHARES)), @@ -583,8 +584,8 @@ def min_collateral( ), COLLATERAL_PRECISION, ) - * 10**18, - 10**18 - 10**14, + * WAD, + WAD - 10**14, ) @@ -630,7 +631,7 @@ def execute_callback( stablecoins: uint256, collateral: uint256, debt: uint256, - calldata: Bytes[10**4], + calldata: Bytes[CALLDATA_MAX_SIZE], ) -> IController.CallbackData: assert callbacker != COLLATERAL_TOKEN.address assert callbacker != BORROWED_TOKEN.address @@ -667,7 +668,7 @@ def _create_loan( N: uint256, _for: address, callbacker: address = empty(address), - calldata: Bytes[10**4] = b"", + calldata: Bytes[CALLDATA_MAX_SIZE] = b"", ) -> uint256: if _for != tx.origin: # We can create a loan for tx.origin (for example when wrapping ETH with EOA), @@ -744,7 +745,7 @@ def create_loan( N: uint256, _for: address = msg.sender, callbacker: address = empty(address), - calldata: Bytes[10**4] = b"", + calldata: Bytes[CALLDATA_MAX_SIZE] = b"", ): """ @notice Create loan but pass stablecoin to a callback first so that it can build leverage @@ -781,7 +782,7 @@ def _add_collateral_borrow( assert debt > 0, "Loan doesn't exist" debt += d_debt - xy: uint256[2] = extcall AMM.withdraw(_for, 10**18) + xy: uint256[2] = extcall AMM.withdraw(_for, WAD) assert xy[0] == 0, "Already in underwater mode" if remove_collateral: xy[1] -= d_collateral @@ -794,7 +795,7 @@ def _add_collateral_borrow( # = 2 * 10**18 * 10**(18 - borrow_decimals) / 10**(collateral_decimals) assert ( d_collateral * staticcall AMM.price_oracle() - > 2 * 10**18 * BORROWED_PRECISION // COLLATERAL_PRECISION + > 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) @@ -868,7 +869,7 @@ def borrow_more( debt: uint256, _for: address = msg.sender, callbacker: address = empty(address), - calldata: Bytes[10**4] = b"", + calldata: Bytes[CALLDATA_MAX_SIZE] = b"", ): """ @notice Borrow more stablecoins while adding more collateral using a callback (to leverage more) @@ -893,7 +894,7 @@ def _borrow_more( debt: uint256, _for: address = msg.sender, callbacker: address = empty(address), - calldata: Bytes[10**4] = b"", + calldata: Bytes[CALLDATA_MAX_SIZE] = b"", ) -> uint256: if debt == 0: return 0 @@ -952,7 +953,7 @@ def repay( _for: address = msg.sender, max_active_band: int256 = max_value(int256), callbacker: address = empty(address), - calldata: Bytes[10**4] = b"", + calldata: Bytes[CALLDATA_MAX_SIZE] = b"", ): """ @notice Repay debt (partially or fully) @@ -972,7 +973,7 @@ def repay( cb: IController.CallbackData = empty(IController.CallbackData) if callbacker != empty(address): assert approval - xy = extcall AMM.withdraw(_for, 10**18) + xy = extcall AMM.withdraw(_for, WAD) self.transferFrom(COLLATERAL_TOKEN, AMM.address, callbacker, xy[1]) cb = self.execute_callback( callbacker, CALLBACK_REPAY, _for, xy[0], xy[1], debt, calldata @@ -987,7 +988,7 @@ def repay( d_debt = debt debt = 0 if callbacker == empty(address): - xy = extcall AMM.withdraw(_for, 10**18) + xy = extcall AMM.withdraw(_for, WAD) total_stablecoins = 0 if xy[0] > 0: @@ -1038,7 +1039,7 @@ def repay( # Not in soft-liquidation - can use callback and move bands new_collateral: uint256 = cb.collateral if callbacker == empty(address): - xy = extcall AMM.withdraw(_for, 10**18) + xy = extcall AMM.withdraw(_for, WAD) new_collateral = xy[1] ns[0] = self._calculate_debt_n1( new_collateral, @@ -1102,13 +1103,13 @@ def _health( @return Health: > 0 = good. """ assert debt > 0, "Loan doesn't exist" - health: int256 = 10**18 - convert(liquidation_discount, int256) + health: int256 = WAD - convert(liquidation_discount, int256) health = ( unsafe_div( convert(staticcall AMM.get_x_down(user), int256) * health, convert(debt, int256), ) - - 10**18 + - WAD ) if full: @@ -1182,7 +1183,7 @@ def health_calculator( n1 = ns[0] x_eff = convert( staticcall AMM.get_x_down(user) - * unsafe_mul(10**18, BORROWED_PRECISION), + * unsafe_mul(WAD, BORROWED_PRECISION), int256, ) @@ -1198,7 +1199,7 @@ def health_calculator( ) health: int256 = unsafe_div(x_eff, debt) - health = health - unsafe_div(health * ld, 10**18) - 10**18 + health = health - unsafe_div(health * ld, WAD) - WAD if full: if n1 > active_band: # We are not in liquidation mode @@ -1214,17 +1215,17 @@ def health_calculator( @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: uint256 = WAD + if frac < WAD: f_remove = unsafe_div( unsafe_mul( - unsafe_add(10**18, unsafe_div(health_limit, 2)), - unsafe_sub(10**18, frac), + unsafe_add(WAD, unsafe_div(health_limit, 2)), + unsafe_sub(WAD, frac), ), - unsafe_add(10**18, health_limit), + unsafe_add(WAD, health_limit), ) f_remove = unsafe_div( - unsafe_mul(unsafe_add(f_remove, frac), frac), 10**18 + unsafe_mul(unsafe_add(f_remove, frac), frac), WAD ) return f_remove @@ -1259,9 +1260,8 @@ def liquidate( final_debt: uint256 = debt # TODO shouldn't clamp max - frac: uint256 = min(_frac, 10**18) - # TODO use wads - debt = unsafe_div(debt * frac + (10**18 - 1), 10**18) + frac: uint256 = min(_frac, WAD) + debt = unsafe_div(debt * frac + (WAD - 1), WAD) assert debt > 0 final_debt = unsafe_sub(final_debt, debt) @@ -1353,7 +1353,7 @@ def liquidate( @view @external -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 @param user Address of the user to liquidate @@ -1366,9 +1366,9 @@ def tokens_to_liquidate(user: address, frac: uint256 = 10**18) -> uint256: stablecoins: uint256 = unsafe_div( (staticcall AMM.get_sum_xy(user))[0] * self._get_f_remove(frac, health_limit), - 10**18, + WAD, ) - debt: uint256 = unsafe_div(self._debt(user)[0] * frac, 10**18) + debt: uint256 = unsafe_div(self._debt(user)[0] * frac, WAD) return unsafe_sub(max(debt, stablecoins), stablecoins) @@ -1429,7 +1429,6 @@ def amm_price() -> uint256: """ @notice Current price from the AMM @dev Marked as reentrant because AMM has a nonreentrant decorator - # TODO check if @reentrant is actually needed """ return staticcall AMM.get_p() @@ -1546,8 +1545,9 @@ def collect_fees() -> uint256: @internal def _collect_fees(admin_fee: uint256) -> uint256: + if admin_fee == 0: + return 0 - # TODO add early termination condition for admin fee == 0 _to: address = staticcall FACTORY.fee_receiver() # Borrowing-based fees @@ -1563,7 +1563,7 @@ def _collect_fees(admin_fee: uint256) -> uint256: if to_be_repaid > processed: self.processed = to_be_repaid fees: uint256 = ( - unsafe_sub(to_be_repaid, processed) * admin_fee // 10**18 + unsafe_sub(to_be_repaid, processed) * admin_fee // WAD ) self.transfer(BORROWED_TOKEN, _to, fees) log IController.CollectFees(amount=fees, new_supply=loan.initial_debt) From 11ea6ca733eeb95f28fc61648c987bc7eb08cd4d Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 11 Aug 2025 12:25:33 +0400 Subject: [PATCH 090/413] chore: remove crvUSD restriction --- contracts/lending/LendingFactory.vy | 21 +-------------------- tests/lending/conftest.py | 7 ++----- tests/lending/test_oracle_attack.py | 14 ++++---------- 3 files changed, 7 insertions(+), 35 deletions(-) diff --git a/contracts/lending/LendingFactory.vy b/contracts/lending/LendingFactory.vy index 1b35a1db..88846ceb 100644 --- a/contracts/lending/LendingFactory.vy +++ b/contracts/lending/LendingFactory.vy @@ -13,14 +13,11 @@ from ethereum.ercs import IERC20Detailed from contracts.interfaces import IVault from contracts.interfaces import ILlamalendController as IController from contracts.interfaces import IAMM -from contracts.interfaces import IPool from contracts.interfaces import IPriceOracle -from contracts.interfaces import ILendingFactory +from contracts.interfaces import ILendingFactory implements: ILendingFactory -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% @@ -53,16 +50,11 @@ vaults: public(IVault[10**18]) _vaults_index: HashMap[IVault, uint256] market_count: public(uint256) -# Index to find vaults by a non-crvUSD token -token_to_vaults: public(HashMap[address, IVault[10**18]]) -token_market_count: public(HashMap[address, uint256]) - names: public(HashMap[uint256, String[64]]) @deploy def __init__( - stablecoin: address, amm: address, controller: address, vault: address, @@ -73,7 +65,6 @@ def __init__( ): """ @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) @@ -81,7 +72,6 @@ def __init__( @param admin Admin address (DAO) @param fee_receiver Receiver of interest and admin fees """ - STABLECOIN = stablecoin self.amm_impl = amm self.controller_impl = controller self.vault_impl = vault @@ -141,7 +131,6 @@ def _create( @notice Internal method for creation of the vault """ assert borrowed_token != collateral_token, "Same token" - assert borrowed_token == STABLECOIN or collateral_token == STABLECOIN assert A >= MIN_A and A <= MAX_A, "Wrong A" assert fee <= MAX_FEE, "Fee too high" assert fee >= MIN_FEE, "Fee too low" @@ -196,16 +185,8 @@ def _create( self.vaults[market_count] = vault 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.address, controller, amm] diff --git a/tests/lending/conftest.py b/tests/lending/conftest.py index 8c929926..e702b621 100644 --- a/tests/lending/conftest.py +++ b/tests/lending/conftest.py @@ -69,12 +69,9 @@ def factory_partial(): @pytest.fixture(scope="module") -def factory(factory_partial, stablecoin, amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, admin): +def factory(factory_partial, amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, admin): with boa.env.prank(admin): - return factory_partial.deploy( - stablecoin.address, - amm_impl, controller_impl, vault_impl, - price_oracle_impl, mpolicy_impl, admin, admin) + return factory_partial.deploy(amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, admin, admin) @pytest.fixture(scope="module", params=product([2, 6, 8, 18], [True, False])) diff --git a/tests/lending/test_oracle_attack.py b/tests/lending/test_oracle_attack.py index 0f8fd8de..ef214bac 100644 --- a/tests/lending/test_oracle_attack.py +++ b/tests/lending/test_oracle_attack.py @@ -39,12 +39,9 @@ def hacker(accounts): @pytest.fixture(scope="module") -def factory_new(factory_partial, stablecoin, amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, admin): +def factory_new(factory_partial, amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, admin): with boa.env.prank(admin): - return factory_partial.deploy( - stablecoin.address, - amm_impl, controller_impl, vault_impl, - price_oracle_impl, mpolicy_impl, admin, admin) + return factory_partial.deploy(amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, admin, admin) @pytest.fixture(scope="module") @@ -53,13 +50,10 @@ def amm_old_interface(): @pytest.fixture(scope="module") -def factory_old(factory_partial, stablecoin, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, amm_old_interface, admin): +def factory_old(factory_partial, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, amm_old_interface, admin): 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, admin, admin) + return factory_partial.deploy(amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, admin, admin) @pytest.fixture(scope='module') From 456dd64b732e888f714e573728d328d7e00b1b1e Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 11 Aug 2025 11:04:43 +0200 Subject: [PATCH 091/413] fix: added signed wad constant --- contracts/Controller.vy | 7 ++++--- contracts/constants.vy | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 242dea70..8664c5ce 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -50,6 +50,7 @@ FACTORY: immutable(IFactory) from contracts import constants as c WAD: constant(uint256) = c.WAD +SWAD: constant(int256) = c.SWAD DEAD_SHARES: constant(uint256) = c.DEAD_SHARES MIN_AMM_FEE: constant(uint256) = 10**6 # 1e-12, still needs to be above 0 @@ -1103,13 +1104,13 @@ def _health( @return Health: > 0 = good. """ assert debt > 0, "Loan doesn't exist" - health: int256 = WAD - convert(liquidation_discount, int256) + health: int256 = SWAD - convert(liquidation_discount, int256) health = ( unsafe_div( convert(staticcall AMM.get_x_down(user), int256) * health, convert(debt, int256), ) - - WAD + - SWAD ) if full: @@ -1199,7 +1200,7 @@ def health_calculator( ) health: int256 = unsafe_div(x_eff, debt) - health = health - unsafe_div(health * ld, WAD) - WAD + health = health - unsafe_div(health * ld, SWAD) - SWAD if full: if n1 > active_band: # We are not in liquidation mode diff --git a/contracts/constants.vy b/contracts/constants.vy index c21c773e..5a7d5101 100644 --- a/contracts/constants.vy +++ b/contracts/constants.vy @@ -3,4 +3,5 @@ MAX_TICKS: constant(int256) = 50 DEAD_SHARES: constant(uint256) = 1000 # TODO make sure this is used everywhere -WAD: constant(uint256) = 10**18 \ No newline at end of file +WAD: constant(uint256) = 10**18 +SWAD: constant(int256) = 10**18 \ No newline at end of file From 33a871c0aed4cbeffe74ebc6bfd2442dfcc29c75 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 11 Aug 2025 11:05:53 +0200 Subject: [PATCH 092/413] ci: add initial test runner --- .github/workflows/act.sh | 15 ++++++++++ .github/workflows/test.yaml | 59 +++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100755 .github/workflows/act.sh create mode 100644 .github/workflows/test.yaml 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/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 00000000..4283bb3c --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,59 @@ +name: tests-boa + +on: [push] + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # RPC_ETHEREUM: ${{ secrets.RPC_ETHEREUM }} + +jobs: + tests: + name: ${{ matrix.folder }} (${{ matrix.venom.name }}) + runs-on: ubuntu-latest + strategy: + matrix: + folder: + - "tests/flashloan" + - "tests/lm_callback" + - "tests/amm" + - "tests/lending" + - "tests/price_oracles" + - "tests/swap" + venom: + - { name: "standard mode", value: false } + # - { name: "venom mode", value: true } + continue-on-error: ${{ matrix.venom.value }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + version: "0.8.7" + 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: Install Nightly Vyper if Venom is enabled + if: ${{ matrix.venom.value }} + run: | + uv pip install --force-reinstall 'git+https://github.com/vyperlang/vyper.git@master#egg=vyper' + + - name: Run tests + run: | + export VENOM=${{ matrix.venom.value }} + uv run pytest ${{ matrix.folder }} -n auto + # source .venv/bin/activate + # uv pip freeze From f87e28b78e1c497fabdcd2919fd42c96bc4e2687 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 11 Aug 2025 11:33:40 +0200 Subject: [PATCH 093/413] ci: clone submodules --- .github/workflows/test.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 4283bb3c..749f3c9a 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -26,6 +26,8 @@ jobs: steps: - name: Checkout repo uses: actions/checkout@v4 + with: + submodules: recursive - name: Install uv uses: astral-sh/setup-uv@v6 From e74ab6f8031eed961a65470a62d6342458e96272 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 11 Aug 2025 12:21:21 +0200 Subject: [PATCH 094/413] fix: extra validation --- contracts/Controller.vy | 3 ++- contracts/lending/LendingFactory.vy | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 8664c5ce..08ef4bac 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -227,7 +227,8 @@ def set_price_oracle(price_oracle: IPriceOracle, max_deviation: uint256): assert max_deviation <= MAX_ORACLE_PRICE_DEVIATION or max_deviation == max_value(uint256) # dev: invalid max deviation # Validate the new oracle has required methods - new_price: uint256 = extcall price_oracle.price_w() + 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() diff --git a/contracts/lending/LendingFactory.vy b/contracts/lending/LendingFactory.vy index 1b35a1db..f7535d87 100644 --- a/contracts/lending/LendingFactory.vy +++ b/contracts/lending/LendingFactory.vy @@ -95,6 +95,7 @@ def __init__( self.fee_receiver = fee_receiver +# TODO use snekmate's @internal @pure def ln_int(_x: uint256) -> int256: From a1702925183af9bbe190f01f45dea5524c13ec02 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 11 Aug 2025 19:38:22 +0400 Subject: [PATCH 095/413] fix: max_p_base inconsistency --- contracts/Controller.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index d0f7b41a..a8a0264e 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -454,7 +454,7 @@ def max_p_base() -> uint256: if n1 <= n_min: break p_base_prev: uint256 = p_base - p_base = unsafe_div(p_base * A, Aminus1) + p_base = staticcall AMM.p_oracle_up(n1) if p_base > p_oracle: return p_base_prev return p_base From 2cef9c964b07cb84fdca13c72c52c30071860586 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 11 Aug 2025 19:45:04 +0400 Subject: [PATCH 096/413] chore: remove token_to_vaults and token_market_count from tests --- tests/lending/test_vault.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/lending/test_vault.py b/tests/lending/test_vault.py index e2f2f5ee..0a2aee82 100644 --- a/tests/lending/test_vault.py +++ b/tests/lending/test_vault.py @@ -25,13 +25,6 @@ def test_vault_creation(vault, market_controller, market_amm, market_mpolicy, fa 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.vaults(factory.vaults_index(vault.address)) == vault.address From f4c0e320e7ba497ced9d7fc5b462a2ad575098af Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 13 Aug 2025 13:49:11 +0200 Subject: [PATCH 097/413] test: add initial tests for price oracle setter --- .github/workflows/test.yaml | 1 + contracts/MintController.vy | 2 +- contracts/lending/LendingFactory.vy | 3 + contracts/testing/ERC20Mock.vy | 78 +---- tests/boosted_lm_callback/conftest.py | 4 +- tests/conftest.py | 4 +- tests/controller/conftest.py | 19 ++ tests/controller/test_set_price_oracle.py | 217 ++++++++++++++ tests/lm_callback/conftest.py | 4 +- tests/stableborrow/conftest.py | 4 +- tests/stableborrow/stabilize/conftest.py | 4 +- .../stateful/test_agg_monetary_policy.py | 2 +- tests/swap/conftest.py | 4 +- tests/utils/constants.py | 11 +- tests/utils/deploy.py | 266 ++++++++++++++++++ tests/utils/deployers.py | 149 +++++----- 16 files changed, 623 insertions(+), 149 deletions(-) create mode 100644 tests/controller/conftest.py create mode 100644 tests/controller/test_set_price_oracle.py create mode 100644 tests/utils/deploy.py diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 749f3c9a..e4372810 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,6 +19,7 @@ jobs: - "tests/lending" - "tests/price_oracles" - "tests/swap" + - "tests/controller" venom: - { name: "standard mode", value: false } # - { name: "venom mode", value: true } diff --git a/contracts/MintController.vy b/contracts/MintController.vy index 2cadb997..bf377cd7 100644 --- a/contracts/MintController.vy +++ b/contracts/MintController.vy @@ -35,4 +35,4 @@ def __init__( amm, ) - assert extcall core.BORROWED_TOKEN.approve(core.FACTORY, max_value(uint256), default_return_value=True) + assert extcall core.BORROWED_TOKEN.approve(core.FACTORY.address, max_value(uint256), default_return_value=True) diff --git a/contracts/lending/LendingFactory.vy b/contracts/lending/LendingFactory.vy index b9ee9f71..2c2e6e25 100644 --- a/contracts/lending/LendingFactory.vy +++ b/contracts/lending/LendingFactory.vy @@ -146,6 +146,7 @@ def _create( 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" + # TODO code offset is not required anymore monetary_policy: address = create_from_blueprint( self.monetary_policy_impl, borrowed_token, min_rate, max_rate, code_offset=3) @@ -222,6 +223,7 @@ def create( """ res: address[3] = self._create(borrowed_token, collateral_token, A, fee, loan_discount, liquidation_discount, price_oracle, name, min_borrow_rate, max_borrow_rate) + # TODO duplicate code if supply_limit < max_value(uint256): extcall IVault(res[0]).set_max_supply(supply_limit) @@ -286,6 +288,7 @@ def create_from_pool( borrowed_token, collateral_token, A, fee, loan_discount, liquidation_discount, price_oracle, name, min_borrow_rate, max_borrow_rate, ) + # TODO duplicate code if supply_limit < max_value(uint256): extcall IVault(res[0]).set_max_supply(supply_limit) diff --git a/contracts/testing/ERC20Mock.vy b/contracts/testing/ERC20Mock.vy index 4070b457..c2db01c1 100644 --- a/contracts/testing/ERC20Mock.vy +++ b/contracts/testing/ERC20Mock.vy @@ -1,73 +1,15 @@ -# @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 snekmate.tokens import erc20 +from snekmate.auth import ownable -event Approval: - _owner: indexed(address) - _spender: indexed(address) - _value: uint256 +initializes: ownable +initializes: erc20[ownable := 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 +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") \ No newline at end of file diff --git a/tests/boosted_lm_callback/conftest.py b/tests/boosted_lm_callback/conftest.py index 6abc7ef6..bab16fc9 100644 --- a/tests/boosted_lm_callback/conftest.py +++ b/tests/boosted_lm_callback/conftest.py @@ -9,7 +9,7 @@ STABLECOIN_DEPLOYER, WETH_DEPLOYER, CONTROLLER_FACTORY_DEPLOYER, - CONTROLLER_DEPLOYER, + MINT_CONTROLLER_DEPLOYER, AMM_DEPLOYER, CONSTANT_MONETARY_POLICY_DEPLOYER, BOOSTED_LM_CALLBACK_DEPLOYER, @@ -79,7 +79,7 @@ def controller_prefactory(stablecoin, weth, admin, accounts): @pytest.fixture(scope="module") def controller_interface(): - return CONTROLLER_DEPLOYER + return MINT_CONTROLLER_DEPLOYER @pytest.fixture(scope="module") diff --git a/tests/conftest.py b/tests/conftest.py index 4628cfff..0058e257 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -53,7 +53,7 @@ def token_mock(): 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 token_mock.deploy(digits) return f @@ -61,7 +61,7 @@ def f(digits): def get_borrowed_token(token_mock, admin) -> Callable[[int], Any]: def f(digits): with boa.env.prank(admin): - return token_mock.deploy("Rugworks USD", "rUSD", digits) + return token_mock.deploy(digits) return f diff --git a/tests/controller/conftest.py b/tests/controller/conftest.py new file mode 100644 index 00000000..8fbb62d0 --- /dev/null +++ b/tests/controller/conftest.py @@ -0,0 +1,19 @@ +from pytest import fixture +from tests.utils.deploy import Protocol +from tests.utils.deployers import ERC20_MOCK_DEPLOYER + +@fixture(scope="module") +def proto(): + return Protocol() + +@fixture(scope="module") +def admin(proto: Protocol): + return proto.admin + +@fixture(scope="module", params=range(2, 19)) +def decimals(request): + return request.param + +@fixture(scope="module") +def collat(decimals): + return ERC20_MOCK_DEPLOYER.deploy(decimals) diff --git a/tests/controller/test_set_price_oracle.py b/tests/controller/test_set_price_oracle.py new file mode 100644 index 00000000..c23c81b2 --- /dev/null +++ b/tests/controller/test_set_price_oracle.py @@ -0,0 +1,217 @@ +import pytest +import boa +from tests.utils.deployers import ( + MINT_CONTROLLER_DEPLOYER, + LL_CONTROLLER_DEPLOYER, + AMM_DEPLOYER, + 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() +def mint_market(proto, collat): + return proto.create_mint_market( + collat, + proto.price_oracle, + proto.mint_monetary_policy, + A=1000, + amm_fee=10**16, + admin_fee=0, + loan_discount= int(0.09 * 10**18), + liquidation_discount=int(0.06 * 10**18), + debt_ceiling=1000 * 10**18 + ) + + +@pytest.fixture() +def lend_market(proto, collat): + return proto.create_lending_market( + borrowed_token=proto.crvUSD, # TODO param other tokens + collateral_token=collat, + A=1000, + fee=10**16, + loan_discount=int(0.09 * 10**18), + liquidation_discount=int(0.06 * 10**18), + price_oracle=proto.price_oracle, + name="Test Vault", + min_borrow_rate=10**15 // (365 * 86400), # 0.1% APR (per second) + max_borrow_rate=10**18 // (365 * 86400) # 100% APR (per second) + ) + +@pytest.fixture(params=["mint", "lending"]) +def market(request, mint_market, lend_market): + """Parametrized fixture that provides both mint and lending markets.""" + if request.param == "mint": + return mint_market + else: + return lend_market + +@pytest.fixture() +def controller(market): + """Parametrized controller fixture that works with both market types.""" + # Check if it's a mint market by looking for 'controller' key structure + # Mint markets have 'controller' and 'amm' + # Lending markets have 'vault', 'controller', 'amm', 'oracle', 'monetary_policy' + if 'vault' in market: + # It's a lending market + return LL_CONTROLLER_DEPLOYER.at(market['controller']) + else: + # It's a mint market + return MINT_CONTROLLER_DEPLOYER.at(market['controller']) + +@pytest.fixture() +def amm(market): + return AMM_DEPLOYER.at(market['amm']) + + +@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) + + +@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) + + +@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_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 + + +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) + + +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("deviation>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/lm_callback/conftest.py b/tests/lm_callback/conftest.py index 6f7c04af..569be144 100644 --- a/tests/lm_callback/conftest.py +++ b/tests/lm_callback/conftest.py @@ -8,7 +8,7 @@ STABLECOIN_DEPLOYER, WETH_DEPLOYER, CONTROLLER_FACTORY_DEPLOYER, - CONTROLLER_DEPLOYER, + MINT_CONTROLLER_DEPLOYER, AMM_DEPLOYER, CONSTANT_MONETARY_POLICY_DEPLOYER, LM_CALLBACK_DEPLOYER, @@ -78,7 +78,7 @@ def controller_prefactory(stablecoin, weth, admin, accounts): @pytest.fixture(scope="module") def controller_interface(): - return CONTROLLER_DEPLOYER + return MINT_CONTROLLER_DEPLOYER @pytest.fixture(scope="module") diff --git a/tests/stableborrow/conftest.py b/tests/stableborrow/conftest.py index 4f2f13f0..7f9cfd91 100644 --- a/tests/stableborrow/conftest.py +++ b/tests/stableborrow/conftest.py @@ -5,7 +5,7 @@ STABLECOIN_DEPLOYER, WETH_DEPLOYER, CONTROLLER_FACTORY_DEPLOYER, - CONTROLLER_DEPLOYER, + MINT_CONTROLLER_DEPLOYER, AMM_DEPLOYER, CONSTANT_MONETARY_POLICY_DEPLOYER, FAKE_LEVERAGE_DEPLOYER @@ -46,7 +46,7 @@ def controller_prefactory(controller_factory_impl, stablecoin, weth, admin, acco @pytest.fixture(scope="session") def controller_interface(): - return CONTROLLER_DEPLOYER + return MINT_CONTROLLER_DEPLOYER @pytest.fixture(scope="session") diff --git a/tests/stableborrow/stabilize/conftest.py b/tests/stableborrow/stabilize/conftest.py index 50f3221f..0db1ed93 100644 --- a/tests/stableborrow/stabilize/conftest.py +++ b/tests/stableborrow/stabilize/conftest.py @@ -53,13 +53,13 @@ def collateral_token(get_collateral_token): @pytest.fixture(scope="module") def stablecoin_a(admin): with boa.env.prank(admin): - return ERC20_MOCK_DEPLOYER.deploy("USDa", "USDa", 6) + return ERC20_MOCK_DEPLOYER.deploy(6) @pytest.fixture(scope="module") def stablecoin_b(admin): with boa.env.prank(admin): - return ERC20_MOCK_DEPLOYER.deploy("USDb", "USDb", 18) + return ERC20_MOCK_DEPLOYER.deploy(18) @pytest.fixture(scope="module") diff --git a/tests/stableborrow/stabilize/stateful/test_agg_monetary_policy.py b/tests/stableborrow/stabilize/stateful/test_agg_monetary_policy.py index f524a3af..bc24c2ae 100644 --- a/tests/stableborrow/stabilize/stateful/test_agg_monetary_policy.py +++ b/tests/stableborrow/stabilize/stateful/test_agg_monetary_policy.py @@ -58,7 +58,7 @@ def initializer(self, digits): 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) diff --git a/tests/swap/conftest.py b/tests/swap/conftest.py index f8ee20ee..edb94692 100644 --- a/tests/swap/conftest.py +++ b/tests/swap/conftest.py @@ -25,13 +25,13 @@ def swap_deployer(swap_impl, admin): @pytest.fixture(scope="session") def redeemable_coin(admin): with boa.env.prank(admin): - return ERC20_MOCK_DEPLOYER.deploy("Unbranded Redeemable USD", "urUSD", 6) + return ERC20_MOCK_DEPLOYER.deploy(6) @pytest.fixture(scope="session") def volatile_coin(admin): with boa.env.prank(admin): - return ERC20_MOCK_DEPLOYER.deploy("Volatile USD", "vUSD", 18) + return ERC20_MOCK_DEPLOYER.deploy(18) @pytest.fixture(scope="session") diff --git a/tests/utils/constants.py b/tests/utils/constants.py index e5d7e4cb..c28e77b5 100644 --- a/tests/utils/constants.py +++ b/tests/utils/constants.py @@ -1,3 +1,12 @@ import boa +from tests.utils.deployers import CONSTANTS_DEPLOYER, CONTROLLER_DEPLOYER -ZERO_ADDRESS = boa.eval("empty(address)") \ No newline at end of file +ZERO_ADDRESS = boa.eval("empty(address)") +MAX_UINT256 = boa.eval("max_value(uint256)") + +# Constants from contracts/constants.vy +WAD = CONSTANTS_DEPLOYER._constants.WAD +SWAD = CONSTANTS_DEPLOYER._constants.SWAD + +# Constants from Controller.vy +MAX_ORACLE_PRICE_DEVIATION = CONTROLLER_DEPLOYER._constants.MAX_ORACLE_PRICE_DEVIATION \ No newline at end of file diff --git a/tests/utils/deploy.py b/tests/utils/deploy.py new file mode 100644 index 00000000..0adad274 --- /dev/null +++ b/tests/utils/deploy.py @@ -0,0 +1,266 @@ +""" +Deploy function for the complete llamalend protocol suite. +Provides deployment of both mint and lending protocols with all necessary contracts. +""" + +import boa +from typing import Dict, Any +from tests.utils.deployers import ( + # Core contracts + STABLECOIN_DEPLOYER, + AMM_DEPLOYER, + MINT_CONTROLLER_DEPLOYER, + CONTROLLER_FACTORY_DEPLOYER, + + # Lending contracts + VAULT_DEPLOYER, + LL_CONTROLLER_DEPLOYER, + LENDING_FACTORY_DEPLOYER, + + # Price oracles + DUMMY_PRICE_ORACLE_DEPLOYER, + CRYPTO_FROM_POOL_DEPLOYER, + + # Monetary policies + CONSTANT_MONETARY_POLICY_DEPLOYER, + SEMILOG_MONETARY_POLICY_DEPLOYER, + + # Testing contracts + WETH_DEPLOYER, + ERC20_MOCK_DEPLOYER, +) + + +class Protocol: + """ + 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") + + # Deploy core infrastructure + with boa.env.prank(self.admin): + # Deploy stablecoin + self.crvUSD = STABLECOIN_DEPLOYER.deploy('Curve USD', 'crvUSD') + + # Deploy WETH + self.weth = WETH_DEPLOYER.deploy() + + # Deploy shared AMM implementation (used by both mint and lending) + self.amm_impl = AMM_DEPLOYER.deploy_as_blueprint() + + # Deploy a dummy price oracle for testing + self.price_oracle = DUMMY_PRICE_ORACLE_DEPLOYER.deploy(self.admin, initial_price) + + # Deploy Mint Protocol + # Deploy controller implementation + self.mint_controller_impl = MINT_CONTROLLER_DEPLOYER.deploy_as_blueprint() + + # 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 + self.mint_factory.set_implementations( + self.mint_controller_impl.address, + self.amm_impl.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) + self.mint_monetary_policy.set_rate(0) # 0% by default + + # Deploy Lending Protocol + # Deploy vault implementation + self.vault_impl = VAULT_DEPLOYER.deploy() + + # Deploy lending controller implementation + self.ll_controller_impl = LL_CONTROLLER_DEPLOYER.deploy_as_blueprint() + + # Deploy price oracle implementation for lending + self.price_oracle_impl = CRYPTO_FROM_POOL_DEPLOYER.deploy_as_blueprint() + + # Deploy monetary policy implementation for lending + self.mpolicy_impl = SEMILOG_MONETARY_POLICY_DEPLOYER.deploy_as_blueprint() + + # Deploy lending factory + self.lending_factory = LENDING_FACTORY_DEPLOYER.deploy( + self.amm_impl.address, + self.ll_controller_impl.address, + self.vault_impl.address, + self.price_oracle_impl.address, + self.mpolicy_impl.address, + self.admin, + self.fee_receiver + ) + + def create_mint_market( + self, + collateral_token: Any, + price_oracle: Any, + monetary_policy: Any, + A: int, + amm_fee: int, + admin_fee: int, + loan_discount: int, + liquidation_discount: int, + debt_ceiling: int + ) -> Dict[str, Any]: + """ + 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%) + admin_fee: Admin fee share (e.g., 0) + 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' addresses + """ + with boa.env.prank(self.admin): + self.mint_factory.add_market( + collateral_token.address, + A, + amm_fee, + admin_fee, + price_oracle.address, + monetary_policy.address, + loan_discount, + liquidation_discount, + debt_ceiling + ) + + controller_address = self.mint_factory.get_controller(collateral_token.address) + amm_address = self.mint_factory.get_amm(collateral_token.address) + + return { + 'controller': controller_address, + 'amm': amm_address + } + + def create_lending_market( + self, + borrowed_token: Any, + collateral_token: Any, + A: int, + fee: int, + loan_discount: int, + liquidation_discount: int, + price_oracle: Any, + name: str, + min_borrow_rate: int, + max_borrow_rate: int + ) -> Dict[str, Any]: + """ + 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%) + + Returns: + Dictionary with 'vault', 'controller', 'amm', 'oracle', and 'monetary_policy' addresses + """ + with boa.env.prank(self.admin): + vault, controller, amm = self.lending_factory.create( + borrowed_token.address, + collateral_token.address, + A, + fee, + loan_discount, + liquidation_discount, + price_oracle.address, + name, + min_borrow_rate, + max_borrow_rate + ) + + return { + 'vault': vault, + 'controller': controller, + 'amm': amm + } + + def get_deployed_contracts(self) -> Dict[str, Any]: + """ + Get all deployed contracts in the protocol. + + Returns: + Dictionary containing all deployed contract addresses + """ + return { + 'admin': self.admin, + 'crvUSD': self.crvUSD, + 'weth': self.weth, + 'amm_impl': self.amm_impl, + 'price_oracle': self.price_oracle, + 'mint_factory': self.mint_factory, + 'mint_controller_impl': self.mint_controller_impl, + 'mint_monetary_policy': self.mint_monetary_policy, + 'lending_factory': self.lending_factory, + 'vault_impl': self.vault_impl, + 'll_controller_impl': self.ll_controller_impl, + 'price_oracle_impl': self.price_oracle_impl, + 'mpolicy_impl': self.mpolicy_impl + } + +if __name__ == "__main__": + # import cProfile + # import pstats + + # profiler = cProfile.Profile() + # profiler.enable() + + proto = Protocol() + collat = ERC20_MOCK_DEPLOYER.deploy(18) + proto.create_mint_market( + collat, + proto.price_oracle, + proto.mint_monetary_policy, + A=1000, + amm_fee=10**16, + admin_fee=0, + loan_discount= int(0.8 * 10**18), + liquidation_discount=int(0.85 * 10**18), + debt_ceiling=1000 * 10**18 + ) + + # profiler.disable() + # stats = pstats.Stats(profiler) + # stats.dump_stats('protocol_deploy.prof') + # print("Profile saved to protocol_deploy.prof") + # print("Run: snakeviz protocol_deploy.prof") \ No newline at end of file diff --git a/tests/utils/deployers.py b/tests/utils/deployers.py index b36a014c..b860b8b0 100644 --- a/tests/utils/deployers.py +++ b/tests/utils/deployers.py @@ -4,9 +4,17 @@ """ import boa +from vyper.compiler.settings import OptimizationLevel -# Compiler args if needed -compiler_args = {} +# 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/" @@ -18,87 +26,96 @@ FLASHLOAN_CONTRACT_PATH = "contracts/flashloan/" STABLESWAP_NG_PATH = "contracts/testing/stableswap-ng/contracts/main/" +# 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) -CONTROLLER_DEPLOYER = boa.load_partial(BASE_CONTRACT_PATH + "Controller.vy", compiler_args=compiler_args) -CONTROLLER_FACTORY_DEPLOYER = boa.load_partial(BASE_CONTRACT_PATH + "ControllerFactory.vy", compiler_args=compiler_args) -STABLECOIN_DEPLOYER = boa.load_partial(BASE_CONTRACT_PATH + "Stablecoin.vy", compiler_args=compiler_args) -STABLESWAP_DEPLOYER = boa.load_partial(BASE_CONTRACT_PATH + "Stableswap.vy", compiler_args=compiler_args) +AMM_DEPLOYER = boa.load_partial(BASE_CONTRACT_PATH + "AMM.vy", compiler_args=compiler_args_default) +# Controller.vy has #pragma optimize codesize +CONTROLLER_DEPLOYER = boa.load_partial(BASE_CONTRACT_PATH + "Controller.vy", compiler_args=compiler_args_codesize) +# MintController.vy has #pragma optimize 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 -VAULT_DEPLOYER = boa.load_partial(LENDING_CONTRACT_PATH + "Vault.vy", compiler_args=compiler_args) -LL_CONTROLLER_DEPLOYER = boa.load_partial(LENDING_CONTRACT_PATH + "LLController.vy", compiler_args=compiler_args) -LENDING_FACTORY_DEPLOYER = boa.load_partial(LENDING_CONTRACT_PATH + "LendingFactory.vy", compiler_args=compiler_args) +# Lending contracts - all have #pragma optimize codesize +VAULT_DEPLOYER = boa.load_partial(LENDING_CONTRACT_PATH + "Vault.vy", compiler_args=compiler_args_codesize) +LL_CONTROLLER_DEPLOYER = boa.load_partial(LENDING_CONTRACT_PATH + "LLController.vy", compiler_args=compiler_args_codesize) +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) +FLASH_LENDER_DEPLOYER = boa.load_partial(FLASHLOAN_CONTRACT_PATH + "FlashLender.vy", compiler_args=compiler_args_default) -# Monetary policies -CONSTANT_MONETARY_POLICY_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "ConstantMonetaryPolicy.vy", compiler_args=compiler_args) -SEMILOG_MONETARY_POLICY_DEPLOYER = boa.load_partial(MPOLICIES_CONTRACT_PATH + "SemilogMonetaryPolicy.vy", compiler_args=compiler_args) -SECONDARY_MONETARY_POLICY_DEPLOYER = boa.load_partial(MPOLICIES_CONTRACT_PATH + "SecondaryMonetaryPolicy.vy", compiler_args=compiler_args) -AGG_MONETARY_POLICY2_DEPLOYER = boa.load_partial(MPOLICIES_CONTRACT_PATH + "AggMonetaryPolicy2.vy", compiler_args=compiler_args) -AGG_MONETARY_POLICY3_DEPLOYER = boa.load_partial(MPOLICIES_CONTRACT_PATH + "AggMonetaryPolicy3.vy", compiler_args=compiler_args) +# Monetary policies - all have no pragma +CONSTANT_MONETARY_POLICY_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "ConstantMonetaryPolicy.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) -CRYPTO_FROM_POOL_DEPLOYER = boa.load_partial(PRICE_ORACLES_CONTRACT_PATH + "CryptoFromPool.vy", compiler_args=compiler_args) -EMA_PRICE_ORACLE_DEPLOYER = boa.load_partial(PRICE_ORACLES_CONTRACT_PATH + "EmaPriceOracle.vy", compiler_args=compiler_args) -AGGREGATE_STABLE_PRICE3_DEPLOYER = boa.load_partial(PRICE_ORACLES_CONTRACT_PATH + "AggregateStablePrice3.vy", compiler_args=compiler_args) -CRYPTO_WITH_STABLE_PRICE_DEPLOYER = boa.load_partial(PRICE_ORACLES_CONTRACT_PATH + "CryptoWithStablePrice.vy", compiler_args=compiler_args) -CRYPTO_WITH_STABLE_PRICE_AND_CHAINLINK_DEPLOYER = boa.load_partial(PRICE_ORACLES_CONTRACT_PATH + "CryptoWithStablePriceAndChainlink.vy", compiler_args=compiler_args) +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 -PROXY_ORACLE_DEPLOYER = boa.load_partial(PRICE_ORACLES_CONTRACT_PATH + "proxy/ProxyOracle.vy", compiler_args=compiler_args) -PROXY_ORACLE_FACTORY_DEPLOYER = boa.load_partial(PRICE_ORACLES_CONTRACT_PATH + "proxy/ProxyOracleFactory.vy", compiler_args=compiler_args) +# 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) -LP_ORACLE_CRYPTO_DEPLOYER = boa.load_partial(PRICE_ORACLES_CONTRACT_PATH + "lp-oracles/LPOracleCrypto.vy", compiler_args=compiler_args) -LP_ORACLE_FACTORY_DEPLOYER = boa.load_partial(PRICE_ORACLES_CONTRACT_PATH + "lp-oracles/LPOracleFactory.vy", compiler_args=compiler_args) +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) -PEG_KEEPER_REGULATOR_DEPLOYER = boa.load_partial(STABILIZER_CONTRACT_PATH + "PegKeeperRegulator.vy", compiler_args=compiler_args) +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) # Callback contracts -LM_CALLBACK_DEPLOYER = boa.load_partial(BASE_CONTRACT_PATH + "LMCallback.vy", compiler_args=compiler_args) -BOOSTED_LM_CALLBACK_DEPLOYER = boa.load_partial(BASE_CONTRACT_PATH + "BoostedLMCallback.vy", compiler_args=compiler_args) +LM_CALLBACK_DEPLOYER = boa.load_partial(BASE_CONTRACT_PATH + "LMCallback.vy", compiler_args=compiler_args_default) +BOOSTED_LM_CALLBACK_DEPLOYER = boa.load_partial(BASE_CONTRACT_PATH + "BoostedLMCallback.vy", compiler_args=compiler_args_default) # Testing/Mock contracts -ERC20_MOCK_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "ERC20Mock.vy", compiler_args=compiler_args) -ERC20_CRV_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "ERC20CRV.vy", compiler_args=compiler_args) -WETH_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "WETH.vy", compiler_args=compiler_args) -VOTING_ESCROW_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "VotingEscrow.vy", compiler_args=compiler_args) -VE_DELEGATION_MOCK_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "VEDelegationMock.vy", compiler_args=compiler_args) -GAUGE_CONTROLLER_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "GaugeController.vy", compiler_args=compiler_args) -MINTER_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "Minter.vy", compiler_args=compiler_args) -FAKE_LEVERAGE_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "FakeLeverage.vy", compiler_args=compiler_args) -BLOCK_COUNTER_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "BlockCounter.vy", compiler_args=compiler_args) -DUMMY_FLASH_BORROWER_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "DummyFlashBorrower.vy", compiler_args=compiler_args) -DUMMY_LM_CALLBACK_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "DummyLMCallback.vy", compiler_args=compiler_args) -LM_CALLBACK_WITH_REVERTS_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "LMCallbackWithReverts.vy", compiler_args=compiler_args) -MOCK_FACTORY_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "MockFactory.vy", compiler_args=compiler_args) -MOCK_MARKET_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "MockMarket.vy", compiler_args=compiler_args) -MOCK_RATE_SETTER_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "MockRateSetter.vy", compiler_args=compiler_args) -MOCK_PEG_KEEPER_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "MockPegKeeper.vy", compiler_args=compiler_args) -MOCK_RATE_ORACLE_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "MockRateOracle.vy", compiler_args=compiler_args) -CHAINLINK_AGGREGATOR_MOCK_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "ChainlinkAggregatorMock.vy", compiler_args=compiler_args) -TRICRYPTO_MOCK_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "TricryptoMock.vy", compiler_args=compiler_args) -MOCK_SWAP2_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "MockSwap2.vy", compiler_args=compiler_args) -MOCK_SWAP3_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "MockSwap3.vy", compiler_args=compiler_args) -SWAP_FACTORY_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "SwapFactory.vy", compiler_args=compiler_args) -OPTIMIZE_MATH_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "OptimizeMath.vy", compiler_args=compiler_args) -TEST_PACKING_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "TestPacking.vy", compiler_args=compiler_args) -OLD_AMM_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "OldAMM.vy", compiler_args=compiler_args) +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) +VE_DELEGATION_MOCK_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "VEDelegationMock.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) +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) +OPTIMIZE_MATH_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "OptimizeMath.vy", compiler_args=compiler_args_default) +TEST_PACKING_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "TestPacking.vy", compiler_args=compiler_args_default) +OLD_AMM_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "OldAMM.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) -MOCK_CRYPTO_SWAP_DEPLOYER = boa.load_partial(PRICE_ORACLES_CONTRACT_PATH + "lp-oracles/testing/MockCryptoSwap.vy", compiler_args=compiler_args) -MOCK_STABLE_SWAP_NO_ARGUMENT_DEPLOYER = boa.load_partial(PRICE_ORACLES_CONTRACT_PATH + "lp-oracles/testing/MockStableSwapNoArgument.vy", compiler_args=compiler_args) +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) -CURVE_STABLESWAP_NG_DEPLOYER = boa.load_partial(STABLESWAP_NG_PATH + "CurveStableSwapNG.vy", compiler_args=compiler_args) -CURVE_STABLESWAP_NG_MATH_DEPLOYER = boa.load_partial(STABLESWAP_NG_PATH + "CurveStableSwapNGMath.vy", compiler_args=compiler_args) -CURVE_STABLESWAP_NG_VIEWS_DEPLOYER = boa.load_partial(STABLESWAP_NG_PATH + "CurveStableSwapNGViews.vy", compiler_args=compiler_args) \ No newline at end of file +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) \ No newline at end of file From d7ba1d84af8b12342d6ef9fed395aeb05d0600d8 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 13 Aug 2025 14:11:51 +0200 Subject: [PATCH 098/413] perf: improve codesize --- contracts/Controller.vy | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 54508c78..21ca7abe 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -30,7 +30,6 @@ MAX_AMM_FEE: immutable( uint256 ) # let's set to MIN_TICKS / A: for example, 4% max fee for A=100 A: immutable(uint256) -Aminus1: immutable(uint256) LOGN_A_RATIO: immutable(int256) # log(A / (A - 1)) SQRT_BAND_RATIO: immutable(uint256) @@ -137,7 +136,6 @@ def __init__( AMM = _AMM A = staticcall AMM.A() - Aminus1 = A - 1 LOGN_A_RATIO = math._wad_ln(convert(A * WAD // (A - 1), int256)) SQRT_BAND_RATIO = isqrt(10**36 * A // (A - 1)) @@ -393,7 +391,7 @@ def get_y_effective( 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 From b9e4aa71877c2ab31bc86439ee169722c18780ab Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 13 Aug 2025 15:56:05 +0200 Subject: [PATCH 099/413] fix: I hate eip170 --- contracts/Controller.vy | 22 ++++++++++++++-------- contracts/lending/LLController.vy | 3 ++- tests/controller/test_set_price_oracle.py | 3 ++- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 21ca7abe..9dea3c28 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -234,7 +234,7 @@ def set_price_oracle(price_oracle: IPriceOracle, max_deviation: uint256): 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, "deviation>max" + assert delta <= max_delta # dev: deviation > max extcall AMM.set_price_oracle(price_oracle) @@ -623,6 +623,12 @@ def transfer(token: IERC20, _to: address, amount: uint256): assert extcall token.transfer(_to, amount, default_return_value=True) +@internal +@view +def _check_loan_exists(debt: uint256): + assert debt > 0, "No loan" + + @internal def execute_callback( callbacker: address, @@ -779,11 +785,11 @@ def _add_collateral_borrow( 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 xy: uint256[2] = extcall AMM.withdraw(_for, WAD) - assert xy[0] == 0, "Already in underwater mode" + assert xy[0] == 0, "Underwater" if remove_collateral: xy[1] -= d_collateral else: @@ -966,7 +972,7 @@ def repay( 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) approval: bool = self._check_approval(_for) xy: uint256[2] = empty(uint256[2]) @@ -1102,7 +1108,7 @@ def _health( @param liquidation_discount Liquidation discount to use (can be 0) @return Health: > 0 = good. """ - assert debt > 0, "Loan doesn't exist" + self._check_loan_exists(debt) health: int256 = SWAD - convert(liquidation_discount, int256) health = ( unsafe_div( @@ -1165,7 +1171,7 @@ def health_calculator( collateral: int256 = 0 x_eff: int256 = 0 debt += d_debt - assert debt > 0, "Non-positive debt" + assert debt > 0, "debt<0" active_band: int256 = staticcall AMM.active_band_with_skip() @@ -1256,7 +1262,7 @@ def liquidate( if health_limit != 0: assert ( self._health(user, debt, True, health_limit) < 0 - ), "Not enough rekt" + ) # dev: not enough rekt final_debt: uint256 = debt # TODO shouldn't clamp max @@ -1303,7 +1309,7 @@ def liquidate( debt, calldata, ) - assert cb.stablecoins >= to_repay, "not enough proceeds" + assert cb.stablecoins >= to_repay, "no enough proceeds" if cb.stablecoins > to_repay: self.transferFrom( BORROWED_TOKEN, diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy index 2cdd4b91..12ae4476 100644 --- a/contracts/lending/LLController.vy +++ b/contracts/lending/LLController.vy @@ -68,6 +68,7 @@ exports: ( core.set_borrowing_discounts, core.set_callback, core.set_monetary_policy, + core.set_price_oracle, # For backward compatibility core.minted, core.redeemed, @@ -256,5 +257,5 @@ def set_admin_fee(admin_fee: uint256): @param admin_fee The fee which should be no higher than MAX_ADMIN_FEE """ core._check_admin() - assert admin_fee <= MAX_ADMIN_FEE, "admin_fee is higher than MAX_ADMIN_FEE" + assert admin_fee <= MAX_ADMIN_FEE # dev: admin_fee is higher than MAX_ADMIN_FEE self.admin_fee = admin_fee diff --git a/tests/controller/test_set_price_oracle.py b/tests/controller/test_set_price_oracle.py index c23c81b2..48fd6e8e 100644 --- a/tests/controller/test_set_price_oracle.py +++ b/tests/controller/test_set_price_oracle.py @@ -9,6 +9,7 @@ ) 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 @@ -171,7 +172,7 @@ def test_price_deviation_check_exceeds_limit(controller, different_price_oracle, # 10% price difference, but only 5% max deviation allowed max_deviation = 5 * 10**16 # 5% - with boa.reverts("deviation>max"): + with boa.reverts(dev="deviation > max"): controller.set_price_oracle(different_price_oracle, max_deviation, sender=admin) From 086d5ea262aabf5f83222dbc71e383f7b820b055 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 14 Aug 2025 17:32:18 +0200 Subject: [PATCH 100/413] test: replace mint for testing with deal --- tests/amm/test_amount_for_price.py | 8 +++--- tests/amm/test_deposit_withdraw.py | 2 +- tests/amm/test_exchange.py | 8 +++--- tests/amm/test_exchange_dy.py | 10 +++---- tests/amm/test_flip.py | 6 ++--- tests/amm/test_flip_dy.py | 6 ++--- tests/amm/test_oracle_change_noloss.py | 22 ++++++++-------- tests/amm/test_st_exchange.py | 6 ++--- tests/amm/test_st_exchange_dy.py | 6 ++--- tests/amm/test_xdown_yup_invariants.py | 26 +++++++++---------- tests/amm/test_xdown_yup_invariants_dy.py | 14 +++++----- tests/boosted_lm_callback/conftest.py | 2 +- tests/boosted_lm_callback/test_as_gauge.py | 10 +++---- tests/boosted_lm_callback/test_lm_callback.py | 12 ++++----- tests/boosted_lm_callback/test_st_as_gauge.py | 2 +- .../test_st_lm_callback.py | 2 +- tests/lending/conftest.py | 4 +-- tests/lending/test_bigfuzz.py | 22 ++++++++-------- .../lending/test_health_calculator_create.py | 2 +- .../test_health_calculator_stateful.py | 6 ++--- tests/lending/test_health_in_trades.py | 6 ++--- tests/lending/test_max_borrowable.py | 4 +-- tests/lending/test_monetary_policy.py | 2 +- tests/lending/test_oracle_attack.py | 6 ++--- tests/lending/test_secondary_mpolicy.py | 2 +- .../lending/test_st_interest_conservation.py | 10 +++---- tests/lending/test_vault.py | 10 +++---- tests/lm_callback/conftest.py | 2 +- tests/lm_callback/test_add_new_lm_callback.py | 4 +-- tests/lm_callback/test_as_gauge.py | 8 +++--- tests/lm_callback/test_lm_callback.py | 12 ++++----- tests/lm_callback/test_lm_callback_reverts.py | 2 +- tests/lm_callback/test_rewards_kill_unkill.py | 4 +-- tests/lm_callback/test_st_as_gauge.py | 2 +- tests/lm_callback/test_st_lm_callback.py | 2 +- tests/stableborrow/conftest.py | 2 +- tests/stableborrow/stabilize/conftest.py | 10 +++---- .../stateful/test_agg_monetary_policy.py | 2 +- .../stabilize/unitary/test_pk_profit.py | 2 +- .../unitary/test_price_aggregation.py | 4 +-- .../test_price_aggregation_with_chainlink.py | 2 +- tests/stableborrow/test_approval.py | 10 +++---- tests/stableborrow/test_bigfuzz.py | 10 +++---- tests/stableborrow/test_create_repay.py | 6 ++--- .../test_create_repay_stateful.py | 6 ++--- tests/stableborrow/test_extra_health.py | 2 +- tests/stableborrow/test_leverage.py | 6 ++--- tests/stableborrow/test_liquidate.py | 6 ++--- tests/stableborrow/test_lm_callback.py | 2 +- .../test_st_interest_conservation.py | 6 ++--- tests/swap/conftest.py | 4 +-- tests/swap/test_price.py | 4 +-- 52 files changed, 168 insertions(+), 168 deletions(-) diff --git a/tests/amm/test_amount_for_price.py b/tests/amm/test_amount_for_price.py index 1d34e2f5..ba5ed42d 100644 --- a/tests/amm/test_amount_for_price.py +++ b/tests/amm/test_amount_for_price.py @@ -25,7 +25,7 @@ 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) + boa.deal(collateral_token, amm.address, deposit_amount) prices = [oracle_price] prices.append(amm.get_p()) @@ -34,7 +34,7 @@ def test_amount_for_price(price_oracle, amm, accounts, collateral_token, borrowe # 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) + boa.deal(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,11 +49,11 @@ 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) + boa.deal(borrowed_token, user, amount) amm.exchange(0, 1, amount, 0) else: - collateral_token._mint_for_testing(user, amount) + boa.deal(collateral_token, user, amount) amm.exchange(1, 0, amount, 0) p = amm.get_p() diff --git a/tests/amm/test_deposit_withdraw.py b/tests/amm/test_deposit_withdraw.py index bd30f7ca..8ce61952 100644 --- a/tests/amm/test_deposit_withdraw.py +++ b/tests/amm/test_deposit_withdraw.py @@ -28,7 +28,7 @@ def test_deposit_withdraw(amm, amounts, accounts, ns, dns, fracs, collateral_tok amm.deposit_range(user, amount, n1, n2) else: amm.deposit_range(user, amount, n1, n2) - collateral_token._mint_for_testing(amm.address, amount) + boa.deal(collateral_token, amm.address, amount) deposits[user] = amount assert collateral_token.balanceOf(user) == 0 diff --git a/tests/amm/test_exchange.py b/tests/amm/test_exchange.py index 229b1e79..7df7582d 100644 --- a/tests/amm/test_exchange.py +++ b/tests/amm/test_exchange.py @@ -14,7 +14,7 @@ def test_dxdy_limits(amm, amounts, accounts, ns, dns, collateral_token, 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) + boa.deal(collateral_token, amm.address, amount) # Swap 0 dx, dy = amm.get_dxdy(0, 1, 0) @@ -60,7 +60,7 @@ def test_exchange_down_up(amm, amounts, accounts, ns, dns, amount, amm.deposit_range(user, amount, n1, n2) else: amm.deposit_range(user, amount, n1, n2) - collateral_token._mint_for_testing(amm.address, amount) + boa.deal(collateral_token, amm.address, amount) p_before = amm.get_p() @@ -69,7 +69,7 @@ def test_exchange_down_up(amm, amounts, accounts, ns, dns, 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) + boa.deal(borrowed_token, u, dx2) with boa.env.prank(u): amm.exchange(0, 1, dx2, 0) assert borrowed_token.balanceOf(u) == 0 @@ -91,7 +91,7 @@ def test_exchange_down_up(amm, amounts, accounts, ns, dns, amount, 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)) + boa.deal(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..40d1e2c0 100644 --- a/tests/amm/test_exchange_dy.py +++ b/tests/amm/test_exchange_dy.py @@ -29,7 +29,7 @@ def test_dydx_limits(amm, amounts, accounts, ns, dns, collateral_token, admin, b 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) + boa.deal(collateral_token, amm.address, amount) # Swap 0 dx, dy = amm.get_dydx(0, 1, 0) @@ -77,7 +77,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) + boa.deal(collateral_token, amm.address, amount) # Swap 0 dy, dx = amm.get_dydx(0, 1, 0) @@ -142,7 +142,7 @@ def test_exchange_dy_down_up(amm, amounts, accounts, ns, dns, amount, borrowed_t amm.deposit_range(user, amount, n1, n2) else: amm.deposit_range(user, amount, n1, n2) - collateral_token._mint_for_testing(amm.address, amount) + boa.deal(collateral_token, amm.address, amount) p_before = amm.get_p() @@ -152,7 +152,7 @@ def test_exchange_dy_down_up(amm, amounts, accounts, ns, dns, amount, borrowed_t dy2, dx2 = amm.get_dydx(0, 1, dy) assert dy == dy2 assert approx(dx, dx2, 1e-6) - borrowed_token._mint_for_testing(u, dx2) + boa.deal(borrowed_token, u, dx2) with boa.env.prank(u): with boa.reverts("Slippage"): amm.exchange_dy(0, 1, dy2, dx2 - 1) # crvUSD --> ETH @@ -177,7 +177,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)) + boa.deal(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..732a44b2 100644 --- a/tests/amm/test_flip.py +++ b/tests/amm/test_flip.py @@ -19,7 +19,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) + boa.deal(collateral_token, amm.address, AMOUNT_D) p = amm.price_oracle() initial_y = sum(amm.bands_y(n) for n in range(1, 6)) @@ -36,7 +36,7 @@ def test_flip(amm, price_oracle, collateral_token, borrowed_token, accounts, adm 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) + boa.deal(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)) @@ -64,7 +64,7 @@ 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) + boa.deal(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)) diff --git a/tests/amm/test_flip_dy.py b/tests/amm/test_flip_dy.py index ab6752b6..87a121af 100644 --- a/tests/amm/test_flip_dy.py +++ b/tests/amm/test_flip_dy.py @@ -33,7 +33,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) + boa.deal(collateral_token, amm.address, amount_d) p = amm.price_oracle() initial_y = sum(amm.bands_y(n) for n in range(1, 6)) @@ -53,7 +53,7 @@ def test_flip(amm, price_oracle, collateral_token, borrowed_token, accounts, adm 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) + boa.deal(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)) @@ -88,7 +88,7 @@ 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) + boa.deal(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)) diff --git a/tests/amm/test_oracle_change_noloss.py b/tests/amm/test_oracle_change_noloss.py index 5aed7361..811bc6f5 100644 --- a/tests/amm/test_oracle_change_noloss.py +++ b/tests/amm/test_oracle_change_noloss.py @@ -31,10 +31,10 @@ def test_buy_with_shift(amm, collateral_token, borrowed_token, price_oracle, acc # Deposit with boa.env.prank(admin): amm.deposit_range(user, collateral_amount, n1, n1 + dn) - collateral_token._mint_for_testing(amm.address, collateral_amount) + boa.deal(collateral_token, amm.address, collateral_amount) # Swap stablecoin for collateral - borrowed_token._mint_for_testing(user, amount) + boa.deal(borrowed_token, user, amount) with boa.env.prank(user): amm.exchange(0, 1, amount, 0) b = borrowed_token.balanceOf(user) @@ -49,7 +49,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 + boa.deal(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 @@ -78,10 +78,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) + boa.deal(collateral_token, amm.address, collateral_amount) # Swap max (buy) - borrowed_token._mint_for_testing(user, MANY) + boa.deal(borrowed_token, user, MANY) with boa.env.prank(user): amm.exchange(0, 1, MANY, 0) @@ -125,10 +125,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) + boa.deal(collateral_token, amm.address, collateral_amount) # Swap stablecoin for collateral - borrowed_token._mint_for_testing(user, amount) + boa.deal(borrowed_token, user, amount) with boa.env.prank(user): amm.exchange(0, 1, amount, 0) b = borrowed_token.balanceOf(user) @@ -143,7 +143,7 @@ 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 + boa.deal(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 @@ -170,10 +170,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) + boa.deal(collateral_token, amm.address, collateral_amount) # Swap stablecoin for collateral - borrowed_token._mint_for_testing(user, amount) + boa.deal(borrowed_token, user, amount) with boa.env.prank(user): amm.exchange(0, 1, amount, 0) b = borrowed_token.balanceOf(user) @@ -188,7 +188,7 @@ 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 + boa.deal(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) diff --git a/tests/amm/test_st_exchange.py b/tests/amm/test_st_exchange.py index 3c77b394..29f8692e 100644 --- a/tests/amm/test_st_exchange.py +++ b/tests/amm/test_st_exchange.py @@ -30,7 +30,7 @@ def initializer(self, amounts, ns, dns): 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) + boa.deal(collateral_token, self.amm.address, amount) except Exception as e: if 'Amount too low' in str(e): assert amount // (dn + 1) <= 100 @@ -53,7 +53,7 @@ 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) + boa.deal(in_token, u, amount - u_amount) with boa.env.prank(u): self.amm.exchange(i, j, amount, 0) @@ -88,7 +88,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) + boa.deal(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)) diff --git a/tests/amm/test_st_exchange_dy.py b/tests/amm/test_st_exchange_dy.py index c6200163..1f00538c 100644 --- a/tests/amm/test_st_exchange_dy.py +++ b/tests/amm/test_st_exchange_dy.py @@ -30,7 +30,7 @@ def initializer(self, amounts, ns, dns): 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) + boa.deal(collateral_token, self.amm.address, amount) except Exception as e: if 'Amount too low' in str(e): assert amount // (dn + 1) <= 100 @@ -54,7 +54,7 @@ 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) + boa.deal(in_token, u, required_amount - u_amount) with boa.env.prank(u): self.amm.exchange_dy(i, j, reduced_amount, required_amount) @@ -92,7 +92,7 @@ def teardown(self): if n < 50: dy, dx = self.amm.get_dydx(1, 0, to_receive) if dy > 0: - self.collateral_token._mint_for_testing(u, dx) + boa.deal(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)) diff --git a/tests/amm/test_xdown_yup_invariants.py b/tests/amm/test_xdown_yup_invariants.py index 3c0efd2b..4e32711a 100644 --- a/tests/amm/test_xdown_yup_invariants.py +++ b/tests/amm/test_xdown_yup_invariants.py @@ -25,11 +25,11 @@ def test_immediate(amm, price_oracle, collateral_token, borrowed_token, accounts 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) + boa.deal(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) + boa.deal(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 +46,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) + boa.deal(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) + boa.deal(collateral_token, user, trade_amount) i = 1 j = 0 @@ -80,13 +80,13 @@ def test_immediate_above_p0(amm, price_oracle, collateral_token, borrowed_token, 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) + boa.deal(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) + boa.deal(borrowed_token, user, pump_amount) amm.exchange(0, 1, pump_amount, 0) p_after_1 = amm.get_p() @@ -94,7 +94,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) + boa.deal(collateral_token, user, trade_amount) with boa.env.prank(user): amm.exchange(1, 0, trade_amount, 0) @@ -122,13 +122,13 @@ def test_immediate_in_band(amm, price_oracle, collateral_token, borrowed_token, 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) + boa.deal(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) + boa.deal(borrowed_token, user, pump_amount) amm.exchange(0, 1, pump_amount, 0) p_after_1 = amm.get_p() @@ -137,7 +137,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) + boa.deal(borrowed_token, user, trade_amount) amm.exchange(0, 1, trade_amount, 0) p_after_2 = amm.get_p() @@ -172,7 +172,7 @@ def test_adiabatic(amm, price_oracle, collateral_token, borrowed_token, accounts 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) + boa.deal(collateral_token, amm.address, deposit_amount) for i in range(2): boa.env.time_travel(600) price_oracle.set_price(p_o_1) @@ -198,11 +198,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) + boa.deal(borrowed_token, user, amount) else: i = 1 j = 0 - collateral_token._mint_for_testing(user, amount) + boa.deal(collateral_token, user, amount) with boa.env.prank(user): amm.exchange(i, j, amount, 0) diff --git a/tests/amm/test_xdown_yup_invariants_dy.py b/tests/amm/test_xdown_yup_invariants_dy.py index 48815600..f0c5cb6b 100644 --- a/tests/amm/test_xdown_yup_invariants_dy.py +++ b/tests/amm/test_xdown_yup_invariants_dy.py @@ -41,7 +41,7 @@ def test_immediate(amm, price_oracle, collateral_token, borrowed_token, accounts 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) + boa.deal(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 +53,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) + boa.deal(borrowed_token, user, pump_amount) amm.exchange_dy(0, 1, pump_recv_amount, pump_amount) prices.append(amm.get_p()) @@ -63,14 +63,14 @@ 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) + boa.deal(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, trade_amount = amm.get_dydx(1, 0, trade_recv_amount) with boa.env.prank(user): - collateral_token._mint_for_testing(user, trade_amount) + boa.deal(collateral_token, user, trade_amount) i = 1 j = 0 @@ -108,7 +108,7 @@ def test_adiabatic(amm, price_oracle, collateral_token, borrowed_token, accounts 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) + boa.deal(collateral_token, amm.address, deposit_amount) for i in range(2): boa.env.time_travel(600) price_oracle.set_price(p_o_1) @@ -136,13 +136,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) + boa.deal(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) + boa.deal(collateral_token, user, _amount) with boa.env.prank(user): amm.exchange_dy(i, j, recv_amount, _amount) diff --git a/tests/boosted_lm_callback/conftest.py b/tests/boosted_lm_callback/conftest.py index bab16fc9..45dac3c5 100644 --- a/tests/boosted_lm_callback/conftest.py +++ b/tests/boosted_lm_callback/conftest.py @@ -51,7 +51,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 diff --git a/tests/boosted_lm_callback/test_as_gauge.py b/tests/boosted_lm_callback/test_as_gauge.py index 985e4d7a..ca3f0454 100644 --- a/tests/boosted_lm_callback/test_as_gauge.py +++ b/tests/boosted_lm_callback/test_as_gauge.py @@ -26,7 +26,7 @@ def test_gauge_integral_one_user(accounts, admin, collateral_token, crv, boosted 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 @@ -114,8 +114,8 @@ def test_gauge_integral(accounts, admin, collateral_token, crv, boosted_lm_callb # 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 @@ -255,8 +255,8 @@ def test_mining_with_votelock( # 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) collateral_token.approve(market_controller.address, MAX_UINT256, sender=alice) collateral_token.approve(market_controller.address, MAX_UINT256, sender=bob) diff --git a/tests/boosted_lm_callback/test_lm_callback.py b/tests/boosted_lm_callback/test_lm_callback.py index eddb9eb3..f0f8aa78 100644 --- a/tests/boosted_lm_callback/test_lm_callback.py +++ b/tests/boosted_lm_callback/test_lm_callback.py @@ -30,8 +30,8 @@ def test_simple_exchange( # 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) @@ -95,8 +95,8 @@ 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 @@ -289,8 +289,8 @@ def test_full_repay_underwater( # 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) diff --git a/tests/boosted_lm_callback/test_st_as_gauge.py b/tests/boosted_lm_callback/test_st_as_gauge.py index 3898e34e..5e8e36b1 100644 --- a/tests/boosted_lm_callback/test_st_as_gauge.py +++ b/tests/boosted_lm_callback/test_st_as_gauge.py @@ -202,7 +202,7 @@ def test_state_machine( ): for acct in accounts[:5]: with boa.env.prank(admin): - collateral_token._mint_for_testing(acct, 1000 * 10**18) + boa.deal(collateral_token, acct, 1000 * 10**18) crv.transfer(acct, 10 ** 20) with boa.env.prank(acct): diff --git a/tests/boosted_lm_callback/test_st_lm_callback.py b/tests/boosted_lm_callback/test_st_lm_callback.py index e12315cb..b2bf5e4e 100644 --- a/tests/boosted_lm_callback/test_st_lm_callback.py +++ b/tests/boosted_lm_callback/test_st_lm_callback.py @@ -293,7 +293,7 @@ def test_state_machine( ): for acct in accounts[:5]: with boa.env.prank(admin): - collateral_token._mint_for_testing(acct, 1000 * 10**18) + boa.deal(collateral_token, acct, 1000 * 10**18) crv.transfer(acct, 10 ** 20) with boa.env.prank(acct): diff --git a/tests/lending/conftest.py b/tests/lending/conftest.py index 9caf98a8..db4d7e58 100644 --- a/tests/lending/conftest.py +++ b/tests/lending/conftest.py @@ -150,7 +150,7 @@ def mock_token_interface(): 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) + boa.deal(borrowed_token, admin, amount) borrowed_token.approve(vault.address, 2**256 - 1) vault.deposit(amount) return market_controller @@ -161,5 +161,5 @@ def fake_leverage(collateral_token, borrowed_token, market_controller, admin): with boa.env.prank(admin): leverage = FAKE_LEVERAGE_DEPLOYER.deploy(borrowed_token.address, collateral_token.address, market_controller.address, 3000 * 10**18) - collateral_token._mint_for_testing(leverage.address, 1000 * 10**collateral_token.decimals()) + boa.deal(collateral_token, leverage.address, 1000 * 10**collateral_token.decimals()) return leverage diff --git a/tests/lending/test_bigfuzz.py b/tests/lending/test_bigfuzz.py index b27484b4..2d92494c 100644 --- a/tests/lending/test_bigfuzz.py +++ b/tests/lending/test_bigfuzz.py @@ -66,7 +66,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(): @@ -93,7 +93,7 @@ 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) + 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(): @@ -137,7 +137,7 @@ def repay(self, ratio, uid): 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(): @@ -159,7 +159,7 @@ 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: @@ -205,7 +205,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): @@ -256,10 +256,10 @@ def trade_to_price(self, p): 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) + boa.deal(self.borrowed_token, user, amount) 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) @@ -268,11 +268,11 @@ 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) + boa.deal(self.borrowed_token, 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) + boa.deal(self.collateral_token, user, amount) self.market_amm.exchange(1, 0, amount, 0) @rule(emode=extended_mode, frac=liquidate_frac) @@ -288,7 +288,7 @@ def self_liquidate_and_health(self, emode, frac): debt = self.market_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( @@ -328,7 +328,7 @@ def liquidate(self, uid, luid, emode, frac): debt = self.market_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(): diff --git a/tests/lending/test_health_calculator_create.py b/tests/lending/test_health_calculator_create.py index 887b60da..5dda2587 100644 --- a/tests/lending/test_health_calculator_create.py +++ b/tests/lending/test_health_calculator_create.py @@ -18,7 +18,7 @@ def test_health_calculator_create(market_amm, filled_controller, collateral_toke except Exception: calculator_fail = True - collateral_token._mint_for_testing(user, collateral) + boa.deal(collateral_token, user, collateral) with boa.env.prank(user): try: diff --git a/tests/lending/test_health_calculator_stateful.py b/tests/lending/test_health_calculator_stateful.py index 1656e722..2632b993 100644 --- a/tests/lending/test_health_calculator_stateful.py +++ b/tests/lending/test_health_calculator_stateful.py @@ -104,7 +104,7 @@ def create_loan(self, c_amount, amount_frac, n, user_id): return try: - self.collateral_token._mint_for_testing(user, c_amount) + boa.deal(collateral_token, user, c_amount) except Exception: return # Probably overflow @@ -155,7 +155,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() @@ -185,7 +185,7 @@ def borrow_more(self, c_amount, amount_frac, user_id): 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() diff --git a/tests/lending/test_health_in_trades.py b/tests/lending/test_health_in_trades.py index 0386797e..761d2ea7 100644 --- a/tests/lending/test_health_in_trades.py +++ b/tests/lending/test_health_in_trades.py @@ -39,7 +39,7 @@ def initializer(self, collateral_amount, n): n)) user = self.accounts[0] with boa.env.prank(user): - self.collateral._mint_for_testing(user, collateral_amount) + boa.deal(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 +59,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(collateral, user, amount) self.amm.exchange(1, 0, amount, 0) @rule(oracle_step=oracle_step) @@ -79,7 +79,7 @@ def random_trade(self, amount_fraction, is_pump): self.amm.exchange(0, 1, amount, 0) else: amount = int(self.collateral_amount * amount_fraction) - self.collateral._mint_for_testing(user, amount) + boa.deal(collateral, user, amount) self.amm.exchange(1, 0, amount, 0) @rule(t=t) diff --git a/tests/lending/test_max_borrowable.py b/tests/lending/test_max_borrowable.py index c5621438..9582e7e6 100644 --- a/tests/lending/test_max_borrowable.py +++ b/tests/lending/test_max_borrowable.py @@ -17,7 +17,7 @@ def test_max_borrowable(borrowed_token, collateral_token, market_amm, filled_con # 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) + boa.deal(collateral_token, 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) @@ -54,7 +54,7 @@ def test_min_collateral(borrowed_token, collateral_token, market_amm, filled_con # 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) + boa.deal(collateral_token, 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) diff --git a/tests/lending/test_monetary_policy.py b/tests/lending/test_monetary_policy.py index 3ab75317..35a4ccc4 100644 --- a/tests/lending/test_monetary_policy.py +++ b/tests/lending/test_monetary_policy.py @@ -16,7 +16,7 @@ def test_monetary_policy(filled_controller, collateral_token, borrowed_token, ma 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) + boa.deal(collateral_token, admin, c_amount) if to_borrow > available: with boa.reverts(): filled_controller.create_loan(c_amount, to_borrow, 5) diff --git a/tests/lending/test_oracle_attack.py b/tests/lending/test_oracle_attack.py index 53b38788..90e0bbc4 100644 --- a/tests/lending/test_oracle_attack.py +++ b/tests/lending/test_oracle_attack.py @@ -147,21 +147,21 @@ def template_vuln_test(vault, controller, amm, admin, borrowed_token, price_orac # 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) + boa.deal(borrowed_token, admin, b_amount) borrowed_token.approve(vault.address, MAX) 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) + 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 with boa.env.prank(hacker): - borrowed_token._mint_for_testing(hacker, hacker_crvusd_reserves) + boa.deal(borrowed_token, hacker, hacker_crvusd_reserves) spent, received = amm.exchange(0, 1, hacker_crvusd_reserves, 0) # print(f"Bought {received/1e18:.3f} for {spent/1e18:.2f}") diff --git a/tests/lending/test_secondary_mpolicy.py b/tests/lending/test_secondary_mpolicy.py index cd3c7833..a66fb174 100644 --- a/tests/lending/test_secondary_mpolicy.py +++ b/tests/lending/test_secondary_mpolicy.py @@ -72,7 +72,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_st_interest_conservation.py b/tests/lending/test_st_interest_conservation.py index 7a94013d..32c8dda5 100644 --- a/tests/lending/test_st_interest_conservation.py +++ b/tests/lending/test_st_interest_conservation.py @@ -32,7 +32,7 @@ def __init__(self): 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) @@ -67,7 +67,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 @@ -103,7 +103,7 @@ def repay(self, amount, 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: @@ -123,7 +123,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 @@ -157,7 +157,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 diff --git a/tests/lending/test_vault.py b/tests/lending/test_vault.py index 0a2aee82..85b950ab 100644 --- a/tests/lending/test_vault.py +++ b/tests/lending/test_vault.py @@ -33,7 +33,7 @@ def test_deposit_and_withdraw(vault, borrowed_token, accounts, admin, supply_lim 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): @@ -99,7 +99,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) @@ -123,7 +123,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) @@ -146,7 +146,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): @@ -169,7 +169,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): diff --git a/tests/lm_callback/conftest.py b/tests/lm_callback/conftest.py index 569be144..259a394d 100644 --- a/tests/lm_callback/conftest.py +++ b/tests/lm_callback/conftest.py @@ -50,7 +50,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 diff --git a/tests/lm_callback/test_add_new_lm_callback.py b/tests/lm_callback/test_add_new_lm_callback.py index c5b71b01..132e2d9a 100644 --- a/tests/lm_callback/test_add_new_lm_callback.py +++ b/tests/lm_callback/test_add_new_lm_callback.py @@ -24,9 +24,9 @@ def test_add_new_lm_callback( 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 diff --git a/tests/lm_callback/test_as_gauge.py b/tests/lm_callback/test_as_gauge.py index 69ee439e..261c881e 100644 --- a/tests/lm_callback/test_as_gauge.py +++ b/tests/lm_callback/test_as_gauge.py @@ -18,7 +18,7 @@ def test_gauge_integral_one_user(accounts, admin, collateral_token, crv, lm_call 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 @@ -102,8 +102,8 @@ 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 @@ -241,7 +241,7 @@ def test_set_killed( 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) diff --git a/tests/lm_callback/test_lm_callback.py b/tests/lm_callback/test_lm_callback.py index e3d4599b..05565da4 100644 --- a/tests/lm_callback/test_lm_callback.py +++ b/tests/lm_callback/test_lm_callback.py @@ -23,8 +23,8 @@ def test_simple_exchange( # 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) @@ -88,8 +88,8 @@ 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 @@ -280,8 +280,8 @@ def test_full_repay_underwater( # 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) diff --git a/tests/lm_callback/test_lm_callback_reverts.py b/tests/lm_callback/test_lm_callback_reverts.py index 8d651fd0..3895b936 100644 --- a/tests/lm_callback/test_lm_callback_reverts.py +++ b/tests/lm_callback/test_lm_callback_reverts.py @@ -21,7 +21,7 @@ def test_add_new_lm_callback( 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)] diff --git a/tests/lm_callback/test_rewards_kill_unkill.py b/tests/lm_callback/test_rewards_kill_unkill.py index e7a2464e..a7c58260 100644 --- a/tests/lm_callback/test_rewards_kill_unkill.py +++ b/tests/lm_callback/test_rewards_kill_unkill.py @@ -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) @@ -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) diff --git a/tests/lm_callback/test_st_as_gauge.py b/tests/lm_callback/test_st_as_gauge.py index 85c0d008..c71ed3f8 100644 --- a/tests/lm_callback/test_st_as_gauge.py +++ b/tests/lm_callback/test_st_as_gauge.py @@ -178,7 +178,7 @@ def test_state_machine( ): for acct in accounts[:5]: with boa.env.prank(admin): - collateral_token._mint_for_testing(acct, 1000 * 10**18) + boa.deal(collateral_token, acct, 1000 * 10**18) crv.transfer(acct, 10 ** 20) with boa.env.prank(acct): diff --git a/tests/lm_callback/test_st_lm_callback.py b/tests/lm_callback/test_st_lm_callback.py index 8db32670..aea0f8de 100644 --- a/tests/lm_callback/test_st_lm_callback.py +++ b/tests/lm_callback/test_st_lm_callback.py @@ -253,7 +253,7 @@ def test_state_machine( ): for acct in accounts[:5]: with boa.env.prank(admin): - collateral_token._mint_for_testing(acct, 1000 * 10**18) + boa.deal(collateral_token, acct, 1000 * 10**18) crv.transfer(acct, 10 ** 20) with boa.env.prank(acct): diff --git a/tests/stableborrow/conftest.py b/tests/stableborrow/conftest.py index 7f9cfd91..89e00d48 100644 --- a/tests/stableborrow/conftest.py +++ b/tests/stableborrow/conftest.py @@ -126,7 +126,7 @@ def f(collateral_token, market_controller): with boa.env.prank(admin): leverage = FAKE_LEVERAGE_DEPLOYER.deploy(stablecoin.address, collateral_token.address, market_controller.address, 3000 * 10**18) - collateral_token._mint_for_testing(leverage.address, 1000 * 10**collateral_token.decimals()) + 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 0db1ed93..66ab1705 100644 --- a/tests/stableborrow/stabilize/conftest.py +++ b/tests/stableborrow/stabilize/conftest.py @@ -169,8 +169,8 @@ def dummy_tricrypto(stablecoin_a, 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) @@ -308,14 +308,14 @@ 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) else: market_controller_agg.borrow_more(collateral_amount, amount) else: - coin._mint_for_testing(acct, amount) + boa.deal(coin, acct, amount) return f @@ -337,7 +337,7 @@ def provide_token_to_peg_keepers_no_sleep(initial_amounts, swaps, peg_keepers, r # 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) diff --git a/tests/stableborrow/stabilize/stateful/test_agg_monetary_policy.py b/tests/stableborrow/stabilize/stateful/test_agg_monetary_policy.py index bc24c2ae..260cc7ec 100644 --- a/tests/stableborrow/stabilize/stateful/test_agg_monetary_policy.py +++ b/tests/stableborrow/stabilize/stateful/test_agg_monetary_policy.py @@ -117,7 +117,7 @@ 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]) self.swaps[n].add_liquidity(x, 0) # Add twice to record the price for MA self.swaps[n].add_liquidity(x, 0) diff --git a/tests/stableborrow/stabilize/unitary/test_pk_profit.py b/tests/stableborrow/stabilize/unitary/test_pk_profit.py index 17703123..b2c58828 100644 --- a/tests/stableborrow/stabilize/unitary/test_pk_profit.py +++ b/tests/stableborrow/stabilize/unitary/test_pk_profit.py @@ -24,7 +24,7 @@ def _inner(amount, i=None): 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) diff --git a/tests/stableborrow/stabilize/unitary/test_price_aggregation.py b/tests/stableborrow/stabilize/unitary/test_price_aggregation.py index c00ce12b..1bcfa2cf 100644 --- a/tests/stableborrow/stabilize/unitary/test_price_aggregation.py +++ b/tests/stableborrow/stabilize/unitary/test_price_aggregation.py @@ -12,7 +12,7 @@ def test_price_aggregator(stableswap_a, stableswap_b, stablecoin_a, agg, admin): 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 @@ -47,7 +47,7 @@ def test_crypto_agg(dummy_tricrypto, crypto_agg, stableswap_a, stablecoin_a, adm assert approx(p, 1000 * 10**18, 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..f86498a1 100644 --- a/tests/stableborrow/stabilize/unitary/test_price_aggregation_with_chainlink.py +++ b/tests/stableborrow/stabilize/unitary/test_price_aggregation_with_chainlink.py @@ -65,7 +65,7 @@ def true_raw_price(internal_raw_price, external_oracle_price): ) 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..8898cf5b 100644 --- a/tests/stableborrow/test_approval.py +++ b/tests/stableborrow/test_approval.py @@ -10,7 +10,7 @@ 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) @@ -24,10 +24,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): @@ -141,7 +141,7 @@ 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) + 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) @@ -168,7 +168,7 @@ def f(sleep_time, user, someone_else): # Ensure approved account has enough to liquidate with boa.env.prank(someone_else): - collateral_token._mint_for_testing(someone_else, collateral_amount) + boa.deal(collateral_token, 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) diff --git a/tests/stableborrow/test_bigfuzz.py b/tests/stableborrow/test_bigfuzz.py index 652674d6..61ded52a 100644 --- a/tests/stableborrow/test_bigfuzz.py +++ b/tests/stableborrow/test_bigfuzz.py @@ -87,7 +87,7 @@ 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(): @@ -149,7 +149,7 @@ 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: @@ -195,7 +195,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): @@ -248,7 +248,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) @@ -271,7 +271,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) diff --git a/tests/stableborrow/test_create_repay.py b/tests/stableborrow/test_create_repay.py index e2cfe31e..0af6c8e9 100644 --- a/tests/stableborrow/test_create_repay.py +++ b/tests/stableborrow/test_create_repay.py @@ -10,7 +10,7 @@ def test_create_loan(controller_factory, stablecoin, collateral_token, market_co 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 @@ -69,7 +69,7 @@ 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) @@ -124,7 +124,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) diff --git a/tests/stableborrow/test_create_repay_stateful.py b/tests/stableborrow/test_create_repay_stateful.py index 358090fb..f1b40823 100644 --- a/tests/stableborrow/test_create_repay_stateful.py +++ b/tests/stableborrow/test_create_repay_stateful.py @@ -65,7 +65,7 @@ def create_loan(self, c_amount, amount, n, 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 @@ -110,7 +110,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 @@ -136,7 +136,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 diff --git a/tests/stableborrow/test_extra_health.py b/tests/stableborrow/test_extra_health.py index 3d501e1b..144e8f78 100644 --- a/tests/stableborrow/test_extra_health.py +++ b/tests/stableborrow/test_extra_health.py @@ -12,7 +12,7 @@ def test_create_loan(controller_factory, stablecoin, collateral_token, market_co 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 diff --git a/tests/stableborrow/test_leverage.py b/tests/stableborrow/test_leverage.py index 564fa346..1bf96841 100644 --- a/tests/stableborrow/test_leverage.py +++ b/tests/stableborrow/test_leverage.py @@ -12,7 +12,7 @@ def test_leverage(collateral_token, stablecoin, market_controller, market_amm, f controller_mint = stablecoin.balanceOf(market_controller.address) with boa.env.prank(user): - collateral_token._mint_for_testing(user, amount) + boa.deal(collateral_token, 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 @@ -43,7 +43,7 @@ def test_leverage_property(collateral_token, stablecoin, market_controller, mark 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: @@ -64,7 +64,7 @@ def test_leverage_property(collateral_token, stablecoin, market_controller, mark 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) + boa.deal(collateral_token, user, amount) market_controller.borrow_more_extended(amount, more_debt, fake_leverage.address, [0]) debt += more_debt if more_debt > 0: diff --git a/tests/stableborrow/test_liquidate.py b/tests/stableborrow/test_liquidate.py index 476c762b..3f24cb17 100644 --- a/tests/stableborrow/test_liquidate.py +++ b/tests/stableborrow/test_liquidate.py @@ -21,8 +21,8 @@ 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) + 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) @@ -111,7 +111,7 @@ 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) + 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) diff --git a/tests/stableborrow/test_lm_callback.py b/tests/stableborrow/test_lm_callback.py index a5e719e6..9d967c9a 100644 --- a/tests/stableborrow/test_lm_callback.py +++ b/tests/stableborrow/test_lm_callback.py @@ -22,7 +22,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) diff --git a/tests/stableborrow/test_st_interest_conservation.py b/tests/stableborrow/test_st_interest_conservation.py index caf9b068..95e5e320 100644 --- a/tests/stableborrow/test_st_interest_conservation.py +++ b/tests/stableborrow/test_st_interest_conservation.py @@ -67,7 +67,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 @@ -131,7 +131,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 @@ -165,7 +165,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 diff --git a/tests/swap/conftest.py b/tests/swap/conftest.py index edb94692..299acc2d 100644 --- a/tests/swap/conftest.py +++ b/tests/swap/conftest.py @@ -50,8 +50,8 @@ def swap(swap_deployer, swap_impl, redeemable_coin, volatile_coin, admin): @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) + boa.deal(redeemable_coin, admin, 10**6 * 10**6) + boa.deal(volatile_coin, 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) diff --git a/tests/swap/test_price.py b/tests/swap/test_price.py index 79386af3..072cc444 100644 --- a/tests/swap/test_price.py +++ b/tests/swap/test_price.py @@ -15,7 +15,7 @@ def test_price(swap_w_d, redeemable_coin, volatile_coin, accounts, amount, ix): 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.deal(from_coin, 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 @@ -34,7 +34,7 @@ def test_ema(swap_w_d, redeemable_coin, volatile_coin, accounts, amount, ix, dt0 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.deal(from_coin, user, amount) boa.env.time_travel(dt0) swap_w_d.exchange(ix, 1-ix, amount, 0) # Time didn't pass yet From dbe5f74e217ef1d2f431ac6dc826416386e03149 Mon Sep 17 00:00:00 2001 From: Alberto Date: Sun, 17 Aug 2025 13:57:21 +0200 Subject: [PATCH 101/413] test: remove useless tests This component has been deployed already and will never be redeployed again. Also brownie is deprecated. If crvEUR ever happens tests will have to be rewritten anyways. --- tests_brownie/__init__.py | 0 tests_brownie/stablecoin/__init__.py | 0 tests_brownie/stablecoin/conftest.py | 26 ---- tests_brownie/stablecoin/test_admin.py | 38 ----- tests_brownie/stablecoin/test_approve.py | 172 ---------------------- tests_brownie/stablecoin/test_burn.py | 62 -------- tests_brownie/stablecoin/test_mint.py | 24 --- tests_brownie/stablecoin/test_transfer.py | 79 ---------- 8 files changed, 401 deletions(-) delete mode 100644 tests_brownie/__init__.py delete mode 100644 tests_brownie/stablecoin/__init__.py delete mode 100644 tests_brownie/stablecoin/conftest.py delete mode 100644 tests_brownie/stablecoin/test_admin.py delete mode 100644 tests_brownie/stablecoin/test_approve.py delete mode 100644 tests_brownie/stablecoin/test_burn.py delete mode 100644 tests_brownie/stablecoin/test_mint.py delete mode 100644 tests_brownie/stablecoin/test_transfer.py diff --git a/tests_brownie/__init__.py b/tests_brownie/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests_brownie/stablecoin/__init__.py b/tests_brownie/stablecoin/__init__.py deleted file mode 100644 index e69de29b..00000000 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}) From 975e378b33099c1e19993198d785f68c4633a5db Mon Sep 17 00:00:00 2001 From: Alberto Date: Sun, 17 Aug 2025 14:00:25 +0200 Subject: [PATCH 102/413] refactor: move view logic to separate contracts --- contracts/Controller.vy | 303 +++++--------- contracts/ControllerView.vy | 389 ++++++++++++++++++ contracts/MintController.vy | 6 +- contracts/constants.vy | 1 + contracts/interfaces/IControllerView.vyi | 39 ++ contracts/interfaces/ILlamalendController.vyi | 6 + contracts/lending/LLController.vy | 66 ++- contracts/lending/LLControllerView.vy | 93 +++++ 8 files changed, 665 insertions(+), 238 deletions(-) create mode 100644 contracts/ControllerView.vy create mode 100644 contracts/interfaces/IControllerView.vyi create mode 100644 contracts/lending/LLControllerView.vy diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 9dea3c28..1cead5c7 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -7,6 +7,8 @@ @license Copyright (c) Curve.Fi, 2020-2025 - all rights reserved """ +# TODO restore comments that have been cut to make the bytecode fit + from contracts.interfaces import IAMM from contracts.interfaces import IMonetaryPolicy from contracts.interfaces import ILMGauge @@ -16,8 +18,10 @@ from ethereum.ercs import IERC20 from ethereum.ercs import IERC20Detailed from contracts.interfaces import IMintController as IController +from contracts.interfaces import IControllerView as IView implements: IController +implements: IView from snekmate.utils import math @@ -48,12 +52,13 @@ FACTORY: immutable(IFactory) 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 -MIN_TICKS_UINT: constant(uint256) = 4 CALLBACK_DEPOSIT: constant(bytes4) = method_id( "callback_deposit(address,uint256,uint256,uint256,bytes)", @@ -77,12 +82,15 @@ 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 +MAX_ORACLE_PRICE_DEVIATION: constant(uint256) = WAD // 2 # 50% deviation ################################################################ # STORAGE # ################################################################ +view_impl: public(address) +_view: IView + liquidation_discount: public(uint256) loan_discount: public(uint256) _monetary_policy: IMonetaryPolicy @@ -127,9 +135,12 @@ def __init__( loan_discount: uint256, liquidation_discount: uint256, _AMM: IAMM, + view_impl: address, ): + # In MintController the correct way to limit borrowing # is through the debt ceiling. + # TODO move this to mint controller self.borrow_cap = max_value(uint256) FACTORY = IFactory(msg.sender) @@ -149,6 +160,7 @@ def __init__( COLLATERAL_PRECISION = pow_mod256(10, 18 - collateral_decimals) BORROWED_TOKEN = _borrowed_token + # TODO refactor this logic to be shared with view borrowed_decimals: uint256 = convert( staticcall IERC20Detailed(BORROWED_TOKEN.address).decimals(), uint256 ) @@ -158,6 +170,42 @@ def __init__( self.liquidation_discount = liquidation_discount self.loan_discount = loan_discount self._total_debt.rate_mul = WAD + self._set_view(view_impl) + + +# TODO expose in ll +@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 +def _set_view(view_impl: address): + """ + @notice Set the view implementation + @param view New view implementation + """ + self.view_impl = view_impl + view: address = create_from_blueprint( + view_impl, + self, + SQRT_BAND_RATIO, + LOGN_A_RATIO, + AMM, + A, + COLLATERAL_TOKEN, + BORROWED_TOKEN, + ) + self._view = IView(view) + + + # TODO event @view @@ -222,20 +270,27 @@ def set_price_oracle(price_oracle: IPriceOracle, max_deviation: uint256): 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 - + 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 + 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 # dev: deviation > max - + assert delta <= max_delta # dev: deviation > max + extcall AMM.set_price_oracle(price_oracle) @@ -320,6 +375,12 @@ def _debt(user: address) -> (uint256, uint256): return (debt, rate_mul) +# TODO check if needed +# @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 debt(user: address) -> uint256: @@ -353,9 +414,13 @@ def total_debt() -> uint256: @internal -@view -def get_y_effective( - collateral: uint256, N: uint256, discount: uint256 +@pure +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, @@ -385,13 +450,13 @@ def get_y_effective( WAD, ), ), - unsafe_mul(SQRT_BAND_RATIO, N), + unsafe_mul(_SQRT_BAND_RATIO, N), ) y_effective: uint256 = d_y_effective for i: uint256 in range(1, MAX_TICKS_UINT): if i == N: break - d_y_effective = unsafe_div(d_y_effective * unsafe_sub(A, 1), 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 @@ -416,10 +481,12 @@ def _calculate_debt_n1( # 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( + 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 @@ -455,6 +522,7 @@ def _calculate_debt_n1( return n1 +# TODO delete @internal @view def max_p_base() -> uint256: @@ -502,91 +570,17 @@ def max_borrowable( @param user User to calculate the value for (only necessary for nonzero extra_health) @return Maximum amount of stablecoin to borrow """ - # Cannot borrow beyond the amount of coins Controller has - cap: uint256 = staticcall BORROWED_TOKEN.balanceOf(self) + current_debt - - return self._max_borrowable( - collateral, - N, - cap, - current_debt, - user, - ) - - -@internal -@view -def _max_borrowable( - collateral: uint256, - N: uint256, - cap: uint256, - current_debt: uint256 = 0, - user: address = empty(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], + return staticcall self._view.max_borrowable( + collateral, N, current_debt, 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 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 - @param user User to calculate the value for (only necessary for nonzero extra_health) - @return Minimal collateral required - """ - # 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() - * 10 - ** 18 // 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, - ) + return staticcall self._view.min_collateral(debt, N, user) @external @@ -1006,8 +1000,12 @@ def repay( self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins) total_stablecoins += cb.stablecoins if total_stablecoins < d_debt: - _d_debt_effective: uint256 = unsafe_sub(d_debt, xy[0] + cb.stablecoins) # <= _d_debt - self.transferFrom(BORROWED_TOKEN, msg.sender, self, _d_debt_effective) + _d_debt_effective: uint256 = unsafe_sub( + d_debt, xy[0] + cb.stablecoins + ) # <= _d_debt + self.transferFrom( + BORROWED_TOKEN, msg.sender, self, _d_debt_effective + ) total_stablecoins += _d_debt_effective if total_stablecoins > d_debt: @@ -1156,65 +1154,9 @@ def health_calculator( @param N Number of bands in case loan doesn't yet exist @return Signed health value """ - ns: int256[2] = staticcall 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, "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 + return staticcall self._view.health_calculator( + user, d_collateral, d_debt, full, N + ) @internal @@ -1230,9 +1172,7 @@ def _get_f_remove(frac: uint256, health_limit: uint256) -> uint256: ), unsafe_add(WAD, health_limit), ) - f_remove = unsafe_div( - unsafe_mul(unsafe_add(f_remove, frac), frac), WAD - ) + f_remove = unsafe_div(unsafe_mul(unsafe_add(f_remove, frac), frac), WAD) return f_remove @@ -1241,9 +1181,9 @@ def _get_f_remove(frac: uint256, health_limit: uint256) -> uint256: def liquidate( user: address, min_x: uint256, - _frac: uint256 = 10 ** 18, + _frac: uint256 = 10**18, callbacker: address = empty(address), - calldata: Bytes[10 ** 4] = b"" + calldata: Bytes[10**4] = b"", ): """ @notice Perform a bad liquidation (or self-liquidation) of user if health is not good @@ -1262,7 +1202,7 @@ def liquidate( if health_limit != 0: assert ( self._health(user, debt, True, health_limit) < 0 - ) # dev: not enough rekt + ) # dev: not enough rekt final_debt: uint256 = debt # TODO shouldn't clamp max @@ -1403,38 +1343,16 @@ def users_to_liquidate( @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[IController.Position, 1000] = [] - for i: uint256 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] = staticcall AMM.get_sum_xy(user) - out.append( - IController.Position( - user=user, x=xy[0], y=xy[1], debt=debt, health=health - ) - ) - ix += 1 - return out + return staticcall self._view.users_to_liquidate(_from, _limit) @view @external -@reentrant +@reentrant # TODO make this consistent def amm_price() -> uint256: """ @notice Current price from the AMM - @dev Marked as reentrant because AMM has a nonreentrant decorator + @dev Marked as reentrant because AMM already has a nonreentrant decorator """ return staticcall AMM.get_p() @@ -1447,11 +1365,7 @@ def user_prices(user: address) -> uint256[2]: # Upper, lower @param user User address @return (upper_price, lower_price) """ - 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]) - ] + return staticcall self._view.user_prices(user) @view @@ -1462,14 +1376,7 @@ def user_state(user: address) -> uint256[4]: @param user User to return the state for @return (collateral, stablecoin, debt, N) """ - 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)[0], - convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256), - ] + return staticcall self._view.user_state(user) @external @@ -1568,9 +1475,7 @@ def _collect_fees(admin_fee: uint256) -> uint256: # Difference between to_be_repaid and processed amount is exactly due to interest charged if to_be_repaid > processed: self.processed = to_be_repaid - fees: uint256 = ( - unsafe_sub(to_be_repaid, processed) * admin_fee // WAD - ) + fees: uint256 = (unsafe_sub(to_be_repaid, processed) * admin_fee // WAD) self.transfer(BORROWED_TOKEN, _to, fees) log IController.CollectFees(amount=fees, new_supply=loan.initial_debt) return fees diff --git a/contracts/ControllerView.vy b/contracts/ControllerView.vy new file mode 100644 index 00000000..fdb312f1 --- /dev/null +++ b/contracts/ControllerView.vy @@ -0,0 +1,389 @@ +# pragma version 0.4.3 +# pragma nonreentrancy on + +from contracts.interfaces import IAMM +from contracts.interfaces import IMintController as IController + +from ethereum.ercs import IERC20 +from ethereum.ercs import IERC20Detailed + +from contracts import Controller as core +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.MIN_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, + _borrowed_token: IERC20, +): + """ + @notice Initialize the ControllerView contract + @param _controller Address of the Controller contract + """ + CONTROLLER = _controller + SQRT_BAND_RATIO = _sqrt_band_ratio + LOGN_A_RATIO = _logn_a_ratio + AMM = _amm + A = _A + COLLATERAL_TOKEN = _collateral_token + COLLATERAL_PRECISION = convert( + staticcall IERC20Detailed(COLLATERAL_TOKEN.address).decimals(), uint256 + ) + BORROWED_TOKEN = _borrowed_token + BORROWED_PRECISION = convert( + staticcall IERC20Detailed(BORROWED_TOKEN.address).decimals(), uint256 + ) + + +@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 + + +# TODO LLController View + + +@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 + """ + n_loans: uint256 = self._n_loans() + limit: uint256 = _limit + if _limit == 0: + limit = 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 = self._loans(ix) + debt: uint256 = self._debt(user) + health: int256 = self._health(user, True) + if health < 0: + xy: uint256[2] = staticcall AMM.get_sum_xy(user) + out.append( + IController.Position( + user=user, x=xy[0], y=xy[1], debt=debt, health=health + ) + ) + ix += 1 + return out + + +@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, + current_debt: uint256 = 0, + user: address = empty(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(self) + current_debt + + return self._max_borrowable( + collateral, + N, + cap, + current_debt, + user, + ) + + +# TODO use pointers to natpsec +@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/MintController.vy b/contracts/MintController.vy index bf377cd7..28d83e26 100644 --- a/contracts/MintController.vy +++ b/contracts/MintController.vy @@ -33,6 +33,10 @@ def __init__( loan_discount, liquidation_discount, amm, + empty(address), # to replace at deployment with view blueprint ) - assert extcall core.BORROWED_TOKEN.approve(core.FACTORY.address, max_value(uint256), default_return_value=True) + # TODO do this differently + assert extcall core.BORROWED_TOKEN.approve( + core.FACTORY.address, max_value(uint256), default_return_value=True + ) diff --git a/contracts/constants.vy b/contracts/constants.vy index 5a7d5101..ceebd8fa 100644 --- a/contracts/constants.vy +++ b/contracts/constants.vy @@ -1,4 +1,5 @@ MAX_TICKS_UINT: constant(uint256) = 50 +MIN_TICKS_UINT: constant(uint256) = 4 MAX_TICKS: constant(int256) = 50 DEAD_SHARES: constant(uint256) = 1000 diff --git a/contracts/interfaces/IControllerView.vyi b/contracts/interfaces/IControllerView.vyi new file mode 100644 index 00000000..618b9b79 --- /dev/null +++ b/contracts/interfaces/IControllerView.vyi @@ -0,0 +1,39 @@ +from contracts.interfaces import IMintController + +# 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[IMintController.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/ILlamalendController.vyi b/contracts/interfaces/ILlamalendController.vyi index a6a6bee5..8c51c0c4 100644 --- a/contracts/interfaces/ILlamalendController.vyi +++ b/contracts/interfaces/ILlamalendController.vyi @@ -376,3 +376,9 @@ def admin_fee() -> uint256: def borrowed_balance() -> uint256: ... + +@external +@view +def borrow_cap() -> uint256: + ... + diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy index 12ae4476..9053736d 100644 --- a/contracts/lending/LLController.vy +++ b/contracts/lending/LLController.vy @@ -49,7 +49,6 @@ exports: ( core.loan_exists, core.loan_ix, core.loans, - core.min_collateral, core.monetary_policy, core.n_loans, core.remove_collateral, @@ -57,9 +56,6 @@ exports: ( core.set_extra_health, core.tokens_to_liquidate, core.total_debt, - core.user_prices, - core.user_state, - core.users_to_liquidate, core.admin_fees, core.factory, core.liquidate, @@ -69,11 +65,17 @@ exports: ( core.set_callback, core.set_monetary_policy, core.set_price_oracle, - # For backward compatibility - core.minted, - core.redeemed, core.processed, core.repaid, + # From view contract + core.user_prices, + core.user_state, + core.users_to_liquidate, + core.min_collateral, + core.max_borrowable, + # For compatibility with mint markets ABI + core.minted, + core.redeemed, ) # TODO reorder exports in a way that make sense @@ -101,6 +103,7 @@ def __init__( monetary_policy: IMonetaryPolicy, loan_discount: uint256, liquidation_discount: uint256, + view_impl: address, ): """ @notice Controller constructor deployed by the factory from blueprint @@ -120,9 +123,22 @@ def __init__( loan_discount, liquidation_discount, amm, + view_impl, ) - assert extcall core.BORROWED_TOKEN.approve(VAULT.address, max_value(uint256), default_return_value=True) + core.borrow_cap = 0 + assert extcall core.BORROWED_TOKEN.approve( + VAULT.address, max_value(uint256), default_return_value=True + ) + + +@external +@view +def borrow_cap() -> uint256: + """ + @notice Current borrow cap + """ + return core.borrow_cap @external @@ -153,35 +169,7 @@ def borrowed_balance() -> uint256: return self._borrowed_balance() -@external -@view -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 - """ - # Cannot borrow beyond the amount of coins Controller has or beyond borrow_cap - _total_debt: uint256 = core._get_total_debt() - cap: uint256 = unsafe_sub(max(core.borrow_cap, _total_debt), _total_debt) - cap = min(self._borrowed_balance() + current_debt, cap) - - return core._max_borrowable( - collateral, - N, - cap, - current_debt, - user, - ) - +# TODO delete deprecated and legacy files @external def create_loan( @@ -257,5 +245,7 @@ def set_admin_fee(admin_fee: uint256): @param admin_fee The fee which should be no higher than MAX_ADMIN_FEE """ core._check_admin() - assert admin_fee <= MAX_ADMIN_FEE # dev: admin_fee is higher than MAX_ADMIN_FEE + assert ( + admin_fee <= MAX_ADMIN_FEE + ) # dev: admin_fee is higher than MAX_ADMIN_FEE self.admin_fee = admin_fee diff --git a/contracts/lending/LLControllerView.vy b/contracts/lending/LLControllerView.vy new file mode 100644 index 00000000..3f2a483e --- /dev/null +++ b/contracts/lending/LLControllerView.vy @@ -0,0 +1,93 @@ +from ethereum.ercs import IERC20 +from contracts.interfaces import IMintController as 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, + _borrowed_token: IERC20, +): + core.__init__( + _controller, + _sqrt_band_ratio, + _logn_a_ratio, + _amm, + _A, + _collateral_token, + _borrowed_token, + ) + + +@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 _borrowed_balance() -> uint256: + ll_core: ILlamalendController = ILlamalendController( + core.CONTROLLER.address + ) + return staticcall ll_core.borrowed_balance() + + +@external +@view +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 + """ + # 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._borrowed_balance() + current_debt, cap) + + return core._max_borrowable( + collateral, + N, + cap, + current_debt, + user, + ) From 56f439bc6afcb2c1f4d734a883b229625e6c0466 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 18 Aug 2025 12:11:39 +0400 Subject: [PATCH 103/413] fix: MAX_TICKS_UINT and cap in ControllerView --- contracts/ControllerView.vy | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/ControllerView.vy b/contracts/ControllerView.vy index fdb312f1..e73d4788 100644 --- a/contracts/ControllerView.vy +++ b/contracts/ControllerView.vy @@ -13,7 +13,7 @@ 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.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 @@ -301,8 +301,8 @@ def _max_borrowable( collateral: uint256, N: uint256, cap: uint256, - current_debt: uint256 = 0, - user: address = empty(address), + current_debt: uint256, + user: address, ) -> uint256: # Calculation of maximum which can be borrowed. @@ -349,7 +349,7 @@ def max_borrowable( @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(self) + current_debt + cap: uint256 = staticcall BORROWED_TOKEN.balanceOf(CONTROLLER.address) + current_debt return self._max_borrowable( collateral, From 6e73bffdba3b53290f1c8374ccece7224593c42e Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 18 Aug 2025 12:13:25 +0400 Subject: [PATCH 104/413] chore: leave dust in the AMM (CHECK WITH AUDITORS) --- contracts/AMM.vy | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/contracts/AMM.vy b/contracts/AMM.vy index b8e63fed..ad23a0a5 100644 --- a/contracts/AMM.vy +++ b/contracts/AMM.vy @@ -77,7 +77,7 @@ active_band: public(int256) min_band: public(int256) max_band: public(int256) -_price_oracle_contract: IPriceOracle +_price_oracle_contract: IPriceOracle # https://github.com/vyperlang/vyper/issues/4721 @view @@ -778,10 +778,8 @@ def withdraw(user: address, frac: uint256) -> uint256[2]: x -= dx y -= dy - # If withdrawal is the last one - withdraw dust to the user + # If withdrawal is the last one - leave dust in the AMM if new_shares == 0: - dx += x - dy += y x = 0 y = 0 @@ -1233,7 +1231,7 @@ def calc_swap_in(pump: bool, out_amount: uint256, p_o: uint256[2], in_precision: dy: uint256 = unsafe_div(y_dest * antifee, 10**18) # MORE than y_dest out.out_amount = out_amount out.in_amount += dy - out.ticks_in[j] = y + dy + out.ticks_in[j] = y + dy break else: From 4ce86279ec94c2d3f3a78a1d0b1db53793c2a60c Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 18 Aug 2025 12:14:17 +0400 Subject: [PATCH 105/413] chore: 'Not enough rekt' assertion message --- contracts/Controller.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 1cead5c7..9454a9aa 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -1202,7 +1202,7 @@ def liquidate( if health_limit != 0: assert ( self._health(user, debt, True, health_limit) < 0 - ) # dev: not enough rekt + ), "Not enough rekt" final_debt: uint256 = debt # TODO shouldn't clamp max From 2967e980e2605e2797387492d78e64576c07b07d Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 18 Aug 2025 12:14:39 +0400 Subject: [PATCH 106/413] fix: import --- contracts/MintController.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/MintController.vy b/contracts/MintController.vy index 28d83e26..fb9af100 100644 --- a/contracts/MintController.vy +++ b/contracts/MintController.vy @@ -9,7 +9,7 @@ factory for mint markets. """ -import Controller as core +from contracts import Controller as core initializes: core # Usually a bad practice to expose through From b9fb47693a19025c940d0461f57f92567171e770 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 18 Aug 2025 12:20:18 +0400 Subject: [PATCH 107/413] chore: deprecate BoostedLMCallback --- .../{ => deprecated}/BoostedLMCallback.vy | 0 tests/boosted_lm_callback/__init__.py | 0 tests/boosted_lm_callback/conftest.py | 172 --------- tests/boosted_lm_callback/test_as_gauge.py | 324 ---------------- tests/boosted_lm_callback/test_lm_callback.py | 348 ------------------ tests/boosted_lm_callback/test_st_as_gauge.py | 223 ----------- .../test_st_lm_callback.py | 314 ---------------- 7 files changed, 1381 deletions(-) rename contracts/{ => deprecated}/BoostedLMCallback.vy (100%) delete mode 100644 tests/boosted_lm_callback/__init__.py delete mode 100644 tests/boosted_lm_callback/conftest.py delete mode 100644 tests/boosted_lm_callback/test_as_gauge.py delete mode 100644 tests/boosted_lm_callback/test_lm_callback.py delete mode 100644 tests/boosted_lm_callback/test_st_as_gauge.py delete mode 100644 tests/boosted_lm_callback/test_st_lm_callback.py diff --git a/contracts/BoostedLMCallback.vy b/contracts/deprecated/BoostedLMCallback.vy similarity index 100% rename from contracts/BoostedLMCallback.vy rename to contracts/deprecated/BoostedLMCallback.vy diff --git a/tests/boosted_lm_callback/__init__.py b/tests/boosted_lm_callback/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/boosted_lm_callback/conftest.py b/tests/boosted_lm_callback/conftest.py deleted file mode 100644 index 45dac3c5..00000000 --- a/tests/boosted_lm_callback/conftest.py +++ /dev/null @@ -1,172 +0,0 @@ -import boa -import pytest -from tests.utils.deployers import ( - ERC20_CRV_DEPLOYER, - VOTING_ESCROW_DEPLOYER, - VE_DELEGATION_MOCK_DEPLOYER, - GAUGE_CONTROLLER_DEPLOYER, - MINTER_DEPLOYER, - STABLECOIN_DEPLOYER, - WETH_DEPLOYER, - CONTROLLER_FACTORY_DEPLOYER, - MINT_CONTROLLER_DEPLOYER, - AMM_DEPLOYER, - CONSTANT_MONETARY_POLICY_DEPLOYER, - BOOSTED_LM_CALLBACK_DEPLOYER, - BLOCK_COUNTER_DEPLOYER -) - - -@pytest.fixture(scope="module") -def crv(admin): - with boa.env.prank(admin): - 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 VOTING_ESCROW_DEPLOYER.deploy(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 VE_DELEGATION_MOCK_DEPLOYER.deploy(voting_escrow) - - -@pytest.fixture(scope="module") -def gauge_controller(admin, crv, voting_escrow): - with boa.env.prank(admin): - return GAUGE_CONTROLLER_DEPLOYER.deploy(crv, voting_escrow) - - -@pytest.fixture(scope="module") -def minter(admin, crv, gauge_controller): - with boa.env.prank(admin): - return MINTER_DEPLOYER.deploy(crv, gauge_controller) - - -# Trader -@pytest.fixture(scope="module") -def chad(collateral_token, admin): - _chad = boa.env.generate_address() - boa.deal(collateral_token, _chad, 10**25) - - return _chad - - -@pytest.fixture(scope="module") -def stablecoin(admin, chad): - with boa.env.prank(admin): - _stablecoin = STABLECOIN_DEPLOYER.deploy('Curve USD', 'crvUSD') - _stablecoin.mint(chad, 10**25) - - return _stablecoin - - -@pytest.fixture(scope="module") -def weth(admin): - with boa.env.prank(admin): - return WETH_DEPLOYER.deploy() - - -@pytest.fixture(scope="module") -def controller_prefactory(stablecoin, weth, admin, accounts): - with boa.env.prank(admin): - return CONTROLLER_FACTORY_DEPLOYER.deploy(stablecoin.address, admin, accounts[0], weth.address) - - -@pytest.fixture(scope="module") -def controller_interface(): - return MINT_CONTROLLER_DEPLOYER - - -@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 AMM_DEPLOYER - - -@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 = 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 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 = BOOSTED_LM_CALLBACK_DEPLOYER.deploy(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 BLOCK_COUNTER_DEPLOYER.deploy() 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 ca3f0454..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 - - 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 - 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): - 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 - 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): - boa.deal(collateral_token, alice, 1000 * 10 ** 18) - boa.deal(collateral_token, 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 f0f8aa78..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): - 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) - 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): - 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 - 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): - 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) - - # 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 5e8e36b1..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): - 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) - 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 b2bf5e4e..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): - 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) - 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) From 83d5fcff7122a2fa017a1dd2aa254eadbeefafa9 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 18 Aug 2025 13:12:11 +0400 Subject: [PATCH 108/413] test: set view_impl in MintController --- tests/utils/deployers.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/utils/deployers.py b/tests/utils/deployers.py index b860b8b0..1dfeca09 100644 --- a/tests/utils/deployers.py +++ b/tests/utils/deployers.py @@ -13,7 +13,7 @@ # Contracts with #pragma optimize codesize compiler_args_codesize = {**compiler_args_default, "optimize": OptimizationLevel.CODESIZE} -# Contracts with #pragma optimize gas +# Contracts with #pragma optimize gas compiler_args_gas = {**compiler_args_default, "optimize": OptimizationLevel.GAS} # Contract paths @@ -33,8 +33,17 @@ AMM_DEPLOYER = boa.load_partial(BASE_CONTRACT_PATH + "AMM.vy", compiler_args=compiler_args_default) # Controller.vy has #pragma optimize codesize 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) +controller_view_impl = CONTROLLER_VIEW_DEPLOYER.deploy_as_blueprint() # MintController.vy has #pragma optimize codesize -MINT_CONTROLLER_DEPLOYER = boa.load_partial(BASE_CONTRACT_PATH + "MintController.vy", compiler_args=compiler_args_codesize) +# view_impl address has to be set in MintController.vy +with open(BASE_CONTRACT_PATH + "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_impl.address},", 1 +) +assert f"{controller_view_impl.address}," in mint_controller_code +MINT_CONTROLLER_DEPLOYER = boa.loads_partial(mint_controller_code, 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) @@ -78,7 +87,6 @@ # Callback contracts LM_CALLBACK_DEPLOYER = boa.load_partial(BASE_CONTRACT_PATH + "LMCallback.vy", compiler_args=compiler_args_default) -BOOSTED_LM_CALLBACK_DEPLOYER = boa.load_partial(BASE_CONTRACT_PATH + "BoostedLMCallback.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) @@ -118,4 +126,4 @@ 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) \ No newline at end of file +CURVE_STABLESWAP_NG_VIEWS_DEPLOYER = boa.load_partial(STABLESWAP_NG_PATH + "CurveStableSwapNGViews.vy", compiler_args=compiler_args_default) From 296fcb7b18b69fe5dc25aa2d1f900595d48e1267 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 18 Aug 2025 18:17:29 +0400 Subject: [PATCH 109/413] fix: create_loan with collbacker --- contracts/Controller.vy | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 9454a9aa..1f439143 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -689,13 +689,13 @@ def _create_loan( calldata, ).collateral - collateral = collateral + more_collateral + total_collateral: uint256 = collateral + more_collateral 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" - n1: int256 = self._calculate_debt_n1(collateral, debt, N, _for) + n1: int256 = self._calculate_debt_n1(total_collateral, debt, N, _for) n2: int256 = n1 + convert(unsafe_sub(N, 1), int256) rate_mul: uint256 = staticcall AMM.get_rate_mul() @@ -710,21 +710,21 @@ def _create_loan( self._update_total_debt(debt, rate_mul, True) - extcall AMM.deposit_range(_for, collateral, n1, n2) + extcall AMM.deposit_range(_for, total_collateral, n1, n2) self.processed += debt self._save_rate() log IController.UserState( user=_for, - collateral=collateral, + collateral=total_collateral, debt=debt, n1=n1, n2=n2, liquidation_discount=liquidation_discount, ) log IController.Borrow( - user=_for, collateral_increase=collateral, loan_increase=debt + user=_for, collateral_increase=total_collateral, loan_increase=debt ) self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) From da7e78aa2f2ddcd48f775ae032a8098f49d41512 Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 19 Aug 2025 11:57:34 +0200 Subject: [PATCH 110/413] docs: better contract natspec --- contracts/Controller.vy | 3 +++ contracts/ControllerView.vy | 8 ++++++++ contracts/MintController.vy | 2 ++ contracts/lending/LLController.vy | 5 +++-- contracts/lending/LLControllerView.vy | 10 ++++++++++ 5 files changed, 26 insertions(+), 2 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 1f439143..60e63ae0 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -5,6 +5,9 @@ @title crvUSD Controller @author Curve.Fi @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 """ # TODO restore comments that have been cut to make the bytecode fit diff --git a/contracts/ControllerView.vy b/contracts/ControllerView.vy index e73d4788..9a8e2767 100644 --- a/contracts/ControllerView.vy +++ b/contracts/ControllerView.vy @@ -1,5 +1,13 @@ # 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 IMintController as IController diff --git a/contracts/MintController.vy b/contracts/MintController.vy index fb9af100..46646f5c 100644 --- a/contracts/MintController.vy +++ b/contracts/MintController.vy @@ -7,9 +7,11 @@ @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 diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy index 9053736d..87032b7f 100644 --- a/contracts/lending/LLController.vy +++ b/contracts/lending/LLController.vy @@ -5,6 +5,9 @@ @title LlamaLend 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 ethereum.ercs import IERC20 @@ -24,10 +27,8 @@ implements: ILlamalendController from snekmate.utils import math -# TODO rename to core from contracts import Controller as core -# TODO rename to core initializes: core exports: ( diff --git a/contracts/lending/LLControllerView.vy b/contracts/lending/LLControllerView.vy index 3f2a483e..4e550982 100644 --- a/contracts/lending/LLControllerView.vy +++ b/contracts/lending/LLControllerView.vy @@ -1,3 +1,13 @@ +# 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 ethereum.ercs import IERC20 from contracts.interfaces import IMintController as IController from contracts.interfaces import ILlamalendController From d7bc58c2560851de1a24b9d094e8327a6ec00da1 Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 19 Aug 2025 11:58:45 +0200 Subject: [PATCH 111/413] chore: remove dead code This function is ignored at compilation and it's only used in the view contract which already contains a copy. --- contracts/Controller.vy | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 60e63ae0..6445a3f6 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -525,38 +525,6 @@ def _calculate_debt_n1( return n1 -# TODO delete -@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 - - @external @view def max_borrowable( From fff2d1db32ad5775d6bc6e54c289a434c053c80c Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 19 Aug 2025 11:59:48 +0200 Subject: [PATCH 112/413] feat: implement versioning --- contracts/Controller.vy | 28 ++++++++++++++-------------- contracts/constants.vy | 1 + contracts/lending/LLController.vy | 6 ++++++ 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 6445a3f6..589f4385 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -10,8 +10,6 @@ @custom:security security@curve.fi """ -# TODO restore comments that have been cut to make the bytecode fit - from contracts.interfaces import IAMM from contracts.interfaces import IMonetaryPolicy from contracts.interfaces import ILMGauge @@ -20,6 +18,7 @@ from contracts.interfaces import IPriceOracle from ethereum.ercs import IERC20 from ethereum.ercs import IERC20Detailed +# TODO just rename interface of mint controller to "icontroller" from contracts.interfaces import IMintController as IController from contracts.interfaces import IControllerView as IView @@ -33,11 +32,12 @@ from snekmate.utils import math ################################################################ AMM: immutable(IAMM) -MAX_AMM_FEE: immutable( - uint256 -) # let's set to MIN_TICKS / A: for example, 4% max fee for A=100 +# TODO move this comment to init +# let's set to MIN_TICKS / A: for example, 4% max fee for A=100 +MAX_AMM_FEE: immutable(uint256) A: immutable(uint256) -LOGN_A_RATIO: immutable(int256) # log(A / (A - 1)) +# log(A / (A - 1)) +LOGN_A_RATIO: immutable(int256) SQRT_BAND_RATIO: immutable(uint256) COLLATERAL_TOKEN: immutable(IERC20) @@ -50,8 +50,7 @@ FACTORY: immutable(IFactory) # CONSTANTS # ################################################################ -# TODO add version - +version: public(constant(String[5])) = c.__version__ from contracts import constants as c @@ -115,11 +114,12 @@ loan: HashMap[address, IController.Loan] liquidation_discounts: public(HashMap[address, uint256]) _total_debt: IController.Loan -# TODO uniform comment style - -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 +# 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) # cumulative amount of assets ever repaid (including admin fees) repaid: public(uint256) @@ -292,7 +292,7 @@ def set_price_oracle(price_oracle: IPriceOracle, max_deviation: uint256): else old_price - new_price ) max_delta: uint256 = old_price * max_deviation // WAD - assert delta <= max_delta # dev: deviation > max + assert delta <= max_delta, "delta>max" extcall AMM.set_price_oracle(price_oracle) diff --git a/contracts/constants.vy b/contracts/constants.vy index ceebd8fa..312945e0 100644 --- a/contracts/constants.vy +++ b/contracts/constants.vy @@ -1,3 +1,4 @@ +__version__: constant(String[5]) = "2.0.0" MAX_TICKS_UINT: constant(uint256) = 50 MIN_TICKS_UINT: constant(uint256) = 4 MAX_TICKS: constant(int256) = 50 diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy index 87032b7f..ff00e825 100644 --- a/contracts/lending/LLController.vy +++ b/contracts/lending/LLController.vy @@ -133,6 +133,12 @@ def __init__( ) +@external +@view +def version() -> String[10]: + return concat(core.version, "-lend") + + @external @view def borrow_cap() -> uint256: From 0550c25ac8373836aab2095fc49886480c267105 Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 19 Aug 2025 12:00:30 +0200 Subject: [PATCH 113/413] fix: tackle todos --- contracts/Controller.vy | 4 +--- contracts/ControllerView.vy | 7 +++---- contracts/interfaces/IMintController.vyi | 14 ++++++++++++++ contracts/lending/LLController.vy | 22 ++++++++++++++-------- 4 files changed, 32 insertions(+), 15 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 589f4385..922f62b4 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -176,7 +176,6 @@ def __init__( self._set_view(view_impl) -# TODO expose in ll @external def set_view(view_impl: address): """ @@ -207,8 +206,7 @@ def _set_view(view_impl: address): ) self._view = IView(view) - - # TODO event + log IController.SetView(view=view) @view diff --git a/contracts/ControllerView.vy b/contracts/ControllerView.vy index 9a8e2767..cd96adcb 100644 --- a/contracts/ControllerView.vy +++ b/contracts/ControllerView.vy @@ -209,9 +209,6 @@ def health_calculator( return health -# TODO LLController View - - @view @external def users_to_liquidate( @@ -357,7 +354,9 @@ def max_borrowable( @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 + cap: uint256 = ( + staticcall BORROWED_TOKEN.balanceOf(CONTROLLER.address) + current_debt + ) return self._max_borrowable( collateral, diff --git a/contracts/interfaces/IMintController.vyi b/contracts/interfaces/IMintController.vyi index a98a2514..5b435dd4 100644 --- a/contracts/interfaces/IMintController.vyi +++ b/contracts/interfaces/IMintController.vyi @@ -69,6 +69,10 @@ event CollectFees: new_supply: uint256 +event SetView: + view: address + + # Structs struct Position: @@ -346,3 +350,13 @@ def repaid() -> uint256: def processed() -> uint256: ... +@view +@external +def version() -> String[1]: + ... + +@external +def set_view(view_impl: address): + ... + + diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy index ff00e825..61e84600 100644 --- a/contracts/lending/LLController.vy +++ b/contracts/lending/LLController.vy @@ -32,11 +32,18 @@ from contracts import Controller as core initializes: core exports: ( + # Loan management core.add_collateral, + core.approve, + core.remove_collateral, + core.repay, + core.set_extra_health, + core.liquidate, + core.save_rate, + # Getters core.amm, core.amm_price, core.approval, - core.approve, core.borrowed_token, core.calculate_debt_n1, core.collateral_token, @@ -52,22 +59,19 @@ exports: ( core.loans, core.monetary_policy, core.n_loans, - core.remove_collateral, - core.save_rate, - core.set_extra_health, core.tokens_to_liquidate, core.total_debt, core.admin_fees, core.factory, - core.liquidate, - core.repay, + core.processed, + core.repaid, + # Setters + core.set_view, core.set_amm_fee, core.set_borrowing_discounts, core.set_callback, core.set_monetary_policy, core.set_price_oracle, - core.processed, - core.repaid, # From view contract core.user_prices, core.user_state, @@ -94,6 +98,8 @@ admin_fee: public(uint256) # TODO check this MAX_ADMIN_FEE: constant(uint256) = 2 * 10**17 # 20% +# TODO add natrix + @deploy def __init__( From c5460aa2f4f160031c05aa770f21c283efd0521f Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 19 Aug 2025 18:36:16 +0400 Subject: [PATCH 114/413] fix: precisions in ControllerView --- contracts/Controller.vy | 2 ++ contracts/ControllerView.vy | 10 ++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 1f439143..210d43a2 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -200,7 +200,9 @@ def _set_view(view_impl: address): AMM, A, COLLATERAL_TOKEN, + COLLATERAL_PRECISION, BORROWED_TOKEN, + BORROWED_PRECISION, ) self._view = IView(view) diff --git a/contracts/ControllerView.vy b/contracts/ControllerView.vy index e73d4788..53fec131 100644 --- a/contracts/ControllerView.vy +++ b/contracts/ControllerView.vy @@ -39,7 +39,9 @@ def __init__( _amm: IAMM, _A: uint256, _collateral_token: IERC20, + _collateral_precision: uint256, _borrowed_token: IERC20, + _borrowed_precision: uint256, ): """ @notice Initialize the ControllerView contract @@ -51,13 +53,9 @@ def __init__( AMM = _amm A = _A COLLATERAL_TOKEN = _collateral_token - COLLATERAL_PRECISION = convert( - staticcall IERC20Detailed(COLLATERAL_TOKEN.address).decimals(), uint256 - ) + COLLATERAL_PRECISION = _collateral_precision BORROWED_TOKEN = _borrowed_token - BORROWED_PRECISION = convert( - staticcall IERC20Detailed(BORROWED_TOKEN.address).decimals(), uint256 - ) + BORROWED_PRECISION = _borrowed_precision @view From 8b98161e96d1ff72f09218fc000f9e635594b06c Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 19 Aug 2025 18:43:50 +0400 Subject: [PATCH 115/413] chore: adjust FakeLeverage contract --- contracts/testing/FakeLeverage.vy | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/testing/FakeLeverage.vy b/contracts/testing/FakeLeverage.vy index 3fc9af5f..fd9606cb 100644 --- a/contracts/testing/FakeLeverage.vy +++ b/contracts/testing/FakeLeverage.vy @@ -28,8 +28,8 @@ def approve_all(): @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] +def callback_deposit(user: address, stablecoins_no_use: uint256, collateral: uint256, debt: uint256, calldata: Bytes[10**4]) -> uint256[2]: + min_amount: uint256 = _abi_decode(calldata, (uint256)) assert STABLECOIN.balanceOf(self) >= debt amount_out: uint256 = debt * 10**18 / self.price assert amount_out >= min_amount @@ -37,8 +37,8 @@ def callback_deposit(user: address, stablecoins_no_use: uint256, collateral: uin @external -def callback_repay(user: address, stablecoins: uint256, collateral: uint256, debt: uint256, extra_args: DynArray[uint256, 5]) -> uint256[2]: - frac: uint256 = extra_args[0] +def callback_repay(user: address, stablecoins: uint256, collateral: uint256, debt: uint256, calldata: Bytes[10**4]) -> uint256[2]: + 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 @@ -46,5 +46,5 @@ def callback_repay(user: address, stablecoins: uint256, collateral: uint256, deb @external -def callback_liquidate(sender: address, stablecoins: uint256, collateral: uint256, debt: uint256, extra_args: DynArray[uint256, 5]) -> uint256[2]: +def callback_liquidate(sender: address, stablecoins: uint256, collateral: uint256, debt: uint256, calldata: Bytes[10**4]) -> uint256[2]: return [STABLECOIN.balanceOf(self), collateral] From 1149f111b29ccbef6505fb31c96e1d4d9087d0a1 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 19 Aug 2025 18:47:06 +0400 Subject: [PATCH 116/413] test: adjust stableborrow tests --- tests/stableborrow/test_approval.py | 9 +---- tests/stableborrow/test_bigfuzz.py | 40 +++++++------------ .../test_create_repay_stateful.py | 1 - tests/stableborrow/test_leverage.py | 32 ++++++++++----- tests/stableborrow/test_liquidate.py | 9 ++--- .../test_st_interest_conservation.py | 1 - 6 files changed, 42 insertions(+), 50 deletions(-) diff --git a/tests/stableborrow/test_approval.py b/tests/stableborrow/test_approval.py index 8898cf5b..68c57be8 100644 --- a/tests/stableborrow/test_approval.py +++ b/tests/stableborrow/test_approval.py @@ -167,12 +167,7 @@ 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): - boa.deal(collateral_token, 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 @@ -182,7 +177,7 @@ def f(sleep_time, user, someone_else): 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 61ded52a..9c9b589c 100644 --- a/tests/stableborrow/test_bigfuzz.py +++ b/tests/stableborrow/test_bigfuzz.py @@ -132,13 +132,13 @@ 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) @@ -289,8 +289,7 @@ 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 @@ -298,9 +297,7 @@ def self_liquidate_and_health(self, emode, frac): elif emode == USE_CALLBACKS: 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 @@ -328,13 +325,10 @@ 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.market_controller.liquidate(user, 0, frac, self.fake_leverage.address, b'') else: self.market_controller.liquidate(user, 0) if emode == USE_CALLBACKS: @@ -350,13 +344,10 @@ def liquidate(self, uid, luid, emode, frac): 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.market_controller.liquidate(user, 0, frac, self.fake_leverage.address, b'') else: self.market_controller.liquidate(user, 0) if emode == USE_CALLBACKS: @@ -365,8 +356,7 @@ def liquidate(self, uid, luid, emode, frac): 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 @@ -374,9 +364,7 @@ def liquidate(self, uid, luid, emode, frac): elif emode == USE_CALLBACKS: 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 diff --git a/tests/stableborrow/test_create_repay_stateful.py b/tests/stableborrow/test_create_repay_stateful.py index f1b40823..491c8a8f 100644 --- a/tests/stableborrow/test_create_repay_stateful.py +++ b/tests/stableborrow/test_create_repay_stateful.py @@ -92,7 +92,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"): diff --git a/tests/stableborrow/test_leverage.py b/tests/stableborrow/test_leverage.py index 1bf96841..7e58b92a 100644 --- a/tests/stableborrow/test_leverage.py +++ b/tests/stableborrow/test_leverage.py @@ -1,4 +1,5 @@ import boa +from eth_abi import encode from ..conftest import approx from hypothesis import given, settings @@ -14,22 +15,31 @@ def test_leverage(collateral_token, stablecoin, market_controller, market_amm, f with boa.env.prank(user): boa.deal(collateral_token, 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 + 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 + 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 - market_controller.repay_extended(fake_leverage.address, [10**18]) + 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( @@ -46,11 +56,12 @@ def test_leverage_property(collateral_token, stablecoin, market_controller, mark boa.deal(collateral_token, user, amount) debt = int(loan_mul * amount * 3000) + calldata = encode(["uint256"], [0]) if (debt // 3000) <= collateral_token.balanceOf(fake_leverage.address) and debt > 0: - market_controller.create_loan_extended(amount, debt, 5, fake_leverage.address, [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) @@ -65,7 +76,7 @@ def test_leverage_property(collateral_token, stablecoin, market_controller, mark if (more_debt // 3000) <= collateral_token.balanceOf(fake_leverage.address): if more_debt > 0: boa.deal(collateral_token, user, amount) - market_controller.borrow_more_extended(amount, more_debt, fake_leverage.address, [0]) + 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 @@ -79,15 +90,16 @@ def test_leverage_property(collateral_token, stablecoin, market_controller, mark 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 if repay_mul == 1.0: diff --git a/tests/stableborrow/test_liquidate.py b/tests/stableborrow/test_liquidate.py index 3f24cb17..ca4988ce 100644 --- a/tests/stableborrow/test_liquidate.py +++ b/tests/stableborrow/test_liquidate.py @@ -4,7 +4,6 @@ from hypothesis import given, settings from hypothesis import strategies as st from ..conftest import approx -from tests.utils.constants import ZERO_ADDRESS N = 5 @@ -28,7 +27,7 @@ def f(sleep_time, discount): 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) + debt = market_controller.max_borrowable(collateral_amount, N) * 99 // 100 with boa.env.prank(user): market_controller.create_loan(collateral_amount, debt, N) @@ -122,10 +121,10 @@ 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: @@ -181,7 +180,7 @@ def test_tokens_to_liquidate(accounts, admin, controller_for_liquidation, market stablecoin.transfer(fee_receiver, 10**10) with boa.env.prank(fee_receiver): - controller.liquidate_extended(user, 0, frac, ZERO_ADDRESS, []) + controller.liquidate(user, 0, frac) balance = stablecoin.balanceOf(fee_receiver) diff --git a/tests/stableborrow/test_st_interest_conservation.py b/tests/stableborrow/test_st_interest_conservation.py index 95e5e320..55ae5a83 100644 --- a/tests/stableborrow/test_st_interest_conservation.py +++ b/tests/stableborrow/test_st_interest_conservation.py @@ -101,7 +101,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): From 7921f5d32bf2716157b239390f7c363efbd4c534 Mon Sep 17 00:00:00 2001 From: Oleg <40476427+amfet42@users.noreply.github.com> Date: Tue, 19 Aug 2025 20:11:56 +0300 Subject: [PATCH 117/413] add calculations for zap --- tests/zaps/__init__.py | 0 .../Partial_Liquidation_Zap_Evaluation.pdf | Bin 0 -> 4135 bytes tests/zaps/partial_liquidation/__init__.py | 0 .../zaps/partial_liquidation/calculations.py | 186 ++++++++ tests/zaps/partial_liquidation/test_frac5.txt | 438 ++++++++++++++++++ 5 files changed, 624 insertions(+) create mode 100644 tests/zaps/__init__.py create mode 100644 tests/zaps/partial_liquidation/Partial_Liquidation_Zap_Evaluation.pdf create mode 100644 tests/zaps/partial_liquidation/__init__.py create mode 100644 tests/zaps/partial_liquidation/calculations.py create mode 100644 tests/zaps/partial_liquidation/test_frac5.txt diff --git a/tests/zaps/__init__.py b/tests/zaps/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/zaps/partial_liquidation/Partial_Liquidation_Zap_Evaluation.pdf b/tests/zaps/partial_liquidation/Partial_Liquidation_Zap_Evaluation.pdf new file mode 100644 index 0000000000000000000000000000000000000000..db6d0a4ba05b31195853978b0e3e56872067673b GIT binary patch literal 4135 zcmb7H*Sg}!5x(asL=X~50VSdUiHwLM2oP`-K_Dbh@a*`)FJRyK-p^`r_RQ|_jCVce z<3ru0>I#4V-Bf|IcDH~dWDNZ0KmPtNU|~F5FAx+J=oyaTDS@E_kp`fWdrCo+sKJ;ix9`I5(gZ+x6Zi$C$P6sdm0 z@bzl!>Jb@MzAK=kyaR7Tn~U{SaDqE>)j1~IgVPR|9-=~a1<=nQ@pQ00r=rY$-E;Ko#d;b1+OH&k6~#Ab`<~j*+pPK z9?`Xhb`{(IjPE;i-vExz!29N+BP+^c54@PPcroHvVBf+11nWDezOC(Ab`Yf!QK2RB zgF%UE8_}Kgx?t=G(T_=FN-&&rZ~=-%ig}sxg9!I~tfvI`8r$+15n4DLL4aN!${lcd zyzAtkffgbe+1#9_<3{PN8s>$`Q(}EsURVe1RJ+@AjU4MiH=FLe#L@9K;6q*s%rkSr z2{DJR`wzH<^ulS|xz`G%_9MGTT!F!A+p3xNEO}ax(Sm*^9HlFuU*;WljJoUKJUs{SY;~0qP9s3$MKu52yC;;<-|ebL24~JRc9)GWdZiH z{p`Mp1%84vx1`voRcPVh@?uVA=n$g)v-%#B=i8i4ulCi3T+7sExn`W}1d{^FsOtG)??|8RGR&xniYLaNFAL;8 zjMdh4%_9cS{Z~mzS(TrMdW83pYrTcoO#xM}R zi?_u~GnpzhvifQc63IrTb8Q!!!vaYqpw%c$^0&=4m&n8qiv2v+@o6Y#r`fi!q~+-F zZM6a4W)dMr6siYt8P^}>n>zJOZe0=i-CG0G;7(yRsmj%|7Ka{T=hj)O^a3~L=%sU9 zi`9@yYYqPZ-khPBTuiYYTd&M?qifrAVc-oJk( z)WCLlSKC-wS!J9wQl0b!gE5?xfzV(eALKM8@z&JZ3>OQu-W%~3tV^V7Gx|y2Zl%U@ zwd4p472i^KEU~W&8Kq$5k=2t`TKQah7yVbz(_4cDu_C_6G)vT|b!GSHQmUue!l}4y z63p=4-FFj)$KAzcf|DOwYI&7=RE2bQb$(UGtaVF<)58?FK_ zd!l$xYm>$H{-k}$(y>0-bLy&%q#?TtJp=UFCi0S?53(h=a1|%{y4P(TX2Wc6&2g8F z=yCmL&3DU5-R^Jc5-99XOLihrLOWq_m%X?EE88=dU#FTLl}@z7)+DFK?=*O!#W78A z>Bm-I7qX8YLuod{K6W=RQLQ)mBV;y5%i=gE7Tom)?2z)3ABWW{aS+Eb~wkZ(ZH#ZFtpJ845+70m7o$*xp%;m|w| zSItLVQmdCB@2-;6=y^$bxf`96$Ile++Rxit(t--T!LxQHrZzh?wnq}(G=XI++XB`qR~y1wG|t&0Q^JGk&aJ2WXmd+JNxm3Bvfu7;HhsIy znIWoGS{in($mRJwIawhOq*r@$vq&$;??#Dn9b?ralftof?QKJy-p;wJUXi5u5uYNx z=WqfW>&pVYG!_~vs~N7RR~_v*QTQ=K!(-$;Gm$*)Aoc167GZT@9s;b_W_q{$#@xv0 zw0;N03?0uw(G_o)!r$1abNeW5K64Ew(|4l(dkUw;1Y zeR-h(=vLGqM3wR%wdk2x3>`S83%q)SMJ(!1+yHnr9nn~{{%X87?7d7?yx@Hc37Rg& KK(I6I#{Lac;Guc| literal 0 HcmV?d00001 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.py b/tests/zaps/partial_liquidation/calculations.py new file mode 100644 index 00000000..c319eaf8 --- /dev/null +++ b/tests/zaps/partial_liquidation/calculations.py @@ -0,0 +1,186 @@ +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 = f"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('./test_frac5.txt', 'w') as f: + f.write(res) diff --git a/tests/zaps/partial_liquidation/test_frac5.txt b/tests/zaps/partial_liquidation/test_frac5.txt new file mode 100644 index 00000000..fcddf90e --- /dev/null +++ b/tests/zaps/partial_liquidation/test_frac5.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 +----------------------------- From d1b69b20c0830cba89e82d16f41a05e777a121bb Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 20 Aug 2025 12:33:41 +0400 Subject: [PATCH 118/413] chore: bring back 'Loan doesn't exist' message --- contracts/Controller.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index c2f66f8e..ff5dda91 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -591,7 +591,7 @@ def transfer(token: IERC20, _to: address, amount: uint256): @internal @view def _check_loan_exists(debt: uint256): - assert debt > 0, "No loan" + assert debt > 0, "Loan doesn't exist" @internal From 15ef2be48a66972fadadb09033debddb445c1273 Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 21 Aug 2025 13:17:18 +0400 Subject: [PATCH 119/413] fix: raw_calls --- contracts/AMM.vy | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/contracts/AMM.vy b/contracts/AMM.vy index ad23a0a5..e83cdc70 100644 --- a/contracts/AMM.vy +++ b/contracts/AMM.vy @@ -728,11 +728,17 @@ def deposit_range(user: address, amount: uint256, n1: int256, n2: int256): res: Bytes[32] = empty(Bytes[32]) success, res = raw_call( lm.address, - abi_encode(method_id("callback_collateral_shares(int256,uint256[],uint256)"), n1, collateral_shares, n_bands), + 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(method_id("callback_user_shares(address,int256,uint256[],uint256)"), user, n1, empty(DynArray[uint256, MAX_TICKS_UINT]), n_bands), + abi_encode( + user, n1, empty(DynArray[uint256, MAX_TICKS_UINT]), n_bands, + method_id=method_id("callback_user_shares(address,int256,uint256[],uint256)") + ), max_outsize=32, revert_on_failure=False) @@ -819,11 +825,17 @@ def withdraw(user: address, frac: uint256) -> uint256[2]: res: Bytes[32] = empty(Bytes[32]) success, res = raw_call( lm.address, - abi_encode(method_id("callback_collateral_shares(int256,uint256[],uint256)"), ns[0], empty(DynArray[uint256, MAX_TICKS_UINT]), len(old_user_shares)), + 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(method_id("callback_user_shares(address,int256,uint256[],uint256)"), user, ns[0], old_user_shares, len(old_user_shares)), + abi_encode( + user, ns[0], old_user_shares, len(old_user_shares), + method_id=method_id("callback_user_shares(address,int256,uint256[],uint256)") + ), max_outsize=32, revert_on_failure=False) return [total_x, total_y] @@ -1117,7 +1129,10 @@ def _exchange(i: uint256, j: uint256, amount: uint256, minmax_amount: uint256, _ res: Bytes[32] = empty(Bytes[32]) success, res = raw_call( lm.address, - abi_encode(method_id("callback_collateral_shares(int256,uint256[],uint256)"), n_start, collateral_shares, len(collateral_shares)), + 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 extcall in_coin.transferFrom(msg.sender, self, in_amount_done, default_return_value=True) From 667f7b38e75bb62da50800f969dde0c13ed8590c Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 21 Aug 2025 13:19:52 +0400 Subject: [PATCH 120/413] fix: adjust LMCallback contract --- contracts/LMCallback.vy | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/contracts/LMCallback.vy b/contracts/LMCallback.vy index ffb5f472..0e658643 100644 --- a/contracts/LMCallback.vy +++ b/contracts/LMCallback.vy @@ -12,8 +12,6 @@ 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,7 +155,7 @@ 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? @@ -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) From 71ea6f605b2fea4e00af52d871c4d85ad903d4a8 Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 21 Aug 2025 13:20:06 +0400 Subject: [PATCH 121/413] test: adjust LMCallback tests --- tests/lm_callback/test_lm_callback.py | 8 ++++---- tests/lm_callback/test_st_as_gauge.py | 3 ++- tests/lm_callback/test_st_lm_callback.py | 5 +++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/lm_callback/test_lm_callback.py b/tests/lm_callback/test_lm_callback.py index 05565da4..81bd5d3b 100644 --- a/tests/lm_callback/test_lm_callback.py +++ b/tests/lm_callback/test_lm_callback.py @@ -105,7 +105,7 @@ def update_integral(): 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, @@ -238,7 +238,7 @@ 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) if total_collateral_from_amm > 0 and total_collateral_from_lm_cb > 0: @@ -300,7 +300,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(), + print(collateral_token.balanceOf(market_amm), lm_callback.total_collateral()) dt = randrange(1, YEAR // 5) @@ -332,7 +332,7 @@ 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) diff --git a/tests/lm_callback/test_st_as_gauge.py b/tests/lm_callback/test_st_as_gauge.py index c71ed3f8..24887443 100644 --- a/tests/lm_callback/test_st_as_gauge.py +++ b/tests/lm_callback/test_st_as_gauge.py @@ -148,7 +148,8 @@ 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) diff --git a/tests/lm_callback/test_st_lm_callback.py b/tests/lm_callback/test_st_lm_callback.py index aea0f8de..426efd89 100644 --- a/tests/lm_callback/test_st_lm_callback.py +++ b/tests/lm_callback/test_st_lm_callback.py @@ -41,7 +41,7 @@ def update_integrals(self): 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) @@ -219,7 +219,8 @@ 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) From 94e3ae84ae43156113fee6ef5aa40cf634e37de5 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 21 Aug 2025 15:44:11 +0200 Subject: [PATCH 122/413] refactor: use snekmate's logs --- contracts/lending/LendingFactory.vy | 33 ++--------------------------- 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/contracts/lending/LendingFactory.vy b/contracts/lending/LendingFactory.vy index 2c2e6e25..41deb069 100644 --- a/contracts/lending/LendingFactory.vy +++ b/contracts/lending/LendingFactory.vy @@ -17,6 +17,7 @@ from contracts.interfaces import IPriceOracle from contracts.interfaces import ILendingFactory implements: ILendingFactory +from snekmate.utils import math # 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 @@ -85,36 +86,6 @@ def __init__( self.fee_receiver = fee_receiver -# TODO use snekmate's -@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: uint256 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: uint256 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) - - @internal def _create( borrowed_token: address, @@ -160,7 +131,7 @@ def _create( self.amm_impl, borrowed_token, 10**convert(18 - staticcall IERC20Detailed(borrowed_token).decimals(), uint256), collateral_token, 10**convert(18 - staticcall IERC20Detailed(collateral_token).decimals(), uint256), - A, isqrt(A_ratio * 10**18), self.ln_int(A_ratio), + A, isqrt(A_ratio * 10**18), math._log2(A_ratio, False), p, fee, convert(0, uint256), price_oracle, code_offset=3) controller: address = create_from_blueprint( From f48c361b58f13e4381e41e8010a47d9e647c8563 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 21 Aug 2025 15:45:32 +0200 Subject: [PATCH 123/413] chore: add TODOs --- contracts/Controller.vy | 1 + contracts/ControllerView.vy | 4 ---- contracts/lending/LendingFactory.vy | 6 ++++-- tests/utils/deploy.py | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index ff5dda91..da49d26c 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -227,6 +227,7 @@ def redeemed() -> uint256: @internal @view def _check_admin(): + # TODO possibly some things can be moved from ownership to param votes? assert msg.sender == staticcall FACTORY.admin(), "only admin" diff --git a/contracts/ControllerView.vy b/contracts/ControllerView.vy index b3400d14..6e000f78 100644 --- a/contracts/ControllerView.vy +++ b/contracts/ControllerView.vy @@ -51,10 +51,6 @@ def __init__( _borrowed_token: IERC20, _borrowed_precision: uint256, ): - """ - @notice Initialize the ControllerView contract - @param _controller Address of the Controller contract - """ CONTROLLER = _controller SQRT_BAND_RATIO = _sqrt_band_ratio LOGN_A_RATIO = _logn_a_ratio diff --git a/contracts/lending/LendingFactory.vy b/contracts/lending/LendingFactory.vy index 41deb069..98cf22ee 100644 --- a/contracts/lending/LendingFactory.vy +++ b/contracts/lending/LendingFactory.vy @@ -61,7 +61,7 @@ def __init__( vault: address, pool_price_oracle: address, monetary_policy: address, - admin: address, + admin: address, # TODO also add params votes? fee_receiver: address, ): """ @@ -79,6 +79,7 @@ def __init__( self.pool_price_oracle_impl = pool_price_oracle self.monetary_policy_impl = monetary_policy + # TODO is this actually useful? self.min_default_borrow_rate = 5 * 10**15 // (365 * 86400) self.max_default_borrow_rate = 50 * 10**16 // (365 * 86400) @@ -126,6 +127,7 @@ def _create( assert p > 0 assert extcall IPriceOracle(price_oracle).price_w() == p + # TODO better diff blueprints from minimal proxy targets in naming vault: IVault = IVault(create_minimal_proxy_to(self.vault_impl)) amm: address = create_from_blueprint( self.amm_impl, @@ -313,7 +315,7 @@ def vaults_index(vault: IVault) -> uint256: def set_implementations(controller: address, amm: address, vault: address, pool_price_oracle: address, monetary_policy: address): """ - @notice Set new implementations (blueprints) for controller, amm, vault, pool price oracle and monetary polcy. + @notice Set new implementations (blueprints) for controller, amm, vault, pool price oracle and monetary policy. Doesn't change existing ones @param controller Address of the controller blueprint @param amm Address of the AMM blueprint diff --git a/tests/utils/deploy.py b/tests/utils/deploy.py index 0adad274..a13cb3d5 100644 --- a/tests/utils/deploy.py +++ b/tests/utils/deploy.py @@ -1,6 +1,6 @@ """ Deploy function for the complete llamalend protocol suite. -Provides deployment of both mint and lending protocols with all necessary contracts. +Provides deployment of both mint and lending markets with all necessary contracts. """ import boa From 02858861ef977997691c15ac0a009154d99b96f4 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 21 Aug 2025 15:46:11 +0200 Subject: [PATCH 124/413] test: add view llamalend controller deployer --- tests/utils/deployers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/utils/deployers.py b/tests/utils/deployers.py index 1dfeca09..8e388e2c 100644 --- a/tests/utils/deployers.py +++ b/tests/utils/deployers.py @@ -51,6 +51,7 @@ # Lending contracts - all have #pragma optimize codesize VAULT_DEPLOYER = boa.load_partial(LENDING_CONTRACT_PATH + "Vault.vy", compiler_args=compiler_args_codesize) LL_CONTROLLER_DEPLOYER = boa.load_partial(LENDING_CONTRACT_PATH + "LLController.vy", compiler_args=compiler_args_codesize) +LL_CONTROLLER_VIEW_DEPLOYER = boa.load_partial(LENDING_CONTRACT_PATH + "LLControllerView.vy", compiler_args=compiler_args_default) LENDING_FACTORY_DEPLOYER = boa.load_partial(LENDING_CONTRACT_PATH + "LendingFactory.vy", compiler_args=compiler_args_codesize) # Flashloan contracts From 706f48a32f7423427508071ad87df43d9f308b1d Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 21 Aug 2025 15:47:28 +0200 Subject: [PATCH 125/413] fix: pass view blueprint to lend factory --- contracts/interfaces/ILendingFactory.vyi | 1 + contracts/lending/LLControllerView.vy | 4 +++ contracts/lending/LendingFactory.vy | 43 +++++++++++++++++------- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/contracts/interfaces/ILendingFactory.vyi b/contracts/interfaces/ILendingFactory.vyi index 660ac92a..4d7b4fd4 100644 --- a/contracts/interfaces/ILendingFactory.vyi +++ b/contracts/interfaces/ILendingFactory.vyi @@ -4,6 +4,7 @@ event SetImplementations: vault: address price_oracle: address monetary_policy: address + view: address event SetDefaultRates: min_rate: uint256 diff --git a/contracts/lending/LLControllerView.vy b/contracts/lending/LLControllerView.vy index 4e550982..c350337c 100644 --- a/contracts/lending/LLControllerView.vy +++ b/contracts/lending/LLControllerView.vy @@ -36,7 +36,9 @@ def __init__( _amm: IAMM, _A: uint256, _collateral_token: IERC20, + _collateral_precision: uint256, _borrowed_token: IERC20, + _borrowed_precision: uint256 ): core.__init__( _controller, @@ -45,7 +47,9 @@ def __init__( _amm, _A, _collateral_token, + _collateral_precision, _borrowed_token, + _borrowed_precision ) diff --git a/contracts/lending/LendingFactory.vy b/contracts/lending/LendingFactory.vy index 98cf22ee..967bdc8b 100644 --- a/contracts/lending/LendingFactory.vy +++ b/contracts/lending/LendingFactory.vy @@ -36,6 +36,7 @@ controller_impl: public(address) vault_impl: public(address) pool_price_oracle_impl: public(address) monetary_policy_impl: public(address) +view_impl: public(address) # Actual min//max borrow rates when creating new markets # for example, 0.5% -> 50% is a good choice @@ -56,10 +57,11 @@ names: public(HashMap[uint256, String[64]]) @deploy def __init__( - amm: address, - controller: address, - vault: address, - pool_price_oracle: address, + amm_impl: address, + controller_impl: address, + vault_impl: address, + pool_price_oracle_impl: address, + view_impl: address, monetary_policy: address, admin: address, # TODO also add params votes? fee_receiver: address, @@ -73,11 +75,15 @@ def __init__( @param admin Admin address (DAO) @param fee_receiver Receiver of interest and admin fees """ - self.amm_impl = amm - self.controller_impl = controller - self.vault_impl = vault - self.pool_price_oracle_impl = pool_price_oracle + # TODO impl vs blueprint is confusing + self.amm_impl = amm_impl + self.controller_impl = controller_impl + self.vault_impl = vault_impl + # TODO everyone is forced to have the same price oracle? + self.pool_price_oracle_impl = pool_price_oracle_impl + # TODO everyone is forced to have the same monetary policy? self.monetary_policy_impl = monetary_policy + self.view_impl = view_impl # TODO is this actually useful? self.min_default_borrow_rate = 5 * 10**15 // (365 * 86400) @@ -140,7 +146,10 @@ def _create( self.controller_impl, vault, amm, borrowed_token, collateral_token, - monetary_policy, loan_discount, liquidation_discount, + monetary_policy, + loan_discount, + liquidation_discount, + self.view_impl, code_offset=3) extcall IAMM(amm).set_admin(controller) @@ -312,8 +321,14 @@ def vaults_index(vault: IVault) -> uint256: @external @nonreentrant -def set_implementations(controller: address, amm: address, vault: address, - pool_price_oracle: address, monetary_policy: address): +def set_implementations( + controller: address, + amm: address, + vault: address, + pool_price_oracle: address, + monetary_policy: address, + view: address, +): """ @notice Set new implementations (blueprints) for controller, amm, vault, pool price oracle and monetary policy. Doesn't change existing ones @@ -322,6 +337,7 @@ def set_implementations(controller: address, amm: address, vault: address, @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 view Address of the view contract blueprint """ assert msg.sender == self.admin @@ -335,13 +351,16 @@ def set_implementations(controller: address, amm: address, vault: address, self.pool_price_oracle_impl = pool_price_oracle if monetary_policy != empty(address): self.monetary_policy_impl = monetary_policy + if view != empty(address): + self.view_impl = view log ILendingFactory.SetImplementations( amm=amm, controller=controller, vault=vault, price_oracle=pool_price_oracle, - monetary_policy=monetary_policy + monetary_policy=monetary_policy, + view=view ) From d242e328999991950dbe5fd74c842a0039098634 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 21 Aug 2025 15:47:41 +0200 Subject: [PATCH 126/413] fix: use correct revert message --- tests/controller/test_set_price_oracle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/controller/test_set_price_oracle.py b/tests/controller/test_set_price_oracle.py index 48fd6e8e..fcff2fa7 100644 --- a/tests/controller/test_set_price_oracle.py +++ b/tests/controller/test_set_price_oracle.py @@ -172,7 +172,7 @@ def test_price_deviation_check_exceeds_limit(controller, different_price_oracle, # 10% price difference, but only 5% max deviation allowed max_deviation = 5 * 10**16 # 5% - with boa.reverts(dev="deviation > max"): + with boa.reverts("delta>max"): controller.set_price_oracle(different_price_oracle, max_deviation, sender=admin) From 1677d4e085da31ae3d2b61c04819317731cf470f Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 21 Aug 2025 15:48:05 +0200 Subject: [PATCH 127/413] fix: correct Protocol class --- tests/utils/deploy.py | 242 +++++++++++++++++++++--------------------- 1 file changed, 123 insertions(+), 119 deletions(-) diff --git a/tests/utils/deploy.py b/tests/utils/deploy.py index a13cb3d5..37ee0edf 100644 --- a/tests/utils/deploy.py +++ b/tests/utils/deploy.py @@ -4,7 +4,10 @@ """ import boa +from boa.contracts.vyper.vyper_contract import VyperDeployer, VyperBlueprint, VyperContract from typing import Dict, Any + + from tests.utils.deployers import ( # Core contracts STABLECOIN_DEPLOYER, @@ -15,6 +18,7 @@ # Lending contracts VAULT_DEPLOYER, LL_CONTROLLER_DEPLOYER, + LL_CONTROLLER_VIEW_DEPLOYER, LENDING_FACTORY_DEPLOYER, # Price oracles @@ -31,11 +35,26 @@ ) +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 + ll_controller: VyperBlueprint + ll_controller_view: VyperBlueprint + price_oracle: VyperBlueprint + mpolicy: VyperBlueprint + + +# TODO rename to Llamalend class Protocol: """ Protocol deployment and management class for llamalend. Handles deployment of core infrastructure and creation of markets. """ + def __init__( self, @@ -51,74 +70,75 @@ def __init__( self.admin = boa.env.generate_address("admin") self.fee_receiver = boa.env.generate_address("fee_receiver") + # Deploy all blueprints + self.blueprints = Blueprints( + amm=AMM_DEPLOYER, + mint_controller=MINT_CONTROLLER_DEPLOYER, + ll_controller=LL_CONTROLLER_DEPLOYER, + ll_controller_view=LL_CONTROLLER_VIEW_DEPLOYER, + price_oracle=CRYPTO_FROM_POOL_DEPLOYER, + mpolicy=SEMILOG_MONETARY_POLICY_DEPLOYER + ) + # Deploy core infrastructure with boa.env.prank(self.admin): # Deploy stablecoin self.crvUSD = STABLECOIN_DEPLOYER.deploy('Curve USD', 'crvUSD') - - # Deploy WETH - self.weth = WETH_DEPLOYER.deploy() - - # Deploy shared AMM implementation (used by both mint and lending) - self.amm_impl = AMM_DEPLOYER.deploy_as_blueprint() - - # Deploy a dummy price oracle for testing - self.price_oracle = DUMMY_PRICE_ORACLE_DEPLOYER.deploy(self.admin, initial_price) - - # Deploy Mint Protocol - # Deploy controller implementation - self.mint_controller_impl = MINT_CONTROLLER_DEPLOYER.deploy_as_blueprint() - - # 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 - self.mint_factory.set_implementations( - self.mint_controller_impl.address, - self.amm_impl.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) - self.mint_monetary_policy.set_rate(0) # 0% by default - - # Deploy Lending Protocol - # Deploy vault implementation - self.vault_impl = VAULT_DEPLOYER.deploy() - - # Deploy lending controller implementation - self.ll_controller_impl = LL_CONTROLLER_DEPLOYER.deploy_as_blueprint() - - # Deploy price oracle implementation for lending - self.price_oracle_impl = CRYPTO_FROM_POOL_DEPLOYER.deploy_as_blueprint() - - # Deploy monetary policy implementation for lending - self.mpolicy_impl = SEMILOG_MONETARY_POLICY_DEPLOYER.deploy_as_blueprint() - - # Deploy lending factory - self.lending_factory = LENDING_FACTORY_DEPLOYER.deploy( - self.amm_impl.address, - self.ll_controller_impl.address, - self.vault_impl.address, - self.price_oracle_impl.address, - self.mpolicy_impl.address, - self.admin, - self.fee_receiver - ) - + 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 vault implementation + self.vault_impl = VAULT_DEPLOYER.deploy() + + # Deploy lending factory + self.lending_factory = LENDING_FACTORY_DEPLOYER.deploy( + self.blueprints.amm.address, + self.blueprints.ll_controller.address, + self.vault_impl.address, + self.blueprints.price_oracle.address, + self.blueprints.ll_controller_view.address, + self.blueprints.mpolicy.address, + self.admin, + self.fee_receiver + ) + def create_mint_market( self, - collateral_token: Any, - price_oracle: Any, - monetary_policy: Any, + collateral_token: VyperContract, + price_oracle: VyperContract, + monetary_policy: VyperContract, A: int, amm_fee: int, admin_fee: int, @@ -166,13 +186,13 @@ def create_mint_market( def create_lending_market( self, - borrowed_token: Any, + borrowed_token: VyperContract, collateral_token: Any, A: int, fee: int, loan_discount: int, liquidation_discount: int, - price_oracle: Any, + price_oracle: VyperContract, name: str, min_borrow_rate: int, max_borrow_rate: int @@ -195,72 +215,56 @@ def create_lending_market( Returns: Dictionary with 'vault', 'controller', 'amm', 'oracle', and 'monetary_policy' addresses """ - with boa.env.prank(self.admin): - vault, controller, amm = self.lending_factory.create( - borrowed_token.address, - collateral_token.address, - A, - fee, - loan_discount, - liquidation_discount, - price_oracle.address, - name, - min_borrow_rate, - max_borrow_rate - ) - - return { - 'vault': vault, - 'controller': controller, - 'amm': amm - } - - def get_deployed_contracts(self) -> Dict[str, Any]: - """ - Get all deployed contracts in the protocol. + result = self.lending_factory.create( + borrowed_token.address, + collateral_token.address, + A, + fee, + loan_discount, + liquidation_discount, + price_oracle.address, + name, + min_borrow_rate, + max_borrow_rate + ) - Returns: - Dictionary containing all deployed contract addresses - """ return { - 'admin': self.admin, - 'crvUSD': self.crvUSD, - 'weth': self.weth, - 'amm_impl': self.amm_impl, - 'price_oracle': self.price_oracle, - 'mint_factory': self.mint_factory, - 'mint_controller_impl': self.mint_controller_impl, - 'mint_monetary_policy': self.mint_monetary_policy, - 'lending_factory': self.lending_factory, - 'vault_impl': self.vault_impl, - 'll_controller_impl': self.ll_controller_impl, - 'price_oracle_impl': self.price_oracle_impl, - 'mpolicy_impl': self.mpolicy_impl + 'vault': result[0], + 'controller': result[1], + 'amm': result[2] } + if __name__ == "__main__": - # import cProfile - # import pstats - - # profiler = cProfile.Profile() - # profiler.enable() - proto = Protocol() + + # Test mint market creation collat = ERC20_MOCK_DEPLOYER.deploy(18) - proto.create_mint_market( + mint_market = proto.create_mint_market( collat, proto.price_oracle, proto.mint_monetary_policy, - A=1000, + A=100, amm_fee=10**16, admin_fee=0, - loan_discount= int(0.8 * 10**18), - liquidation_discount=int(0.85 * 10**18), - debt_ceiling=1000 * 10**18 + loan_discount=9 * 10**16, # 9% + liquidation_discount=6 * 10**16, # 6% + debt_ceiling=10**6 * 10**18 ) - # profiler.disable() - # stats = pstats.Stats(profiler) - # stats.dump_stats('protocol_deploy.prof') - # print("Profile saved to protocol_deploy.prof") - # print("Run: snakeviz protocol_deploy.prof") \ No newline at end of file + # 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 + ) \ No newline at end of file From 261eb1cad76d8c9c2d923c66b2a345cb7ccf5337 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 21 Aug 2025 16:14:52 +0200 Subject: [PATCH 128/413] chore: simplify variable naming --- contracts/AMM.vy | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/contracts/AMM.vy b/contracts/AMM.vy index e83cdc70..3e72d645 100644 --- a/contracts/AMM.vy +++ b/contracts/AMM.vy @@ -77,13 +77,13 @@ active_band: public(int256) min_band: public(int256) max_band: public(int256) -_price_oracle_contract: IPriceOracle +_price_oracle: IPriceOracle # https://github.com/vyperlang/vyper/issues/4721 @view @external def price_oracle_contract() -> IPriceOracle: - return self._price_oracle_contract + return self._price_oracle old_p_o: uint256 @@ -118,9 +118,9 @@ def __init__( _sqrt_band_ratio: uint256, _log_A_ratio: int256, _base_price: uint256, - fee: uint256, - admin_fee: uint256, - price_oracle_contract: IPriceOracle, + _fee: uint256, + _admin_fee: uint256, + _price_oracle: IPriceOracle, ): """ @notice LLAMMA constructor @@ -131,9 +131,9 @@ 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 DEPRECATED, left for backward compatibility - @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 = _borrowed_token @@ -147,10 +147,10 @@ def __init__( A2 = pow_mod256(A, 2) Aminus12 = pow_mod256(unsafe_sub(A, 1), 2) - self.fee = fee - self._price_oracle_contract = price_oracle_contract + self.fee = _fee + self._price_oracle = _price_oracle self.prev_p_o_time = block.timestamp - self.old_p_o = staticcall self._price_oracle_contract.price() + self.old_p_o = staticcall self._price_oracle.price() self.rate_mul = 10**18 @@ -273,12 +273,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(staticcall self._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(extcall self._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] @@ -1723,5 +1723,5 @@ def set_price_oracle(_price_oracle: IPriceOracle): @param _price_oracle New price oracle contract """ assert msg.sender == self.admin - self._price_oracle_contract = _price_oracle + self._price_oracle = _price_oracle log IAMM.SetPriceOracle(price_oracle=_price_oracle) From c3b35f4ee03ad311d08494a3bb7933bc00cc992d Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 21 Aug 2025 16:15:19 +0200 Subject: [PATCH 129/413] test: simplify fixtures --- tests/controller/test_set_price_oracle.py | 43 ++++++++------------- tests/utils/deploy.py | 46 +++++++++++------------ 2 files changed, 39 insertions(+), 50 deletions(-) diff --git a/tests/controller/test_set_price_oracle.py b/tests/controller/test_set_price_oracle.py index fcff2fa7..061f71f3 100644 --- a/tests/controller/test_set_price_oracle.py +++ b/tests/controller/test_set_price_oracle.py @@ -1,9 +1,6 @@ import pytest import boa from tests.utils.deployers import ( - MINT_CONTROLLER_DEPLOYER, - LL_CONTROLLER_DEPLOYER, - AMM_DEPLOYER, DUMMY_PRICE_ORACLE_DEPLOYER, ERC20_MOCK_DEPLOYER, ) @@ -57,19 +54,11 @@ def market(request, mint_market, lend_market): @pytest.fixture() def controller(market): """Parametrized controller fixture that works with both market types.""" - # Check if it's a mint market by looking for 'controller' key structure - # Mint markets have 'controller' and 'amm' - # Lending markets have 'vault', 'controller', 'amm', 'oracle', 'monetary_policy' - if 'vault' in market: - # It's a lending market - return LL_CONTROLLER_DEPLOYER.at(market['controller']) - else: - # It's a mint market - return MINT_CONTROLLER_DEPLOYER.at(market['controller']) + return market['controller'] @pytest.fixture() def amm(market): - return AMM_DEPLOYER.at(market['amm']) + return market['amm'] @pytest.fixture(scope="module") @@ -78,20 +67,6 @@ def new_oracle(admin): return DUMMY_PRICE_ORACLE_DEPLOYER.deploy(admin, 3000 * 10**18, 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) - - -@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_default_behavior(controller, amm, new_oracle, admin): """Test normal oracle update with valid parameters.""" initial_oracle = amm.price_oracle_contract() @@ -129,6 +104,13 @@ def test_max_deviation_validation_boundary(controller, new_oracle, admin, amm): 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 @@ -158,6 +140,13 @@ def test_oracle_validation_missing_methods(controller, broken_oracle, admin): 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 diff --git a/tests/utils/deploy.py b/tests/utils/deploy.py index 37ee0edf..bfeb5090 100644 --- a/tests/utils/deploy.py +++ b/tests/utils/deploy.py @@ -161,28 +161,28 @@ def create_mint_market( debt_ceiling: Maximum debt for this market (e.g., 10**6 * 10**18) Returns: - Dictionary with 'controller' and 'amm' addresses + Dictionary with 'controller' and 'amm' contracts """ - with boa.env.prank(self.admin): - self.mint_factory.add_market( - collateral_token.address, - A, - amm_fee, - admin_fee, - price_oracle.address, - monetary_policy.address, - loan_discount, - liquidation_discount, - debt_ceiling - ) + self.mint_factory.add_market( + collateral_token.address, + A, + amm_fee, + admin_fee, + 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) + controller_address = self.mint_factory.get_controller(collateral_token.address) + amm_address = self.mint_factory.get_amm(collateral_token.address) - return { - 'controller': controller_address, - 'amm': amm_address - } + return { + 'controller': MINT_CONTROLLER_DEPLOYER.at(controller_address), + 'amm': AMM_DEPLOYER.at(amm_address) + } def create_lending_market( self, @@ -213,7 +213,7 @@ def create_lending_market( max_borrow_rate: Maximum borrow rate (e.g., 50 * 10**16 for 50%) Returns: - Dictionary with 'vault', 'controller', 'amm', 'oracle', and 'monetary_policy' addresses + Dictionary with 'vault', 'controller', 'amm' contracts. """ result = self.lending_factory.create( borrowed_token.address, @@ -229,9 +229,9 @@ def create_lending_market( ) return { - 'vault': result[0], - 'controller': result[1], - 'amm': result[2] + 'vault': VAULT_DEPLOYER.at(result[0]), + 'controller': LL_CONTROLLER_DEPLOYER.at(result[1]), + 'amm': AMM_DEPLOYER.at(result[2]) } From 95a533a0e53845d8cbf69bdb6ee80114a36e7e9c Mon Sep 17 00:00:00 2001 From: macket Date: Fri, 22 Aug 2025 17:58:24 +0400 Subject: [PATCH 130/413] test: adjust AMM tests --- tests/amm/test_amount_for_price.py | 9 ++++---- tests/amm/test_deposit_withdraw.py | 3 ++- tests/amm/test_exchange.py | 9 ++++---- tests/amm/test_exchange_dy.py | 11 ++++----- tests/amm/test_flip.py | 7 +++--- tests/amm/test_flip_dy.py | 7 +++--- tests/amm/test_oracle_change_noloss.py | 23 ++++++++++--------- tests/amm/test_price_oracles.py | 7 +++++- tests/amm/test_st_exchange.py | 13 ++++------- tests/amm/test_st_exchange_dy.py | 13 ++++------- tests/amm/test_xdown_yup_invariants.py | 27 ++++++++++++----------- tests/amm/test_xdown_yup_invariants_dy.py | 15 +++++++------ tests/utils/__init__.py | 5 +++++ 13 files changed, 79 insertions(+), 70 deletions(-) diff --git a/tests/amm/test_amount_for_price.py b/tests/amm/test_amount_for_price.py index ba5ed42d..d307f69c 100644 --- a/tests/amm/test_amount_for_price.py +++ b/tests/amm/test_amount_for_price.py @@ -2,6 +2,7 @@ from ..conftest import approx from hypothesis import given, settings from hypothesis import strategies as st +from ..utils import mint_for_testing @given( @@ -25,7 +26,7 @@ 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) - boa.deal(collateral_token, amm.address, deposit_amount) + mint_for_testing(collateral_token, amm.address, deposit_amount) prices = [oracle_price] prices.append(amm.get_p()) @@ -34,7 +35,7 @@ def test_amount_for_price(price_oracle, amm, accounts, collateral_token, borrowe # Dump some to be somewhere inside the bands eamount = int(deposit_amount * amm.get_p() // 10**18 * init_trade_frac) if eamount > 0: - boa.deal(borrowed_token, 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,11 +50,11 @@ def test_amount_for_price(price_oracle, amm, accounts, collateral_token, borrowe assert is_pump == (p_final >= p_initial) if is_pump: - boa.deal(borrowed_token, user, amount) + mint_for_testing(borrowed_token, user, amount) amm.exchange(0, 1, amount, 0) else: - boa.deal(collateral_token, user, amount) + mint_for_testing(collateral_token, user, amount) amm.exchange(1, 0, amount, 0) p = amm.get_p() diff --git a/tests/amm/test_deposit_withdraw.py b/tests/amm/test_deposit_withdraw.py index 8ce61952..3a2fed25 100644 --- a/tests/amm/test_deposit_withdraw.py +++ b/tests/amm/test_deposit_withdraw.py @@ -2,6 +2,7 @@ from ..conftest import approx from hypothesis import given from hypothesis import strategies as st +from ..utils import mint_for_testing DEAD_SHARES = 10**3 @@ -28,7 +29,7 @@ def test_deposit_withdraw(amm, amounts, accounts, ns, dns, fracs, collateral_tok amm.deposit_range(user, amount, n1, n2) else: amm.deposit_range(user, amount, n1, n2) - boa.deal(collateral_token, amm.address, amount) + mint_for_testing(collateral_token, amm.address, amount) deposits[user] = amount assert collateral_token.balanceOf(user) == 0 diff --git a/tests/amm/test_exchange.py b/tests/amm/test_exchange.py index 7df7582d..dc92ad02 100644 --- a/tests/amm/test_exchange.py +++ b/tests/amm/test_exchange.py @@ -2,6 +2,7 @@ from ..conftest import approx from hypothesis import given from hypothesis import strategies as st +from ..utils import mint_for_testing @given( @@ -14,7 +15,7 @@ def test_dxdy_limits(amm, amounts, accounts, ns, dns, collateral_token, admin): for user, amount, n1, dn in zip(accounts[1:6], amounts, ns, dns): n2 = n1 + dn amm.deposit_range(user, amount, n1, n2) - boa.deal(collateral_token, amm.address, amount) + mint_for_testing(collateral_token, amm.address, amount) # Swap 0 dx, dy = amm.get_dxdy(0, 1, 0) @@ -60,7 +61,7 @@ def test_exchange_down_up(amm, amounts, accounts, ns, dns, amount, amm.deposit_range(user, amount, n1, n2) else: amm.deposit_range(user, amount, n1, n2) - boa.deal(collateral_token, amm.address, amount) + mint_for_testing(collateral_token, amm.address, amount) p_before = amm.get_p() @@ -69,7 +70,7 @@ def test_exchange_down_up(amm, amounts, accounts, ns, dns, amount, dx2, dy2 = amm.get_dxdy(0, 1, dx) assert dx == dx2 assert approx(dy, dy2, 1e-6) - boa.deal(borrowed_token, u, dx2) + mint_for_testing(borrowed_token, u, dx2) with boa.env.prank(u): amm.exchange(0, 1, dx2, 0) assert borrowed_token.balanceOf(u) == 0 @@ -91,7 +92,7 @@ def test_exchange_down_up(amm, amounts, accounts, ns, dns, amount, assert dy <= expected_out_amount assert abs(dy - expected_out_amount) <= 2 * fee * expected_out_amount - boa.deal(collateral_token, 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 40d1e2c0..bf81a70b 100644 --- a/tests/amm/test_exchange_dy.py +++ b/tests/amm/test_exchange_dy.py @@ -3,6 +3,7 @@ from ..conftest import approx from hypothesis import given from hypothesis import strategies as st +from ..utils import mint_for_testing @pytest.fixture(scope="module") @@ -29,7 +30,7 @@ def test_dydx_limits(amm, amounts, accounts, ns, dns, collateral_token, admin, b for user, amount, n1, dn in zip(accounts[1:6], amounts, ns, dns): n2 = n1 + dn amm.deposit_range(user, amount, n1, n2) - boa.deal(collateral_token, amm.address, amount) + mint_for_testing(collateral_token, amm.address, amount) # Swap 0 dx, dy = amm.get_dydx(0, 1, 0) @@ -77,7 +78,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) - boa.deal(collateral_token, amm.address, amount) + mint_for_testing(collateral_token, amm.address, amount) # Swap 0 dy, dx = amm.get_dydx(0, 1, 0) @@ -142,7 +143,7 @@ def test_exchange_dy_down_up(amm, amounts, accounts, ns, dns, amount, borrowed_t amm.deposit_range(user, amount, n1, n2) else: amm.deposit_range(user, amount, n1, n2) - boa.deal(collateral_token, amm.address, amount) + mint_for_testing(collateral_token, amm.address, amount) p_before = amm.get_p() @@ -152,7 +153,7 @@ def test_exchange_dy_down_up(amm, amounts, accounts, ns, dns, amount, borrowed_t dy2, dx2 = amm.get_dydx(0, 1, dy) assert dy == dy2 assert approx(dx, dx2, 1e-6) - boa.deal(borrowed_token, u, dx2) + 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 @@ -177,7 +178,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 - boa.deal(collateral_token, 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 732a44b2..7fba3f3a 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) - boa.deal(collateral_token, 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)) @@ -36,7 +37,7 @@ def test_flip(amm, price_oracle, collateral_token, borrowed_token, accounts, adm dx = int(STEP * AMOUNT_D * p / 1e18 / 10**(18-6)) is_empty = False while amm.get_p() < p: - boa.deal(borrowed_token, 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)) @@ -64,7 +65,7 @@ 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: - boa.deal(collateral_token, 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)) diff --git a/tests/amm/test_flip_dy.py b/tests/amm/test_flip_dy.py index 87a121af..603a976f 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?) @@ -33,7 +34,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) - boa.deal(collateral_token, 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)) @@ -53,7 +54,7 @@ def test_flip(amm, price_oracle, collateral_token, borrowed_token, accounts, adm while amm.get_p() < p: dy = amm.get_dy(0, 1, dx) dx = amm.get_dx(0, 1, dy) - boa.deal(borrowed_token, 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)) @@ -88,7 +89,7 @@ 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: - boa.deal(collateral_token, 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)) diff --git a/tests/amm/test_oracle_change_noloss.py b/tests/amm/test_oracle_change_noloss.py index 811bc6f5..311c2cb8 100644 --- a/tests/amm/test_oracle_change_noloss.py +++ b/tests/amm/test_oracle_change_noloss.py @@ -4,6 +4,7 @@ import pytest from hypothesis import given, settings, example from hypothesis import strategies as st +from ..utils import mint_for_testing @pytest.fixture(scope="module") @@ -31,10 +32,10 @@ def test_buy_with_shift(amm, collateral_token, borrowed_token, price_oracle, acc # Deposit with boa.env.prank(admin): amm.deposit_range(user, collateral_amount, n1, n1 + dn) - boa.deal(collateral_token, amm.address, collateral_amount) + mint_for_testing(collateral_token, amm.address, collateral_amount) # Swap stablecoin for collateral - boa.deal(borrowed_token, 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 +50,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 - boa.deal(collateral_token, 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 @@ -78,10 +79,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) - boa.deal(collateral_token, amm.address, collateral_amount) + mint_for_testing(collateral_token, amm.address, collateral_amount) # Swap max (buy) - boa.deal(borrowed_token, user, MANY) + mint_for_testing(borrowed_token, user, MANY) with boa.env.prank(user): amm.exchange(0, 1, MANY, 0) @@ -125,10 +126,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) - boa.deal(collateral_token, amm.address, collateral_amount) + mint_for_testing(collateral_token, amm.address, collateral_amount) # Swap stablecoin for collateral - boa.deal(borrowed_token, 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,7 +144,7 @@ def test_no_untradable_funds(amm, collateral_token, borrowed_token, price_oracle price_oracle.set_price(int(price_oracle.price() * price_shift)) # Trade back - boa.deal(collateral_token, 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 @@ -170,10 +171,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) - boa.deal(collateral_token, amm.address, collateral_amount) + mint_for_testing(collateral_token, amm.address, collateral_amount) # Swap stablecoin for collateral - boa.deal(borrowed_token, 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,7 +189,7 @@ 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 - boa.deal(collateral_token, 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) diff --git a/tests/amm/test_price_oracles.py b/tests/amm/test_price_oracles.py index 1acd877a..427ba3d0 100644 --- a/tests/amm/test_price_oracles.py +++ b/tests/amm/test_price_oracles.py @@ -1,5 +1,6 @@ import boa import pytest +import eth_utils from ..conftest import PRICE, approx from tests.utils.deployers import EMA_PRICE_ORACLE_DEPLOYER @@ -7,7 +8,11 @@ @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] + fn_abi = price_oracle.price._abi + fn_name = fn_abi["name"] + arg_types = ",".join(i["type"] for i in fn_abi["inputs"]) + signature = f"{fn_name}({arg_types})" + signature = eth_utils.keccak(text=signature)[:4] signature = b'\x00' * (32 - len(signature)) + signature return EMA_PRICE_ORACLE_DEPLOYER.deploy(10000, price_oracle.address, signature) diff --git a/tests/amm/test_st_exchange.py b/tests/amm/test_st_exchange.py index 29f8692e..06028b7e 100644 --- a/tests/amm/test_st_exchange.py +++ b/tests/amm/test_st_exchange.py @@ -5,6 +5,7 @@ from hypothesis import strategies as st from hypothesis.stateful import RuleBasedStateMachine, run_state_machine_as_test, rule, invariant, initialize from hypothesis import Phase +from ..utils import mint_for_testing class StatefulExchange(RuleBasedStateMachine): @@ -14,7 +15,6 @@ class StatefulExchange(RuleBasedStateMachine): 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__() @@ -30,7 +30,7 @@ def initializer(self, amounts, ns, dns): try: with boa.env.prank(self.admin): self.amm.deposit_range(user, amount, n1, n2) - boa.deal(collateral_token, 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): assert amount // (dn + 1) <= 100 @@ -53,15 +53,10 @@ def exchange(self, amount, pump, user_id): in_token = self.collateral_token u_amount = in_token.balanceOf(u) if amount > u_amount: - boa.deal(in_token, 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)) @@ -88,7 +83,7 @@ def teardown(self): if n < 50: _, dy = self.amm.get_dxdy(1, 0, to_swap) if dy > 0: - boa.deal(collateral_token, 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)) diff --git a/tests/amm/test_st_exchange_dy.py b/tests/amm/test_st_exchange_dy.py index 1f00538c..928e5963 100644 --- a/tests/amm/test_st_exchange_dy.py +++ b/tests/amm/test_st_exchange_dy.py @@ -5,6 +5,7 @@ from hypothesis import strategies as st from hypothesis.stateful import RuleBasedStateMachine, run_state_machine_as_test, rule, invariant, initialize from hypothesis import Phase +from ..utils import mint_for_testing class StatefulExchange(RuleBasedStateMachine): @@ -14,7 +15,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__() @@ -30,7 +30,7 @@ def initializer(self, amounts, ns, dns): try: with boa.env.prank(self.admin): self.amm.deposit_range(user, amount, n1, n2) - boa.deal(collateral_token, 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): assert amount // (dn + 1) <= 100 @@ -54,15 +54,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: - boa.deal(in_token, 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)) @@ -92,7 +87,7 @@ def teardown(self): if n < 50: dy, dx = self.amm.get_dydx(1, 0, to_receive) if dy > 0: - boa.deal(collateral_token, 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)) diff --git a/tests/amm/test_xdown_yup_invariants.py b/tests/amm/test_xdown_yup_invariants.py index 4e32711a..01881910 100644 --- a/tests/amm/test_xdown_yup_invariants.py +++ b/tests/amm/test_xdown_yup_invariants.py @@ -2,6 +2,7 @@ from hypothesis import strategies as st import boa 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) @@ -25,11 +26,11 @@ def test_immediate(amm, price_oracle, collateral_token, borrowed_token, accounts price_oracle.set_price(p_o) amm.set_fee(0) amm.deposit_range(user, deposit_amount, n1, n1+dn) - boa.deal(collateral_token, amm.address, deposit_amount) + 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): - boa.deal(borrowed_token, 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 +47,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): - boa.deal(borrowed_token, 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): - boa.deal(collateral_token, user, trade_amount) + mint_for_testing(collateral_token, user, trade_amount) i = 1 j = 0 @@ -80,13 +81,13 @@ def test_immediate_above_p0(amm, price_oracle, collateral_token, borrowed_token, with boa.env.prank(admin): amm.set_fee(0) amm.deposit_range(user, deposit_amount, 6, 6) - boa.deal(collateral_token, 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): - boa.deal(borrowed_token, 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 +95,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 - boa.deal(collateral_token, user, trade_amount) + mint_for_testing(collateral_token, user, trade_amount) with boa.env.prank(user): amm.exchange(1, 0, trade_amount, 0) @@ -122,13 +123,13 @@ def test_immediate_in_band(amm, price_oracle, collateral_token, borrowed_token, with boa.env.prank(admin): amm.set_fee(0) amm.deposit_range(user, deposit_amount, 4, 4) - boa.deal(collateral_token, 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): - boa.deal(borrowed_token, 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 +138,7 @@ def test_immediate_in_band(amm, price_oracle, collateral_token, borrowed_token, trade_amount = 2690425910633510 # 181406004646580 with boa.env.prank(user): - boa.deal(borrowed_token, user, trade_amount) + mint_for_testing(borrowed_token, user, trade_amount) amm.exchange(0, 1, trade_amount, 0) p_after_2 = amm.get_p() @@ -172,7 +173,7 @@ def test_adiabatic(amm, price_oracle, collateral_token, borrowed_token, accounts with boa.env.prank(admin): amm.set_fee(0) amm.deposit_range(user, deposit_amount, dn, n1+dn) - boa.deal(collateral_token, amm.address, deposit_amount) + 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) @@ -198,11 +199,11 @@ def test_adiabatic(amm, price_oracle, collateral_token, borrowed_token, accounts if is_pump: i = 0 j = 1 - boa.deal(borrowed_token, user, amount) + mint_for_testing(borrowed_token, user, amount) else: i = 1 j = 0 - boa.deal(collateral_token, user, amount) + mint_for_testing(collateral_token, user, amount) with boa.env.prank(user): amm.exchange(i, j, amount, 0) diff --git a/tests/amm/test_xdown_yup_invariants_dy.py b/tests/amm/test_xdown_yup_invariants_dy.py index f0c5cb6b..5c34d2e8 100644 --- a/tests/amm/test_xdown_yup_invariants_dy.py +++ b/tests/amm/test_xdown_yup_invariants_dy.py @@ -3,6 +3,7 @@ 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) @@ -41,7 +42,7 @@ def test_immediate(amm, price_oracle, collateral_token, borrowed_token, accounts price_oracle.set_price(p_o) amm.set_fee(0) amm.deposit_range(user, deposit_amount, n1, n1+dn) - boa.deal(collateral_token, amm.address, deposit_amount) + 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 +54,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): - boa.deal(borrowed_token, 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 +64,14 @@ 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): - boa.deal(borrowed_token, 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, trade_amount = amm.get_dydx(1, 0, trade_recv_amount) with boa.env.prank(user): - boa.deal(collateral_token, user, trade_amount) + mint_for_testing(collateral_token, user, trade_amount) i = 1 j = 0 @@ -108,7 +109,7 @@ def test_adiabatic(amm, price_oracle, collateral_token, borrowed_token, accounts with boa.env.prank(admin): amm.set_fee(0) amm.deposit_range(user, deposit_amount, dn, n1+dn) - boa.deal(collateral_token, amm.address, deposit_amount) + 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) @@ -136,13 +137,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) - boa.deal(borrowed_token, 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) - boa.deal(collateral_token, user, _amount) + mint_for_testing(collateral_token, user, _amount) with boa.env.prank(user): amm.exchange_dy(i, j, recv_amount, _amount) diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py index e69de29b..fc33ace4 100644 --- a/tests/utils/__init__.py +++ b/tests/utils/__init__.py @@ -0,0 +1,5 @@ +import boa + + +def mint_for_testing(token, to, amount): + boa.deal(token, to, token.balanceOf(to) + amount) From dbe4336c0846b61b1f4f5e9a7e2570793704fc4a Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 22 Aug 2025 16:57:36 +0200 Subject: [PATCH 131/413] ci: temporarily reudce test set --- .github/workflows/test.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index e4372810..c678a2cf 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -15,11 +15,12 @@ jobs: folder: - "tests/flashloan" - "tests/lm_callback" + - "tests/controller" - "tests/amm" - - "tests/lending" - "tests/price_oracles" - - "tests/swap" - - "tests/controller" + # - "tests/lending" + # - "tests/stableborrow" + # - "tests/swap" venom: - { name: "standard mode", value: false } # - { name: "venom mode", value: true } From 13fd79f9411ef9c43084f3cffbd12eedfa0f647d Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 22 Aug 2025 16:57:58 +0200 Subject: [PATCH 132/413] chore: tackle todos --- contracts/Controller.vy | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index da49d26c..da974502 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -227,7 +227,6 @@ def redeemed() -> uint256: @internal @view def _check_admin(): - # TODO possibly some things can be moved from ownership to param votes? assert msg.sender == staticcall FACTORY.admin(), "only admin" @@ -379,12 +378,6 @@ def _debt(user: address) -> (uint256, uint256): return (debt, rate_mul) -# TODO check if needed -# @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 debt(user: address) -> uint256: @@ -1320,7 +1313,7 @@ def users_to_liquidate( @view @external -@reentrant # TODO make this consistent +@reentrant def amm_price() -> uint256: """ @notice Current price from the AMM From cb185bf7dae1a2b0164049e053a84f3e45f361cb Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 22 Aug 2025 16:58:08 +0200 Subject: [PATCH 133/413] chore: more todos --- contracts/ControllerView.vy | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/ControllerView.vy b/contracts/ControllerView.vy index 6e000f78..deac1466 100644 --- a/contracts/ControllerView.vy +++ b/contracts/ControllerView.vy @@ -361,7 +361,6 @@ def max_borrowable( ) -# TODO use pointers to natpsec @external @view def min_collateral( From 872b92cbf754c09d27822196053b3e4e20b128b0 Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 22 Aug 2025 17:02:29 +0200 Subject: [PATCH 134/413] chore: remove legacy code --- contracts/deprecated/BoostedLMCallback.vy | 456 ----- contracts/deprecated/Controller.vy | 1536 ----------------- .../lending/deprecated/LiquidityGauge.vy | 864 ---------- .../deprecated/OneWayLendingFactory.vy | 435 ----- .../deprecated/OneWayLendingFactoryL2.vy | 421 ----- .../deprecated/TwoWayLendingFactory.vy | 625 ------- contracts/lending/deprecated/Vault.vy | 692 -------- 7 files changed, 5029 deletions(-) delete mode 100644 contracts/deprecated/BoostedLMCallback.vy delete mode 100644 contracts/deprecated/Controller.vy delete mode 100644 contracts/lending/deprecated/LiquidityGauge.vy delete mode 100644 contracts/lending/deprecated/OneWayLendingFactory.vy delete mode 100644 contracts/lending/deprecated/OneWayLendingFactoryL2.vy delete mode 100644 contracts/lending/deprecated/TwoWayLendingFactory.vy delete mode 100644 contracts/lending/deprecated/Vault.vy diff --git a/contracts/deprecated/BoostedLMCallback.vy b/contracts/deprecated/BoostedLMCallback.vy deleted file mode 100644 index ffe2b87f..00000000 --- a/contracts/deprecated/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/deprecated/Controller.vy b/contracts/deprecated/Controller.vy deleted file mode 100644 index 02aa1960..00000000 --- a/contracts/deprecated/Controller.vy +++ /dev/null @@ -1,1536 +0,0 @@ -# @version 0.3.10 -# pragma optimize codesize -# pragma evm-version shanghai -""" -@title crvUSD Controller -@author Curve.Fi -@license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved -""" - -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) -MAX_LOAN_DISCOUNT: constant(uint256) = 5 * 10**17 -MIN_LIQUIDATION_DISCOUNT: constant(uint256) = 10**16 # Start liquidating when threshold reached -MAX_TICKS: constant(int256) = 50 -MAX_TICKS_UINT: constant(uint256) = 50 -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 - -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 - -minted: public(uint256) -redeemed: public(uint256) - -monetary_policy: public(MonetaryPolicy) -liquidation_discount: public(uint256) -loan_discount: public(uint256) - -COLLATERAL_TOKEN: immutable(ERC20) -COLLATERAL_PRECISION: immutable(uint256) - -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) - -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 - -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) - -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 - -approval: public(HashMap[address, HashMap[address, bool]]) -extra_health: public(HashMap[address, uint256]) - - -@external -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) - - self.monetary_policy = MonetaryPolicy(monetary_policy) - - 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()) - - COLLATERAL_TOKEN = _collateral_token - BORROWED_TOKEN = _borrowed_token - COLLATERAL_PRECISION = pow_mod256(10, 18 - _collateral_token.decimals()) - BORROWED_PRECISION = pow_mod256(10, 18 - _borrowed_token.decimals()) - - SQRT_BAND_RATIO = isqrt(unsafe_div(10**36 * _A, unsafe_sub(_A, 1))) - - assert _borrowed_token.approve(msg.sender, max_value(uint256), default_return_value=True) - - -@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 - - -@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 - - -@external -@pure -def factory() -> Factory: - """ - @notice Address of the factory - """ - return FACTORY - - -@external -@pure -def amm() -> LLAMMA: - """ - @notice Address of the AMM - """ - return AMM - - -@external -@pure -def collateral_token() -> ERC20: - """ - @notice Address of the collateral token - """ - return COLLATERAL_TOKEN - - -@external -@pure -def borrowed_token() -> ERC20: - """ - @notice Address of the borrowed token - """ - return BORROWED_TOKEN - - -@internal -def _save_rate(): - """ - @notice Save current rate - """ - rate: uint256 = min(self.monetary_policy.rate_write(), MAX_RATE) - AMM.set_rate(rate) - - -@external -@nonreentrant('lock') -def save_rate(): - """ - @notice Save current rate - """ - self._save_rate() - - -@internal -@view -def _debt(user: address) -> (uint256, uint256): - """ - @notice Get the value of debt and rate_mul and update the rate_mul counter - @param user User address - @return (debt, rate_mul) - """ - rate_mul: uint256 = AMM.get_rate_mul() - loan: Loan = self.loan[user] - if loan.initial_debt == 0: - return (0, rate_mul) - else: - # Let user repay 1 smallest decimal more so that the system doesn't lose on precision - # Use ceil div - debt: uint256 = loan.initial_debt * rate_mul - 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 - return (debt, rate_mul) - - -@external -@view -@nonreentrant('lock') -def debt(user: address) -> uint256: - """ - @notice Get the value of debt without changing the state - @param user User address - @return Value of debt - """ - return self._debt(user)[0] - - -@external -@view -@nonreentrant('lock') -def loan_exists(user: address) -> bool: - """ - @notice Check whether there is a loan of `user` in existence - """ - return self.loan[user].initial_debt > 0 - - -# No decorator because used in monetary policy -@external -@view -def total_debt() -> uint256: - """ - @notice Total debt of this controller - """ - rate_mul: uint256 = AMM.get_rate_mul() - loan: Loan = self._total_debt - return loan.initial_debt * rate_mul / loan.rate_mul - - -@internal -@pure -def get_y_effective(collateral: uint256, N: uint256, discount: uint256) -> uint256: - """ - @notice Intermediary method which calculates y_effective defined as x_effective / p_base, - however discounted by loan_discount. - x_effective is an amount which can be obtained from collateral when liquidating - @param collateral Amount of collateral to get the value for - @param N Number of bands the deposit is made into - @param discount Loan discount at 1e18 base (e.g. 1e18 == 100%) - @return y_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_effective * p_oracle_up(n1) - # d_y_effective = y / N / sqrt(A / (A - 1)) - # 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) - ), - unsafe_mul(SQRT_BAND_RATIO, N)) - y_effective: uint256 = d_y_effective - for i in range(1, MAX_TICKS_UINT): - if i == N: - break - d_y_effective = unsafe_div(d_y_effective * Aminus1, 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: - """ - @notice Calculate the upper band number for the deposit to sit in to support - the given debt. 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 - @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) - - # 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]) - # p_oracle_up(n1) = base_price * ((A - 1) / A)**n1 - - # We borrow up until min band touches p_oracle, - # or it touches non-empty bands which cannot be skipped. - # We calculate required n1 for given (collateral, debt), - # and if n1 corresponds to price_oracle being too high, or unreachable band - # - 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 - - # 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) - if n1 < 0: - 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" - - # 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" - - 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: - """ - @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 - - -@external -@view -@nonreentrant('lock') -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 - @param user User to calculate the value for (only necessary for nonzero extra_health) - @return Minimal collateral required - """ - # 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) - - -@external -@view -@nonreentrant('lock') -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. - @param collateral Amount of collateral (at its native precision) - @param debt Amount of requested debt - @param N Number of bands to deposit into - @param user User to calculate n1 for (only necessary for nonzero extra_health) - @return Upper band n1 (n1 <= n2) to deposit into. Signed integer - """ - return self._calculate_debt_n1(collateral, debt, N, user) - - -@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) - - -@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: - 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) - - # Callback - response: Bytes[64] = raw_call( - callbacker, - concat(callback_sig, _abi_encode(user, stablecoins, collateral, debt, callback_args, callback_bytes)), - max_outsize=64 - ) - data.stablecoins = 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) - - 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) - - -@external -@nonreentrant('lock') -def create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address = msg.sender): - """ - @notice Create loan - @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 _for Address to create the loan for - """ - 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) - - -@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) - - # Before callback - self.transfer(BORROWED_TOKEN, callbacker, debt) - - # 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._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) - - -@internal -def _add_collateral_borrow(d_collateral: uint256, d_debt: uint256, _for: address, remove_collateral: bool, - check_rounding: bool): - """ - @notice Internal method to borrow and add or remove collateral - @param d_collateral Amount of collateral to add - @param d_debt Amount of debt increase - @param _for Address to transfer tokens to - @param remove_collateral Remove collateral instead of adding - @param check_rounding Check that amount added is no less than the rounding error on the loan - """ - debt: uint256 = 0 - rate_mul: uint256 = 0 - debt, rate_mul = self._debt(_for) - assert debt > 0, "Loan doesn't exist" - 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" - if remove_collateral: - xy[1] -= d_collateral - else: - xy[1] += d_collateral - if check_rounding: - # We need d(x + p*y) > 1 wei. For that, we do an equivalent check (but with x2 for safety) - # 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 - 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}) - - liquidation_discount: uint256 = 0 - if _for == msg.sender: - liquidation_discount = self.liquidation_discount - self.liquidation_discounts[_for] = liquidation_discount - 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) - else: - log Borrow(_for, d_collateral, d_debt) - - log UserState(_for, xy[1], debt, n1, n2, liquidation_discount) - - -@external -@nonreentrant('lock') -def add_collateral(collateral: uint256, _for: address = msg.sender): - """ - @notice Add extra collateral to avoid bad liqidations - @param collateral Amount of collateral to add - @param _for Address to add collateral for - """ - if collateral == 0: - return - self._add_collateral_borrow(collateral, 0, _for, False, _for != msg.sender) - self.transferFrom(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 - @param collateral Amount of collateral to remove - @param _for Address to remove collateral for - """ - if collateral == 0: - return - assert self._check_approval(_for) - self._add_collateral_borrow(collateral, 0, _for, True, False) - self.transferFrom(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) - @param collateral Amount of collateral to add - @param debt Amount of stablecoin 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 - """ - if debt == 0: - return - assert self._check_approval(_for) - - # Before callback - self.transfer(BORROWED_TOKEN, callbacker, debt) - - # 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) - self._save_rate() - - -@internal -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 - self.loan_ix[_for] = 0 - if loan_ix < last_loan_ix: # Need to replace - last_loan: address = self.loans[last_loan_ix] - self.loans[loan_ix] = last_loan - self.loan_ix[last_loan] = loan_ix - 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) - - 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) - - 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) - - 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 - - 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 - - self._save_rate() - - -@external -@nonreentrant('lock') -def repay_extended(callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b"", _for: address = msg.sender): - """ - @notice Repay loan but get a stablecoin for that from callback (to deleverage) - @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 - """ - 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 - 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 - - 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 - - # No need to check _health() because it's the _for - - # 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 - - self._save_rate() - - -@internal -@view -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 - @param user User address to calculate health for - @param debt The amount of debt to calculate health for - @param full Whether to take into account the price difference above the highest user's band - @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 - - 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) - 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) - - return health - - -@external -@view -@nonreentrant('lock') -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 - @param d_collateral Change in collateral amount (signed) - @param d_debt Change in debt amount (signed) - @param full Whether it's a 'full' health or not - @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 - - -@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) - - 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""): - """ - @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 - @param callbacker Address of the callback contract - @param callback_args Extra arguments for the callback (up to 5) such as min_amount etc - """ - 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" - - final_debt: uint256 = debt - debt = unsafe_div(debt * frac + (10**18 - 1), 10**18) - assert debt > 0 - final_debt = unsafe_sub(final_debt, debt) - - # Withdraw sender's stablecoin 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] - - # 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) - - 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]) - # Request what's left from user - self.transferFrom(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 - # 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) - - else: - # Withdraw collateral - self.transferFrom(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)) - - 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) - if final_debt == 0: - log UserState(user, 0, 0, 0, 0, 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: - """ - @notice Calculate the amount of stablecoins 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 - """ - 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) - - return unsafe_sub(max(debt, stablecoins), stablecoins) - - -@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]) - - -@view -@external -@nonreentrant('lock') -def users_to_liquidate(_from: uint256=0, _limit: uint256=0) -> DynArray[Position, 1000]: - """ - @notice Returns a dynamic array of users who can be "hard-liquidated". - This method is designed for convenience of liquidation bots. - @param _from Loan index to start iteration from - @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 -@view -@external -def amm_price() -> uint256: - """ - @notice Current price from the AMM - """ - return 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])] - - -@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) - """ - 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)] - - -# AMM has nonreentrant decorator -@external -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 - """ - assert msg.sender == FACTORY.admin() - assert fee <= MAX_FEE and fee >= MIN_FEE, "Fee" - AMM.set_fee(fee) - - -@nonreentrant('lock') -@external -def set_monetary_policy(monetary_policy: address): - """ - @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) - - -@nonreentrant('lock') -@external -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() - 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) - - -@external -@nonreentrant('lock') -def set_callback(cb: address): - """ - @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) - - -@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() - - # 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 - - self._save_rate() - - # 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 - - -@external -@view -@nonreentrant('lock') -def check_lock() -> bool: - return True - - -# Allowance methods - -@external -def approve(_spender: address, _allow: bool): - """ - @notice Allow another address to borrow and repay for the user - @param _spender Address to whitelist for the action - @param _allow Whether to turn the approval on or off (no amounts) - """ - self.approval[msg.sender][_spender] = _allow - log Approval(msg.sender, _spender, _allow) - - -@internal -@view -def _check_approval(_for: address) -> bool: - return msg.sender == _for or self.approval[_for][msg.sender] - - -@external -def set_extra_health(_value: uint256): - """ - @notice Add a little bit more to loan_discount to start SL with health higher than usual - @param _value 1e18-based addition to loan_discount - """ - self.extra_health[msg.sender] = _value - log SetExtraHealth(msg.sender, _value) diff --git a/contracts/lending/deprecated/LiquidityGauge.vy b/contracts/lending/deprecated/LiquidityGauge.vy deleted file mode 100644 index 8228ecec..00000000 --- a/contracts/lending/deprecated/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/deprecated/OneWayLendingFactory.vy b/contracts/lending/deprecated/OneWayLendingFactory.vy deleted file mode 100644 index 78634fcb..00000000 --- a/contracts/lending/deprecated/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/deprecated/OneWayLendingFactoryL2.vy b/contracts/lending/deprecated/OneWayLendingFactoryL2.vy deleted file mode 100644 index d2204626..00000000 --- a/contracts/lending/deprecated/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/deprecated/TwoWayLendingFactory.vy b/contracts/lending/deprecated/TwoWayLendingFactory.vy deleted file mode 100644 index 50781d0f..00000000 --- a/contracts/lending/deprecated/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/deprecated/Vault.vy b/contracts/lending/deprecated/Vault.vy deleted file mode 100644 index a96403b5..00000000 --- a/contracts/lending/deprecated/Vault.vy +++ /dev/null @@ -1,692 +0,0 @@ -# @version 0.3.10 -""" -@title Vault -@notice ERC4626+ Vault for lending with crvUSD using LLAMMA algorithm -@author Curve.Fi -@license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved -""" -from vyper.interfaces import ERC20 as ERC20Spec -from vyper.interfaces import ERC20Detailed - - -implements: ERC20Spec -implements: ERC20Detailed - - -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 - -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 -MIN_ASSETS: constant(uint256) = 10000 - -borrowed_token: public(ERC20) -collateral_token: public(ERC20) - -price_oracle: public(PriceOracle) -amm: public(AMM) -controller: public(Controller) -factory: public(Factory) - -maxSupply: public(uint256) - - -# ERC20 publics - -decimals: public(constant(uint8)) = 18 -name: public(String[64]) -symbol: public(String[34]) - -NAME_PREFIX: constant(String[16]) = 'Curve Vault for ' -SYMBOL_PREFIX: constant(String[2]) = 'cv' - -allowance: public(HashMap[address, HashMap[address, uint256]]) -balanceOf: public(HashMap[address, uint256]) -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) - - -@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): - """ - @notice Initializer for vaults - @param amm_impl AMM implementation (blueprint) - @param controller_impl Controller implementation (blueprint) - @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 - """ - assert self.borrowed_token.address == empty(address) - - self.borrowed_token = borrowed_token - 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) - - # ERC20 set up - self.precision = borrowed_precision - borrowed_symbol: String[32] = borrowed_token.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 - self.symbol = concat(SYMBOL_PREFIX, borrowed_symbol) - - 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 - self.maxSupply = max_supply - log SetMaxSupply(max_supply) - - -@external -@view -@nonreentrant('lock') -def borrow_apr() -> uint256: - """ - @notice Borrow APR (annualized and 1e18-based) - """ - return self.amm.rate() * (365 * 86400) - - -@external -@view -@nonreentrant('lock') -def lend_apr() -> uint256: - """ - @notice Lending APR (annualized and 1e18-based) - """ - debt: uint256 = self.controller.total_debt() - if debt == 0: - return 0 - else: - return self.amm.rate() * (365 * 86400) * debt / self._total_assets() - - -@external -@view -def asset() -> ERC20: - """ - @notice Asset which is the same as borrowed_token - """ - return self.borrowed_token - - -@internal -@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() - - -@external -@view -@nonreentrant('lock') -def totalAssets() -> uint256: - """ - @notice Total assets which can be lent out or be in reserve - """ - return self._total_assets() - - -@internal -@view -def _convert_to_shares(assets: uint256, is_floor: bool = True, - _total_assets: uint256 = max_value(uint256)) -> uint256: - total_assets: uint256 = _total_assets - if total_assets == max_value(uint256): - total_assets = self._total_assets() - precision: uint256 = self.precision - numerator: uint256 = (self.totalSupply + DEAD_SHARES) * assets * precision - denominator: uint256 = (total_assets * precision + 1) - if is_floor: - return numerator / denominator - else: - return (numerator + denominator - 1) / denominator - - -@internal -@view -def _convert_to_assets(shares: uint256, is_floor: bool = True, - _total_assets: uint256 = max_value(uint256)) -> uint256: - total_assets: uint256 = _total_assets - if total_assets == max_value(uint256): - total_assets = self._total_assets() - precision: uint256 = self.precision - numerator: uint256 = shares * (total_assets * precision + 1) - denominator: uint256 = (self.totalSupply + DEAD_SHARES) * precision - if is_floor: - return numerator / denominator - else: - return (numerator + denominator - 1) / denominator - - -@external -@view -@nonreentrant('lock') -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 - 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 - else: - pps = (numerator + denominator - 1) / denominator - assert pps > 0 - return pps - - -@external -@view -@nonreentrant('lock') -def convertToShares(assets: uint256) -> uint256: - """ - @notice Returns the amount of shares which the Vault would exchange for the given amount of shares provided - """ - return self._convert_to_shares(assets) - - -@external -@view -@nonreentrant('lock') -def convertToAssets(shares: uint256) -> uint256: - """ - @notice Returns the amount of assets that the Vault would exchange for the amount of shares provided - """ - return self._convert_to_assets(shares) - - -@external -@view -def maxDeposit(receiver: address) -> uint256: - """ - @notice Maximum amount of assets which a given user can deposit (inf) - """ - max_supply: uint256 = self.maxSupply - if max_supply == max_value(uint256): - return max_supply - else: - assets: uint256 = self._total_assets() - return max(max_supply, assets) - assets - - -@external -@view -@nonreentrant('lock') -def previewDeposit(assets: uint256) -> uint256: - """ - @notice Returns the amount of shares which can be obtained upon depositing assets - """ - return self._convert_to_shares(assets) - - -@external -@nonreentrant('lock') -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 - 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) - self._mint(receiver, to_mint) - controller.save_rate() - log Deposit(msg.sender, receiver, assets, to_mint) - return to_mint - - -@external -@view -def maxMint(receiver: address) -> uint256: - """ - @notice Return maximum amount of shares which a given user can mint (inf) - """ - max_supply: uint256 = self.maxSupply - if max_supply == max_value(uint256): - return max_supply - else: - assets: uint256 = self._total_assets() - return self._convert_to_shares(max(max_supply, assets) - assets) - - -@external -@view -@nonreentrant('lock') -def previewMint(shares: uint256) -> uint256: - """ - @notice Calculate the amount of assets which is needed to exactly mint the given amount of shares - """ - return self._convert_to_assets(shares, False) - - -@external -@nonreentrant('lock') -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 - 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) - self._mint(receiver, shares) - controller.save_rate() - log Deposit(msg.sender, receiver, assets, shares) - return assets - - -@external -@view -@nonreentrant('lock') -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)) - - -@external -@view -@nonreentrant('lock') -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) - return self._convert_to_shares(assets, False) - - -@external -@nonreentrant('lock') -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 - @param assets Amount of assets to withdraw - @param receiver Receiver of the assets (optional, sender if not specified) - @param owner Owner who's shares the caller takes. Only can take those if owner gave the approval to the sender. Optional - """ - total_assets: uint256 = self._total_assets() - assert total_assets - assets >= MIN_ASSETS or total_assets == assets, "Need more assets" - shares: uint256 = self._convert_to_shares(assets, False, total_assets) - if owner != msg.sender: - allowance: uint256 = self.allowance[owner][msg.sender] - if allowance != max_value(uint256): - self._approve(owner, msg.sender, allowance - shares) - - controller: Controller = 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) - return shares - - -@external -@view -@nonreentrant('lock') -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.balanceOf[owner]) - - -@external -@view -@nonreentrant('lock') -def previewRedeem(shares: uint256) -> uint256: - """ - @notice Calculate the amount of assets which can be obtained by redeeming the given amount of shares - """ - if self.totalSupply == 0: - assert shares == 0 - return 0 - - else: - assets_to_redeem: uint256 = self._convert_to_assets(shares) - assert assets_to_redeem <= self.borrowed_token.balanceOf(self.controller.address) - return assets_to_redeem - - -@external -@nonreentrant('lock') -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 - @param shares Amount of shares to burn - @param receiver Optional receiver of the assets - @param owner Optional owner of the shares. Can only redeem if owner gave approval to the sender - """ - if owner != msg.sender: - allowance: uint256 = self.allowance[owner][msg.sender] - if allowance != max_value(uint256): - self._approve(owner, msg.sender, allowance - shares) - - total_assets: uint256 = self._total_assets() - assets_to_redeem: uint256 = self._convert_to_assets(shares, True, total_assets) - if total_assets - assets_to_redeem < MIN_ASSETS: - if shares == self.totalSupply: - # This is the last withdrawal, so we can take everything - assets_to_redeem = total_assets - 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) - return assets_to_redeem - - -# ERC20 methods - -@internal -def _approve(_owner: address, _spender: address, _value: uint256): - self.allowance[_owner][_spender] = _value - - log Approval(_owner, _spender, _value) - - -@internal -def _burn(_from: address, _value: uint256): - self.balanceOf[_from] -= _value - self.totalSupply -= _value - - log Transfer(_from, empty(address), _value) - - -@internal -def _mint(_to: address, _value: uint256): - self.balanceOf[_to] += _value - self.totalSupply += _value - - log Transfer(empty(address), _to, _value) - - -@internal -def _transfer(_from: address, _to: address, _value: uint256): - assert _to not in [self, empty(address)] - - self.balanceOf[_from] -= _value - self.balanceOf[_to] += _value - - log Transfer(_from, _to, _value) - - -@external -def transferFrom(_from: address, _to: address, _value: uint256) -> bool: - """ - @notice Transfer tokens from one account to another. - @dev The caller needs to have an allowance from account `_from` greater than or - equal to the value being transferred. An allowance equal to the uint256 type's - maximum, is considered infinite and does not decrease. - @param _from The account which tokens will be spent from. - @param _to The account which tokens will be sent to. - @param _value The amount of tokens to be transferred. - """ - allowance: uint256 = self.allowance[_from][msg.sender] - if allowance != max_value(uint256): - self._approve(_from, msg.sender, allowance - _value) - - self._transfer(_from, _to, _value) - return True - - -@external -def transfer(_to: address, _value: uint256) -> bool: - """ - @notice Transfer tokens to `_to`. - @param _to The account to transfer tokens to. - @param _value The amount of tokens to transfer. - """ - self._transfer(msg.sender, _to, _value) - return True - - -@external -def approve(_spender: address, _value: uint256) -> bool: - """ - @notice Allow `_spender` to transfer up to `_value` amount of tokens from the caller's account. - @dev Non-zero to non-zero approvals are allowed, but should be used cautiously. The methods - increaseAllowance + decreaseAllowance are available to prevent any front-running that - may occur. - @param _spender The account permitted to spend up to `_value` amount of caller's funds. - @param _value The amount of tokens `_spender` is allowed to spend. - """ - self._approve(msg.sender, _spender, _value) - return True - - -@external -def increaseAllowance(_spender: address, _add_value: uint256) -> bool: - """ - @notice Increase the allowance granted to `_spender`. - @dev This function will never overflow, and instead will bound - allowance to MAX_UINT256. This has the potential to grant an - infinite approval. - @param _spender The account to increase the allowance of. - @param _add_value The amount to increase the allowance by. - """ - cached_allowance: uint256 = self.allowance[msg.sender][_spender] - allowance: uint256 = unsafe_add(cached_allowance, _add_value) - - # check for an overflow - if allowance < cached_allowance: - allowance = max_value(uint256) - - if allowance != cached_allowance: - self._approve(msg.sender, _spender, allowance) - - return True - - -@external -def decreaseAllowance(_spender: address, _sub_value: uint256) -> bool: - """ - @notice Decrease the allowance granted to `_spender`. - @dev This function will never underflow, and instead will bound - allowance to 0. - @param _spender The account to decrease the allowance of. - @param _sub_value The amount to decrease the allowance by. - """ - cached_allowance: uint256 = self.allowance[msg.sender][_spender] - allowance: uint256 = unsafe_sub(cached_allowance, _sub_value) - - # check for an underflow - if cached_allowance < allowance: - allowance = 0 - - if allowance != cached_allowance: - self._approve(msg.sender, _spender, allowance) - - return True - - -@external -@view -def admin() -> address: - return self.factory.admin() From e382cc2c56d92f1dd983e4c472135094fd5325ba Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 22 Aug 2025 17:29:23 +0200 Subject: [PATCH 135/413] feat: add logs filter --- contracts/lending/LLController.vy | 1 - tests/utils/__init__.py | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy index 61e84600..7cee2c27 100644 --- a/contracts/lending/LLController.vy +++ b/contracts/lending/LLController.vy @@ -82,7 +82,6 @@ exports: ( core.minted, core.redeemed, ) -# TODO reorder exports in a way that make sense VAULT: immutable(IVault) diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py index fc33ace4..7c39f71d 100644 --- a/tests/utils/__init__.py +++ b/tests/utils/__init__.py @@ -3,3 +3,7 @@ def mint_for_testing(token, to, amount): 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] From badd7d1dfe9f78d083dfa788187e683c0328d31d Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 22 Aug 2025 21:17:47 +0200 Subject: [PATCH 136/413] chore: remove approx util --- tests/amm/test_amount_for_price.py | 12 +++++----- tests/amm/test_deposit_withdraw.py | 6 ++--- tests/amm/test_exchange.py | 8 +++---- tests/amm/test_exchange_dy.py | 5 ++--- tests/amm/test_price_oracles.py | 12 +++++----- tests/amm/test_xdown_yup_invariants.py | 22 +++++++++---------- tests/amm/test_xdown_yup_invariants_dy.py | 9 ++++---- tests/controller/conftest.py | 17 +++++++------- .../test_health_calculator_stateful.py | 6 ++--- tests/lending/test_max_borrowable.py | 6 ++--- tests/lending/test_monetary_policy.py | 4 ++-- tests/lending/test_vault.py | 5 ++--- tests/lm_callback/test_as_gauge.py | 6 ++--- tests/lm_callback/test_lm_callback.py | 22 +++++++++---------- tests/lm_callback/test_st_as_gauge.py | 10 ++++----- tests/lm_callback/test_st_lm_callback.py | 14 ++++++------ tests/stableborrow/stabilize/conftest.py | 3 +-- .../unitary/test_price_aggregation.py | 10 ++++----- .../test_price_aggregation_with_chainlink.py | 6 ++--- tests/stableborrow/test_create_repay.py | 5 ++--- tests/stableborrow/test_leverage.py | 14 ++++++------ tests/stableborrow/test_liquidate.py | 5 ++--- tests/stableborrow/test_lm_callback.py | 3 +-- tests/swap/test_price.py | 10 ++++----- 24 files changed, 105 insertions(+), 115 deletions(-) diff --git a/tests/amm/test_amount_for_price.py b/tests/amm/test_amount_for_price.py index d307f69c..64a04919 100644 --- a/tests/amm/test_amount_for_price.py +++ b/tests/amm/test_amount_for_price.py @@ -1,5 +1,5 @@ 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 @@ -72,8 +72,8 @@ def test_amount_for_price(price_oracle, amm, accounts, collateral_token, borrowe 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) + assert p_max == pytest.approx(amm.p_current_up(n2), rel=1e-8) + assert p_min == pytest.approx(amm.p_current_down(n1), rel=1e-8) if abs(n_final - n0) < 50 - 1: A = amm.A() @@ -87,14 +87,14 @@ def test_amount_for_price(price_oracle, amm, accounts, collateral_token, borrowe return if p_final > p_min * (1 + prec) and p_final < p_max * (1 - prec): - assert approx(p, p_final, prec) + assert p == pytest.approx(p_final, rel=prec) elif p_final >= p_max * (1 - prec): - if not approx(p, p_max, prec): + if not p == pytest.approx(p_max, rel=prec): assert n_final > n2 elif p_final <= p_min * (1 + prec): - if not approx(p, p_min, prec): + if not p == pytest.approx(p_min, rel=prec): assert n_final < n1 diff --git a/tests/amm/test_deposit_withdraw.py b/tests/amm/test_deposit_withdraw.py index 3a2fed25..07695562 100644 --- a/tests/amm/test_deposit_withdraw.py +++ b/tests/amm/test_deposit_withdraw.py @@ -1,5 +1,5 @@ import boa -from ..conftest import approx +import pytest from hypothesis import given from hypothesis import strategies as st from ..utils import mint_for_testing @@ -36,7 +36,7 @@ def test_deposit_withdraw(amm, amounts, accounts, ns, dns, fracs, collateral_tok 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 else: @@ -47,7 +47,7 @@ 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) diff --git a/tests/amm/test_exchange.py b/tests/amm/test_exchange.py index dc92ad02..5409e707 100644 --- a/tests/amm/test_exchange.py +++ b/tests/amm/test_exchange.py @@ -1,5 +1,5 @@ import boa -from ..conftest import approx +import pytest from hypothesis import given from hypothesis import strategies as st from ..utils import mint_for_testing @@ -27,7 +27,7 @@ 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 dx, dy = amm.get_dxdy(1, 0, 10**16) # No liquidity @@ -69,7 +69,7 @@ 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) + 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) @@ -88,7 +88,7 @@ def test_exchange_down_up(amm, amounts, accounts, ns, dns, amount, 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 diff --git a/tests/amm/test_exchange_dy.py b/tests/amm/test_exchange_dy.py index bf81a70b..0f8df795 100644 --- a/tests/amm/test_exchange_dy.py +++ b/tests/amm/test_exchange_dy.py @@ -1,6 +1,5 @@ import boa import pytest -from ..conftest import approx from hypothesis import given from hypothesis import strategies as st from ..utils import mint_for_testing @@ -42,7 +41,7 @@ def test_dydx_limits(amm, amounts, accounts, ns, dns, collateral_token, admin, b 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 @@ -152,7 +151,7 @@ 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) + assert dx == pytest.approx(dx2, rel=1e-6) mint_for_testing(borrowed_token, u, dx2) with boa.env.prank(u): with boa.reverts("Slippage"): diff --git a/tests/amm/test_price_oracles.py b/tests/amm/test_price_oracles.py index 427ba3d0..74dd0190 100644 --- a/tests/amm/test_price_oracles.py +++ b/tests/amm/test_price_oracles.py @@ -1,7 +1,7 @@ import boa import pytest import eth_utils -from ..conftest import PRICE, approx +from ..conftest import PRICE from tests.utils.deployers import EMA_PRICE_ORACLE_DEPLOYER @@ -26,14 +26,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): @@ -47,8 +47,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_xdown_yup_invariants.py b/tests/amm/test_xdown_yup_invariants.py index 01881910..12839fea 100644 --- a/tests/amm/test_xdown_yup_invariants.py +++ b/tests/amm/test_xdown_yup_invariants.py @@ -1,7 +1,7 @@ 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: @@ -70,8 +70,8 @@ def test_immediate(amm, price_oracle, collateral_token, borrowed_token, accounts 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): @@ -111,9 +111,9 @@ def test_immediate_above_p0(amm, price_oracle, collateral_token, borrowed_token, 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) def test_immediate_in_band(amm, price_oracle, collateral_token, borrowed_token, accounts, admin): @@ -152,9 +152,9 @@ def test_immediate_in_band(amm, price_oracle, collateral_token, borrowed_token, 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( @@ -218,8 +218,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/amm/test_xdown_yup_invariants_dy.py b/tests/amm/test_xdown_yup_invariants_dy.py index 5c34d2e8..fa02e63c 100644 --- a/tests/amm/test_xdown_yup_invariants_dy.py +++ b/tests/amm/test_xdown_yup_invariants_dy.py @@ -2,7 +2,6 @@ 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: @@ -87,8 +86,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( @@ -158,8 +157,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/controller/conftest.py b/tests/controller/conftest.py index 8fbb62d0..1719009c 100644 --- a/tests/controller/conftest.py +++ b/tests/controller/conftest.py @@ -1,19 +1,18 @@ from pytest import fixture -from tests.utils.deploy import Protocol from tests.utils.deployers import ERC20_MOCK_DEPLOYER -@fixture(scope="module") -def proto(): - return Protocol() - -@fixture(scope="module") -def admin(proto: Protocol): - return proto.admin +# Common fixtures (proto, admin) are now in tests/conftest.py -@fixture(scope="module", params=range(2, 19)) +@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/lending/test_health_calculator_stateful.py b/tests/lending/test_health_calculator_stateful.py index 2632b993..be241ffc 100644 --- a/tests/lending/test_health_calculator_stateful.py +++ b/tests/lending/test_health_calculator_stateful.py @@ -6,7 +6,7 @@ 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 +import pytest DEAD_SHARES = 1000 @@ -51,8 +51,8 @@ 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 diff --git a/tests/lending/test_max_borrowable.py b/tests/lending/test_max_borrowable.py index 9582e7e6..5bc3057e 100644 --- a/tests/lending/test_max_borrowable.py +++ b/tests/lending/test_max_borrowable.py @@ -1,7 +1,7 @@ import boa +import pytest from hypothesis import given, settings from hypothesis import strategies as st -from ..conftest import approx DEAD_SHARES = 1000 @@ -39,8 +39,8 @@ def test_max_borrowable(borrowed_token, collateral_token, market_amm, filled_con 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) * ( + assert min_collateral == pytest.approx( + collateral_amount, rel=1e-6 + (n**2 + n * DEAD_SHARES) * ( 1 / min(min_collateral, collateral_amount) + 1 / max_borrowable)) diff --git a/tests/lending/test_monetary_policy.py b/tests/lending/test_monetary_policy.py index 35a4ccc4..6f84d5ef 100644 --- a/tests/lending/test_monetary_policy.py +++ b/tests/lending/test_monetary_policy.py @@ -1,7 +1,7 @@ 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) @@ -27,4 +27,4 @@ def test_monetary_policy(filled_controller, collateral_token, borrowed_token, ma 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) + assert rate == pytest.approx(theoretical_rate, rel=1e-4) diff --git a/tests/lending/test_vault.py b/tests/lending/test_vault.py index 85b950ab..03102c2d 100644 --- a/tests/lending/test_vault.py +++ b/tests/lending/test_vault.py @@ -3,7 +3,6 @@ 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 # TODO get this from contract directly @@ -75,7 +74,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 @@ -91,7 +90,7 @@ def inv_pps(self): 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 diff --git a/tests/lm_callback/test_as_gauge.py b/tests/lm_callback/test_as_gauge.py index 261c881e..b75bf40f 100644 --- a/tests/lm_callback/test_as_gauge.py +++ b/tests/lm_callback/test_as_gauge.py @@ -1,6 +1,6 @@ import boa from random import random, randrange -from ..conftest import approx +import pytest MAX_UINT256 = 2 ** 256 - 1 YEAR = 365 * 86400 @@ -82,7 +82,7 @@ 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 @@ -218,7 +218,7 @@ 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) diff --git a/tests/lm_callback/test_lm_callback.py b/tests/lm_callback/test_lm_callback.py index 81bd5d3b..ba58b4fc 100644 --- a/tests/lm_callback/test_lm_callback.py +++ b/tests/lm_callback/test_lm_callback.py @@ -1,6 +1,6 @@ import boa +import pytest from random import random, randrange, choice -from ..conftest import approx MAX_UINT256 = 2 ** 256 - 1 YEAR = 365 * 86400 @@ -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 @@ -128,7 +128,7 @@ 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) print("Bob repays:", repay_amount_bob) @@ -140,7 +140,7 @@ def update_integral(): 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) @@ -154,7 +154,7 @@ def update_integral(): else: 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: @@ -169,7 +169,7 @@ 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) print("Alice repays:", repay_amount_alice) @@ -181,7 +181,7 @@ def update_integral(): 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) + 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) @@ -195,7 +195,7 @@ def update_integral(): else: 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) @@ -242,7 +242,7 @@ def update_integral(): total_collateral_from_lm_cb = lm_callback.total_collateral() 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 +253,7 @@ 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) @@ -335,7 +335,7 @@ def test_full_repay_underwater( 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) + 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_st_as_gauge.py b/tests/lm_callback/test_st_as_gauge.py index 24887443..eb7cb344 100644 --- a/tests/lm_callback/test_st_as_gauge.py +++ b/tests/lm_callback/test_st_as_gauge.py @@ -3,7 +3,7 @@ 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 +import pytest class StateMachine(RuleBasedStateMachine): @@ -63,7 +63,7 @@ def deposit(self, uid, 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) + 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): @@ -92,7 +92,7 @@ def withdraw(self, uid, value): 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) + 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 +114,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): @@ -159,7 +159,7 @@ def teardown(self): 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(): diff --git a/tests/lm_callback/test_st_lm_callback.py b/tests/lm_callback/test_st_lm_callback.py index 426efd89..40342ed2 100644 --- a/tests/lm_callback/test_st_lm_callback.py +++ b/tests/lm_callback/test_st_lm_callback.py @@ -2,7 +2,7 @@ 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 +import pytest class StateMachine(RuleBasedStateMachine): @@ -84,7 +84,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): @@ -128,7 +128,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): @@ -180,7 +180,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 +204,11 @@ 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): """ @@ -230,7 +230,7 @@ def teardown(self): 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(): diff --git a/tests/stableborrow/stabilize/conftest.py b/tests/stableborrow/stabilize/conftest.py index 66ab1705..655823c6 100644 --- a/tests/stableborrow/stabilize/conftest.py +++ b/tests/stableborrow/stabilize/conftest.py @@ -1,6 +1,5 @@ import boa import pytest -from ...conftest import approx from tests.utils.deployers import ( ERC20_MOCK_DEPLOYER, STABLESWAP_DEPLOYER, @@ -349,7 +348,7 @@ def provide_token_to_peg_keepers_no_sleep(initial_amounts, swaps, peg_keepers, r rtoken_mul = 10 ** (18 - rtoken.decimals()) 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") diff --git a/tests/stableborrow/stabilize/unitary/test_price_aggregation.py b/tests/stableborrow/stabilize/unitary/test_price_aggregation.py index 1bcfa2cf..c1bae0c6 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() @@ -20,10 +20,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): @@ -44,7 +44,7 @@ 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 boa.deal(stablecoin_a, admin, amount) 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 f86498a1..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,10 +57,9 @@ 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 diff --git a/tests/stableborrow/test_create_repay.py b/tests/stableborrow/test_create_repay.py index 0af6c8e9..05baded6 100644 --- a/tests/stableborrow/test_create_repay.py +++ b/tests/stableborrow/test_create_repay.py @@ -2,7 +2,6 @@ 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): @@ -41,13 +40,13 @@ def test_create_loan(controller_factory, stablecoin, collateral_token, market_co 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()) + 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( diff --git a/tests/stableborrow/test_leverage.py b/tests/stableborrow/test_leverage.py index 7e58b92a..bdb2a326 100644 --- a/tests/stableborrow/test_leverage.py +++ b/tests/stableborrow/test_leverage.py @@ -1,6 +1,6 @@ import boa +import pytest from eth_abi import encode -from ..conftest import approx from hypothesis import given, settings from hypothesis import strategies as st @@ -65,11 +65,11 @@ def test_leverage_property(collateral_token, stablecoin, market_controller, mark 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) @@ -81,11 +81,11 @@ def test_leverage_property(collateral_token, stablecoin, market_controller, mark 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: diff --git a/tests/stableborrow/test_liquidate.py b/tests/stableborrow/test_liquidate.py index ca4988ce..cff936be 100644 --- a/tests/stableborrow/test_liquidate.py +++ b/tests/stableborrow/test_liquidate.py @@ -3,7 +3,6 @@ from boa import BoaError from hypothesis import given, settings from hypothesis import strategies as st -from ..conftest import approx N = 5 @@ -55,7 +54,7 @@ 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): @@ -185,6 +184,6 @@ def test_tokens_to_liquidate(accounts, admin, controller_for_liquidation, market balance = stablecoin.balanceOf(fee_receiver) if frac < 10**18: - assert approx(balance, initial_balance - tokens_to_liquidate, 1e5, abs_precision=1e5) + assert balance == pytest.approx(initial_balance - 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 9d967c9a..0e86a3a0 100644 --- a/tests/stableborrow/test_lm_callback.py +++ b/tests/stableborrow/test_lm_callback.py @@ -1,7 +1,6 @@ import boa import pytest from collections import defaultdict -from ..conftest import approx from tests.utils.deployers import DUMMY_LM_CALLBACK_DEPLOYER @@ -33,4 +32,4 @@ 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/swap/test_price.py b/tests/swap/test_price.py index 072cc444..1b84eb77 100644 --- a/tests/swap/test_price.py +++ b/tests/swap/test_price.py @@ -1,8 +1,8 @@ import boa +import pytest from hypothesis import given, settings from hypothesis import strategies as st from math import exp -from ..conftest import approx @given( @@ -20,7 +20,7 @@ def test_price(swap_w_d, redeemable_coin, volatile_coin, accounts, amount, ix): 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) + assert p1 == pytest.approx(p2, rel=0.04e-2 * 1.2) @given( @@ -39,9 +39,9 @@ def test_ema(swap_w_d, redeemable_coin, volatile_coin, accounts, amount, ix, 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) + assert swap_w_d.last_price() == pytest.approx(p, rel=1e-5) + assert swap_w_d.price_oracle() == pytest.approx(10**18, rel=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) + assert swap_w_d.price_oracle() == pytest.approx(p1, rel=1e-5) From 874beca1c704c8d11750febe2b70749c3e119b10 Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 22 Aug 2025 21:39:48 +0200 Subject: [PATCH 137/413] chore: remove factory_partial fixture usage --- tests/lending/test_oracle_attack.py | 11 ++++++----- tests/utils/deploy.py | 6 ++---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/lending/test_oracle_attack.py b/tests/lending/test_oracle_attack.py index 90e0bbc4..a8323c97 100644 --- a/tests/lending/test_oracle_attack.py +++ b/tests/lending/test_oracle_attack.py @@ -12,7 +12,7 @@ from hypothesis import given, settings from hypothesis import strategies as st -from tests.utils.deployers import OLD_AMM_DEPLOYER +from tests.utils.deployers import LENDING_FACTORY_DEPLOYER, OLD_AMM_DEPLOYER MAX = 2**256 - 1 @@ -40,9 +40,9 @@ def hacker(accounts): @pytest.fixture(scope="module") -def factory_new(factory_partial, amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, admin): +def factory_new(amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, admin): with boa.env.prank(admin): - return factory_partial.deploy(amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, admin, admin) + return LENDING_FACTORY_DEPLOYER.deploy(amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, admin, admin) @pytest.fixture(scope="module") @@ -51,10 +51,11 @@ def amm_old_interface(): @pytest.fixture(scope="module") -def factory_old(factory_partial, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, amm_old_interface, admin): +def factory_old(controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, amm_old_interface, admin): + # TODO is this really the old factory? I don't think so with boa.env.prank(admin): amm_impl = amm_old_interface.deploy_as_blueprint() - return factory_partial.deploy(amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, admin, admin) + return LENDING_FACTORY_DEPLOYER.deploy(amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, admin, admin) @pytest.fixture(scope='module') diff --git a/tests/utils/deploy.py b/tests/utils/deploy.py index bfeb5090..bd0aafdc 100644 --- a/tests/utils/deploy.py +++ b/tests/utils/deploy.py @@ -54,8 +54,6 @@ class Protocol: 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 @@ -145,7 +143,7 @@ def create_mint_market( loan_discount: int, liquidation_discount: int, debt_ceiling: int - ) -> Dict[str, Any]: + ) -> Dict[str, VyperContract]: """ Create a new mint market in the Controller Factory. @@ -196,7 +194,7 @@ def create_lending_market( name: str, min_borrow_rate: int, max_borrow_rate: int - ) -> Dict[str, Any]: + ) -> Dict[str, VyperContract]: """ Create a new lending market in the Lending Factory. From 60fe8bc78f878496d4f1d8e2f905d0301454f410 Mon Sep 17 00:00:00 2001 From: macket Date: Sat, 23 Aug 2025 10:10:15 +0400 Subject: [PATCH 138/413] fix: admin_fees for LLController --- contracts/Controller.vy | 23 +++++++++++++++++------ contracts/lending/LLController.vy | 7 ++++++- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index da974502..a923adc5 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -1313,7 +1313,7 @@ def users_to_liquidate( @view @external -@reentrant +@reentrant def amm_price() -> uint256: """ @notice Current price from the AMM @@ -1405,10 +1405,9 @@ def admin_fees() -> uint256: """ @notice Calculate the amount of fees obtained from the interest """ - processed: uint256 = self.processed - return unsafe_sub( - max(self._get_total_debt() + self.repaid, processed), processed - ) + # In mint controller, 100% (WAD) fees are + # collected as admin fees. + return self._admin_fees(WAD) @external @@ -1421,6 +1420,18 @@ def collect_fees() -> uint256: return self._collect_fees(WAD) +@internal +@view +def _admin_fees(admin_fee: uint256) -> uint256: + """ + @notice Calculate the amount of fees obtained from the interest + """ + processed: uint256 = self.processed + return unsafe_sub( + max(self._get_total_debt() + self.repaid, processed), processed + ) * admin_fee // WAD + + @internal def _collect_fees(admin_fee: uint256) -> uint256: if admin_fee == 0: @@ -1440,7 +1451,7 @@ def _collect_fees(admin_fee: uint256) -> uint256: # Difference between to_be_repaid and processed amount is exactly due to interest charged if to_be_repaid > processed: self.processed = to_be_repaid - fees: uint256 = (unsafe_sub(to_be_repaid, processed) * admin_fee // WAD) + fees: uint256 = unsafe_sub(to_be_repaid, processed) * admin_fee // WAD self.transfer(BORROWED_TOKEN, _to, fees) log IController.CollectFees(amount=fees, new_supply=loan.initial_debt) return fees diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy index 7cee2c27..3c243864 100644 --- a/contracts/lending/LLController.vy +++ b/contracts/lending/LLController.vy @@ -61,7 +61,6 @@ exports: ( core.n_loans, core.tokens_to_liquidate, core.total_debt, - core.admin_fees, core.factory, core.processed, core.repaid, @@ -227,6 +226,12 @@ def borrow_more( self.lent += _debt +@external +@view +def admin_fees() -> uint256: + return core._admin_fees(self.admin_fee) + + @external def collect_fees() -> uint256: fees: uint256 = core._collect_fees(self.admin_fee) From 64fcae68788a93f172a77ebcd9cbe2a5d276984a Mon Sep 17 00:00:00 2001 From: macket Date: Sat, 23 Aug 2025 16:31:48 +0400 Subject: [PATCH 139/413] chore: clean redundant imports --- contracts/lending/LLController.vy | 4 ---- 1 file changed, 4 deletions(-) diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy index 3c243864..e977ca50 100644 --- a/contracts/lending/LLController.vy +++ b/contracts/lending/LLController.vy @@ -11,9 +11,7 @@ """ from ethereum.ercs import IERC20 -from ethereum.ercs import IERC20Detailed from contracts.interfaces import IAMM -from contracts.interfaces import ILMGauge from contracts.interfaces import IMonetaryPolicy from contracts.interfaces import IVault @@ -25,8 +23,6 @@ from contracts.interfaces import ILlamalendController implements: ILlamalendController -from snekmate.utils import math - from contracts import Controller as core initializes: core From 16f3e9061008547ad7d1eca84ff735097ac348d2 Mon Sep 17 00:00:00 2001 From: macket Date: Sat, 23 Aug 2025 16:55:59 +0400 Subject: [PATCH 140/413] fix: max_borrowable for LLController --- contracts/Controller.vy | 7 ++++--- contracts/ControllerView.vy | 34 +++++-------------------------- contracts/lending/LLController.vy | 26 ++++++++++++++++++++++- 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index a923adc5..e06d7b52 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -535,9 +535,10 @@ def max_borrowable( @param user User to calculate the value for (only necessary for nonzero extra_health) @return Maximum amount of stablecoin to borrow """ - return staticcall self._view.max_borrowable( - collateral, N, current_debt, user - ) + # Cannot borrow beyond the amount of coins Controller has + cap: uint256 = staticcall BORROWED_TOKEN.balanceOf(self) + current_debt + + return staticcall self._view.max_borrowable(collateral, N, cap, user) @external diff --git a/contracts/ControllerView.vy b/contracts/ControllerView.vy index deac1466..c074bb4f 100644 --- a/contracts/ControllerView.vy +++ b/contracts/ControllerView.vy @@ -294,16 +294,17 @@ def _max_p_base() -> uint256: return p_base -@internal +@external @view -def _max_borrowable( +def max_borrowable( collateral: uint256, N: uint256, cap: uint256, - current_debt: uint256, user: address, ) -> uint256: - + """ + @notice Natspec for this function is available in its controller contract + """ # 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. @@ -336,31 +337,6 @@ def _max_borrowable( 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, - current_debt, - user, - ) - - @external @view def min_collateral( diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy index e977ca50..2ce61635 100644 --- a/contracts/lending/LLController.vy +++ b/contracts/lending/LLController.vy @@ -72,7 +72,6 @@ exports: ( core.user_state, core.users_to_liquidate, core.min_collateral, - core.max_borrowable, # For compatibility with mint markets ABI core.minted, core.redeemed, @@ -176,8 +175,33 @@ def borrowed_balance() -> uint256: return self._borrowed_balance() +@external +@view +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 + """ + # Cannot borrow beyond the amount of borrow_cap or borrowed_balance + total_debt: uint256 = core._get_total_debt() + cap: uint256 = unsafe_sub(max(core.borrow_cap, total_debt), total_debt) # don't exceed borrow_cap + cap = min(self._borrowed_balance() + current_debt, cap) # don't exceed borrowed_balance + + return staticcall core._view.max_borrowable(collateral, N, cap, user) + + # TODO delete deprecated and legacy files + @external def create_loan( collateral: uint256, From e263e3352c4bf17ae555da90e7737a233a2a64a2 Mon Sep 17 00:00:00 2001 From: Alberto Date: Sat, 23 Aug 2025 18:34:41 +0200 Subject: [PATCH 141/413] test: rely more on deployers --- tests/amm/conftest.py | 6 ++--- tests/amm/test_exchange_dy.py | 5 ++-- tests/amm/test_flip_dy.py | 5 ++-- tests/amm/test_oracle_change_noloss.py | 5 ++-- tests/amm/test_st_exchange.py | 31 ++++++++++++----------- tests/amm/test_st_exchange_dy.py | 18 ++++++------- tests/amm/test_xdown_yup_invariants_dy.py | 5 ++-- tests/flashloan/conftest.py | 18 +++---------- tests/lending/test_oracle_attack.py | 31 ++++++++++++----------- tests/lending/test_secondary_mpolicy.py | 5 ++-- tests/lm_callback/conftest.py | 29 ++++++++------------- tests/stableborrow/conftest.py | 26 ++++++------------- tests/stableborrow/stabilize/conftest.py | 16 +++++++----- tests/stableborrow/test_bigfuzz.py | 14 +++++----- tests/stableborrow/test_factory.py | 8 +++--- 15 files changed, 102 insertions(+), 120 deletions(-) diff --git a/tests/amm/conftest.py b/tests/amm/conftest.py index eec74352..19f1fd72 100644 --- a/tests/amm/conftest.py +++ b/tests/amm/conftest.py @@ -1,14 +1,14 @@ import boa import pytest from math import sqrt, log -from tests.utils.deployers import AMM_DEPLOYER +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) +def borrowed_token(): + return ERC20_MOCK_DEPLOYER.deploy(6) @pytest.fixture(scope="session") diff --git a/tests/amm/test_exchange_dy.py b/tests/amm/test_exchange_dy.py index 0f8df795..98929ed9 100644 --- a/tests/amm/test_exchange_dy.py +++ b/tests/amm/test_exchange_dy.py @@ -3,11 +3,12 @@ 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(): + return ERC20_MOCK_DEPLOYER.deploy(18) @pytest.fixture(scope="module") diff --git a/tests/amm/test_flip_dy.py b/tests/amm/test_flip_dy.py index 603a976f..00d025ca 100644 --- a/tests/amm/test_flip_dy.py +++ b/tests/amm/test_flip_dy.py @@ -2,6 +2,7 @@ import pytest from pytest import mark # noqa from ..utils import mint_for_testing +from tests.utils.deployers import ERC20_MOCK_DEPLOYER # 1. deposit below (N > 0 in 5 bands) # 2. change price_oracle in a cycle downwards (by 15% just in case?) @@ -13,8 +14,8 @@ @pytest.fixture(scope="module") -def borrowed_token(get_borrowed_token): - return get_borrowed_token(18) +def borrowed_token(): + return ERC20_MOCK_DEPLOYER.deploy(18) @pytest.fixture(scope="module") diff --git a/tests/amm/test_oracle_change_noloss.py b/tests/amm/test_oracle_change_noloss.py index 311c2cb8..ccb94d36 100644 --- a/tests/amm/test_oracle_change_noloss.py +++ b/tests/amm/test_oracle_change_noloss.py @@ -5,11 +5,12 @@ from hypothesis import given, settings, example 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(): + return ERC20_MOCK_DEPLOYER.deploy(18) @pytest.fixture(scope="module") diff --git a/tests/amm/test_st_exchange.py b/tests/amm/test_st_exchange.py index 06028b7e..36cbae2b 100644 --- a/tests/amm/test_st_exchange.py +++ b/tests/amm/test_st_exchange.py @@ -6,6 +6,7 @@ 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): @@ -92,15 +93,15 @@ 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, +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(): @@ -108,14 +109,14 @@ 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(): @@ -133,14 +134,14 @@ 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(): @@ -152,14 +153,14 @@ def test_raise_rounding(admin, accounts, get_amm, get_collateral_token, get_borr 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(): @@ -177,14 +178,14 @@ 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(): diff --git a/tests/amm/test_st_exchange_dy.py b/tests/amm/test_st_exchange_dy.py index 928e5963..d319675e 100644 --- a/tests/amm/test_st_exchange_dy.py +++ b/tests/amm/test_st_exchange_dy.py @@ -6,6 +6,7 @@ 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): @@ -97,15 +98,14 @@ 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]) 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,15 +113,15 @@ 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): +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(): @@ -140,15 +140,15 @@ 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): +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(): diff --git a/tests/amm/test_xdown_yup_invariants_dy.py b/tests/amm/test_xdown_yup_invariants_dy.py index fa02e63c..96b184cf 100644 --- a/tests/amm/test_xdown_yup_invariants_dy.py +++ b/tests/amm/test_xdown_yup_invariants_dy.py @@ -3,6 +3,7 @@ import boa import pytest from ..utils import mint_for_testing +from tests.utils.deployers import ERC20_MOCK_DEPLOYER """ Test that get_x_down and get_y_up don't change: * if we do trades at constant p_o (immediate trades) @@ -11,8 +12,8 @@ @pytest.fixture(scope="module") -def borrowed_token(get_borrowed_token): - return get_borrowed_token(18) +def borrowed_token(): + return ERC20_MOCK_DEPLOYER.deploy(18) @pytest.fixture(scope="module") diff --git a/tests/flashloan/conftest.py b/tests/flashloan/conftest.py index 27ad1c52..09a161fe 100644 --- a/tests/flashloan/conftest.py +++ b/tests/flashloan/conftest.py @@ -41,25 +41,15 @@ def controller_prefactory(controller_factory_impl, stablecoin, weth, admin, acco @pytest.fixture(scope="session") -def controller_interface(): - return FLASH_LENDER_DEPLOYER - - -@pytest.fixture(scope="session") -def controller_impl(controller_interface, admin): +def controller_impl(admin): with boa.env.prank(admin): - return controller_interface.deploy_as_blueprint() - - -@pytest.fixture(scope="session") -def amm_interface(): - return AMM_DEPLOYER + return FLASH_LENDER_DEPLOYER.deploy_as_blueprint() @pytest.fixture(scope="session") -def amm_impl(amm_interface, admin): +def amm_impl(admin): with boa.env.prank(admin): - return amm_interface.deploy_as_blueprint() + return AMM_DEPLOYER.deploy_as_blueprint() @pytest.fixture(scope="session") diff --git a/tests/lending/test_oracle_attack.py b/tests/lending/test_oracle_attack.py index a8323c97..bca1fdb7 100644 --- a/tests/lending/test_oracle_attack.py +++ b/tests/lending/test_oracle_attack.py @@ -12,16 +12,17 @@ from hypothesis import given, settings from hypothesis import strategies as st -from tests.utils.deployers import LENDING_FACTORY_DEPLOYER, OLD_AMM_DEPLOYER +from tests.utils.deployers import LENDING_FACTORY_DEPLOYER, OLD_AMM_DEPLOYER, AMM_DEPLOYER, LL_CONTROLLER_DEPLOYER, VAULT_DEPLOYER, ERC20_MOCK_DEPLOYER MAX = 2**256 - 1 @pytest.fixture(scope='module') -def collateral_token(get_collateral_token): +def collateral_token(admin): decimals = 18 - return get_collateral_token(decimals) + with boa.env.prank(admin): + return ERC20_MOCK_DEPLOYER.deploy(decimals) @pytest.fixture(scope='module') @@ -59,11 +60,11 @@ def factory_old(controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, am @pytest.fixture(scope='module') -def vault_new(factory_new, vault_interface, borrowed_token, collateral_token, price_oracle, admin): +def vault_new(factory_new, borrowed_token, collateral_token, price_oracle, admin): with boa.env.prank(admin): price_oracle.set_price(int(1e18)) - vault = vault_interface.at( + vault = VAULT_DEPLOYER.at( factory_new.create( borrowed_token.address, collateral_token.address, 100, int(0.002 * 1e18), int(0.09 * 1e18), int(0.06 * 1e18), @@ -77,11 +78,11 @@ def vault_new(factory_new, vault_interface, borrowed_token, collateral_token, pr @pytest.fixture(scope='module') -def vault_old(factory_old, vault_interface, borrowed_token, collateral_token, price_oracle, admin): +def vault_old(factory_old, borrowed_token, collateral_token, price_oracle, admin): with boa.env.prank(admin): price_oracle.set_price(int(1e18)) - vault = vault_interface.at( + vault = VAULT_DEPLOYER.at( factory_old.create( borrowed_token.address, collateral_token.address, 100, int(0.006 * 1e18), int(0.09 * 1e18), int(0.06 * 1e18), @@ -95,8 +96,8 @@ def vault_old(factory_old, vault_interface, borrowed_token, collateral_token, pr @pytest.fixture(scope='module') -def controller_new(vault_new, controller_interface, admin): - controller = controller_interface.at(vault_new.controller()) +def controller_new(vault_new, admin): + controller = LL_CONTROLLER_DEPLOYER.at(vault_new.controller()) with boa.env.prank(admin): controller.set_borrow_cap(2 ** 256 - 1) @@ -104,13 +105,13 @@ def controller_new(vault_new, controller_interface, admin): @pytest.fixture(scope='module') -def amm_new(vault_new, amm_interface): - return amm_interface.at(vault_new.amm()) +def amm_new(vault_new): + return AMM_DEPLOYER.at(vault_new.amm()) @pytest.fixture(scope='module') -def controller_old(vault_old, controller_interface, admin): - controller = controller_interface.at(vault_old.controller()) +def controller_old(vault_old, admin): + controller = LL_CONTROLLER_DEPLOYER.at(vault_old.controller()) with boa.env.prank(admin): controller.set_borrow_cap(2 ** 256 - 1) @@ -118,8 +119,8 @@ def controller_old(vault_old, controller_interface, admin): @pytest.fixture(scope='module') -def amm_old(vault_old, amm_old_interface): - return amm_old_interface.at(vault_old.amm()) +def amm_old(vault_old): + return OLD_AMM_DEPLOYER.at(vault_old.amm()) def template_vuln_test(vault, controller, amm, admin, borrowed_token, price_oracle, collateral_token, victim, hacker, diff --git a/tests/lending/test_secondary_mpolicy.py b/tests/lending/test_secondary_mpolicy.py index a66fb174..350f8a30 100644 --- a/tests/lending/test_secondary_mpolicy.py +++ b/tests/lending/test_secondary_mpolicy.py @@ -4,6 +4,7 @@ from hypothesis import settings from hypothesis import strategies as st from tests.utils.deployers import ( + ERC20_MOCK_DEPLOYER, MOCK_FACTORY_DEPLOYER, MOCK_MARKET_DEPLOYER, MOCK_RATE_SETTER_DEPLOYER, @@ -35,8 +36,8 @@ def amm(): @pytest.fixture(scope="module") -def borrowed_token(get_borrowed_token): - return get_borrowed_token(18) +def borrowed_token(): + return ERC20_MOCK_DEPLOYER.deploy(18) @pytest.fixture(scope="module") diff --git a/tests/lm_callback/conftest.py b/tests/lm_callback/conftest.py index 259a394d..67e126f7 100644 --- a/tests/lm_callback/conftest.py +++ b/tests/lm_callback/conftest.py @@ -12,7 +12,8 @@ AMM_DEPLOYER, CONSTANT_MONETARY_POLICY_DEPLOYER, LM_CALLBACK_DEPLOYER, - BLOCK_COUNTER_DEPLOYER + BLOCK_COUNTER_DEPLOYER, + LL_CONTROLLER_DEPLOYER ) @@ -77,25 +78,15 @@ def controller_prefactory(stablecoin, weth, admin, accounts): @pytest.fixture(scope="module") -def controller_interface(): - return MINT_CONTROLLER_DEPLOYER - - -@pytest.fixture(scope="module") -def controller_impl(controller_interface, admin): +def controller_impl(admin): with boa.env.prank(admin): - return controller_interface.deploy_as_blueprint() - - -@pytest.fixture(scope="module") -def amm_interface(): - return AMM_DEPLOYER + return MINT_CONTROLLER_DEPLOYER.deploy_as_blueprint() @pytest.fixture(scope="module") -def amm_impl(stablecoin, amm_interface, admin): +def amm_impl(stablecoin, admin): with boa.env.prank(admin): - return amm_interface.deploy_as_blueprint() + return AMM_DEPLOYER.deploy_as_blueprint() @pytest.fixture(scope="module") @@ -145,13 +136,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, controller_factory, accounts): - return controller_interface.at(market.get_controller(collateral_token.address)) +def market_controller(market, stablecoin, collateral_token, controller_factory, accounts): + return LL_CONTROLLER_DEPLOYER.at(market.get_controller(collateral_token.address)) @pytest.fixture(scope="module") diff --git a/tests/stableborrow/conftest.py b/tests/stableborrow/conftest.py index 89e00d48..a90e9c9d 100644 --- a/tests/stableborrow/conftest.py +++ b/tests/stableborrow/conftest.py @@ -45,25 +45,15 @@ def controller_prefactory(controller_factory_impl, stablecoin, weth, admin, acco @pytest.fixture(scope="session") -def controller_interface(): - return MINT_CONTROLLER_DEPLOYER - - -@pytest.fixture(scope="session") -def controller_impl(controller_interface, admin): +def controller_impl(admin): with boa.env.prank(admin): - return controller_interface.deploy_as_blueprint() - - -@pytest.fixture(scope="session") -def amm_interface(): - return AMM_DEPLOYER + return MINT_CONTROLLER_DEPLOYER.deploy_as_blueprint() @pytest.fixture(scope="session") -def amm_impl(amm_interface, admin): +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") @@ -110,13 +100,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") diff --git a/tests/stableborrow/stabilize/conftest.py b/tests/stableborrow/stabilize/conftest.py index 655823c6..099a97ea 100644 --- a/tests/stableborrow/stabilize/conftest.py +++ b/tests/stableborrow/stabilize/conftest.py @@ -17,7 +17,9 @@ PEG_KEEPER_REGULATOR_DEPLOYER, PEG_KEEPER_V2_DEPLOYER, AGG_MONETARY_POLICY2_DEPLOYER, - CHAINLINK_AGGREGATOR_MOCK_DEPLOYER + CHAINLINK_AGGREGATOR_MOCK_DEPLOYER, + AMM_DEPLOYER, + LL_CONTROLLER_DEPLOYER ) from tests.utils.constants import ZERO_ADDRESS BASE_AMOUNT = 10**6 @@ -45,8 +47,8 @@ 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") @@ -274,8 +276,8 @@ def market_agg(controller_factory, collateral_token, agg_monetary_policy, crypto @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) @@ -284,8 +286,8 @@ def market_amm_agg(market, collateral_token, stablecoin, amm_impl, amm_interface @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 = LL_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) diff --git a/tests/stableborrow/test_bigfuzz.py b/tests/stableborrow/test_bigfuzz.py index 9c9b589c..203f07b7 100644 --- a/tests/stableborrow/test_bigfuzz.py +++ b/tests/stableborrow/test_bigfuzz.py @@ -7,6 +7,7 @@ from hypothesis.stateful import RuleBasedStateMachine, run_state_machine_as_test, rule, invariant from tests.utils.constants import ZERO_ADDRESS +from tests.utils.deployers import AMM_DEPLOYER, LL_CONTROLLER_DEPLOYER # Variables and methods to check # * A @@ -436,12 +437,13 @@ def rug_debt_ceiling(self): @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 = LL_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) @@ -746,7 +748,7 @@ 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, + 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(): diff --git a/tests/stableborrow/test_factory.py b/tests/stableborrow/test_factory.py index 6e6d1d00..4b533326 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, LL_CONTROLLER_DEPLOYER def test_stablecoin_admin(controller_factory, stablecoin, accounts): @@ -20,8 +21,7 @@ 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, @@ -37,8 +37,8 @@ def test_add_market(controller_factory, collateral_token, price_oracle, monetary assert controller_factory.n_collaterals() == 1 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 = LL_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() From 2786dc2cd9e0bd80da23f44804a4da07c3f74d90 Mon Sep 17 00:00:00 2001 From: Alberto Date: Sat, 23 Aug 2025 18:36:20 +0200 Subject: [PATCH 142/413] chore: simplify fixtures --- tests/conftest.py | 110 ++++++++++++++++++------------- tests/lending/conftest.py | 132 ++++++++------------------------------ 2 files changed, 94 insertions(+), 148 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0058e257..9e38133c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,77 +1,101 @@ import os from datetime import timedelta -from math import log -from typing import Any, Callable import boa import pytest from hypothesis import settings -from tests.utils.deployers import ERC20_MOCK_DEPLOYER, DUMMY_PRICE_ORACLE_DEPLOYER +from tests.utils.deploy import Protocol +from tests.utils.deployers import ( + ERC20_MOCK_DEPLOYER, + DUMMY_PRICE_ORACLE_DEPLOYER +) boa.env.enable_fast_mode() PRICE = 3000 +TESTING_DECIMALS = [2, 6, 8, 9, 18] settings.register_profile("default", deadline=timedelta(seconds=1000)) +# TODO add no shrink profile settings.load_profile(os.getenv(u"HYPOTHESIS_PROFILE", "default")) -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 Protocol() -@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 ERC20_MOCK_DEPLOYER +@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(digits) - return f +@pytest.fixture(scope="module") +def price_oracle(proto): + return proto.price_oracle -@pytest.fixture(scope="session") -def get_borrowed_token(token_mock, admin) -> Callable[[int], Any]: - def f(digits): - with boa.env.prank(admin): - return token_mock.deploy(digits) - return f +@pytest.fixture(scope="module") +def amm_impl(proto): + return proto.blueprints.amm + + +@pytest.fixture(scope="module") +def controller_impl(proto): + return proto.blueprints.ll_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 + + +# ============== 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 = DUMMY_PRICE_ORACLE_DEPLOYER.deploy(admin, PRICE * 10**18) - return oracle +def token_mock(): + return ERC20_MOCK_DEPLOYER + + +@pytest.fixture(scope="module") +def collateral_token(): + return ERC20_MOCK_DEPLOYER.deploy(18) diff --git a/tests/lending/conftest.py b/tests/lending/conftest.py index db4d7e58..a44ee364 100644 --- a/tests/lending/conftest.py +++ b/tests/lending/conftest.py @@ -1,93 +1,12 @@ import boa import pytest -from itertools import product -from tests.utils.deployers import ( - AMM_DEPLOYER, - LL_CONTROLLER_DEPLOYER, - VAULT_DEPLOYER, - CRYPTO_FROM_POOL_DEPLOYER, - SEMILOG_MONETARY_POLICY_DEPLOYER, - LENDING_FACTORY_DEPLOYER, - ERC20_MOCK_DEPLOYER, - FAKE_LEVERAGE_DEPLOYER -) - - -@pytest.fixture(scope="session") -def amm_interface(): - return AMM_DEPLOYER - - -@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 LL_CONTROLLER_DEPLOYER - - -@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_interface(): - return VAULT_DEPLOYER - - -@pytest.fixture(scope="session") -def vault_impl(admin): - with boa.env.prank(admin): - return VAULT_DEPLOYER.deploy() - - -@pytest.fixture(scope="session") -def price_oracle_interface(): - return CRYPTO_FROM_POOL_DEPLOYER - +from tests.utils.deployers import FAKE_LEVERAGE_DEPLOYER, ERC20_MOCK_DEPLOYER -@pytest.fixture(scope="session") -def price_oracle_impl(price_oracle_interface, admin): +@pytest.fixture(scope="module", params=[True, False]) +def tokens_for_vault(admin, stablecoin, decimals, request): + stablecoin_is_borrowed = request.param with boa.env.prank(admin): - return price_oracle_interface.deploy_as_blueprint() - - -@pytest.fixture(scope="session") -def mpolicy_interface(): - return SEMILOG_MONETARY_POLICY_DEPLOYER - - -@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 factory_partial(): - return LENDING_FACTORY_DEPLOYER - - -@pytest.fixture(scope="module") -def factory(factory_partial, amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, admin): - with boa.env.prank(admin): - return factory_partial.deploy(amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, admin, 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) + token = ERC20_MOCK_DEPLOYER.deploy(decimals) if stablecoin_is_borrowed: borrowed_token = stablecoin collateral_token = token @@ -108,42 +27,45 @@ def borrowed_token(tokens_for_vault): @pytest.fixture(scope="module") -def vault_controller_amm(factory, borrowed_token, collateral_token, price_oracle, admin): +def lending_market(proto, borrowed_token, collateral_token, price_oracle, admin): with boa.env.prank(admin): - return 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" + result = proto.create_lending_market( + borrowed_token=borrowed_token, + collateral_token=collateral_token, + A=100, + fee=int(0.006 * 1e18), + loan_discount=int(0.09 * 1e18), + liquidation_discount=int(0.06 * 1e18), + price_oracle=price_oracle, + name="Test vault", + min_borrow_rate=int(0.005 * 1e18) // (365 * 86400), # 0.5% APR + max_borrow_rate=int(0.5 * 1e18) // (365 * 86400) # 50% APR ) + return result @pytest.fixture(scope="module") -def vault(vault_interface, vault_controller_amm): - return vault_interface.at(vault_controller_amm[0]) +def vault(lending_market): + return lending_market['vault'] @pytest.fixture(scope="module") -def market_controller(controller_interface, vault_controller_amm, admin): - controller = controller_interface.at(vault_controller_amm[1]) +def market_controller(lending_market, admin): + controller = lending_market['controller'] with boa.env.prank(admin): controller.set_borrow_cap(2**256 - 1) - return controller @pytest.fixture(scope="module") -def market_amm(amm_interface, vault_controller_amm): - return amm_interface.at(vault_controller_amm[2]) +def market_amm(lending_market): + return lending_market['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 ERC20_MOCK_DEPLOYER +def market_mpolicy(market_controller): + from tests.utils.deployers import SEMILOG_MONETARY_POLICY_DEPLOYER + return SEMILOG_MONETARY_POLICY_DEPLOYER.at(market_controller.monetary_policy()) @pytest.fixture(scope="module") From aaba3921267ed944ca15dae13533f3525c6d00ac Mon Sep 17 00:00:00 2001 From: Alberto Date: Sat, 23 Aug 2025 22:35:05 +0200 Subject: [PATCH 143/413] Revert "fix: max_borrowable for LLController" This reverts commit 16f3e9061008547ad7d1eca84ff735097ac348d2. --- contracts/Controller.vy | 7 +++---- contracts/ControllerView.vy | 34 ++++++++++++++++++++++++++----- contracts/lending/LLController.vy | 26 +---------------------- 3 files changed, 33 insertions(+), 34 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index e06d7b52..a923adc5 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -535,10 +535,9 @@ def max_borrowable( @param user User to calculate the value for (only necessary for nonzero extra_health) @return Maximum amount of stablecoin to borrow """ - # Cannot borrow beyond the amount of coins Controller has - cap: uint256 = staticcall BORROWED_TOKEN.balanceOf(self) + current_debt - - return staticcall self._view.max_borrowable(collateral, N, cap, user) + return staticcall self._view.max_borrowable( + collateral, N, current_debt, user + ) @external diff --git a/contracts/ControllerView.vy b/contracts/ControllerView.vy index c074bb4f..deac1466 100644 --- a/contracts/ControllerView.vy +++ b/contracts/ControllerView.vy @@ -294,17 +294,16 @@ def _max_p_base() -> uint256: return p_base -@external +@internal @view -def max_borrowable( +def _max_borrowable( collateral: uint256, N: uint256, cap: uint256, + current_debt: uint256, user: address, ) -> uint256: - """ - @notice Natspec for this function is available in its controller contract - """ + # 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. @@ -337,6 +336,31 @@ def max_borrowable( 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, + current_debt, + user, + ) + + @external @view def min_collateral( diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy index 2ce61635..e977ca50 100644 --- a/contracts/lending/LLController.vy +++ b/contracts/lending/LLController.vy @@ -72,6 +72,7 @@ exports: ( core.user_state, core.users_to_liquidate, core.min_collateral, + core.max_borrowable, # For compatibility with mint markets ABI core.minted, core.redeemed, @@ -175,33 +176,8 @@ def borrowed_balance() -> uint256: return self._borrowed_balance() -@external -@view -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 - """ - # Cannot borrow beyond the amount of borrow_cap or borrowed_balance - total_debt: uint256 = core._get_total_debt() - cap: uint256 = unsafe_sub(max(core.borrow_cap, total_debt), total_debt) # don't exceed borrow_cap - cap = min(self._borrowed_balance() + current_debt, cap) # don't exceed borrowed_balance - - return staticcall core._view.max_borrowable(collateral, N, cap, user) - - # TODO delete deprecated and legacy files - @external def create_loan( collateral: uint256, From 8a6ce38241e69b8e1e8a4cbfbd83a10fc10cf5fc Mon Sep 17 00:00:00 2001 From: Oleg <40476427+amfet42@users.noreply.github.com> Date: Fri, 29 Aug 2025 14:37:53 +0300 Subject: [PATCH 144/413] add test contract --- contracts/zaps/PartialRepayZap.vy | 152 ++++++++++++++++++ .../Partial_Liquidation_Zap_Evaluation.pdf | Bin .../{ => calculations}/calculations.py | 2 +- .../frac5_calc.txt} | 0 .../test_partial_repay_zap.py | 119 ++++++++++++++ 5 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 contracts/zaps/PartialRepayZap.vy rename tests/zaps/partial_liquidation/{ => calculations}/Partial_Liquidation_Zap_Evaluation.pdf (100%) rename tests/zaps/partial_liquidation/{ => calculations}/calculations.py (99%) rename tests/zaps/partial_liquidation/{test_frac5.txt => calculations/frac5_calc.txt} (100%) create mode 100644 tests/zaps/partial_liquidation/test_partial_repay_zap.py diff --git a/contracts/zaps/PartialRepayZap.vy b/contracts/zaps/PartialRepayZap.vy new file mode 100644 index 00000000..f2868df1 --- /dev/null +++ b/contracts/zaps/PartialRepayZap.vy @@ -0,0 +1,152 @@ +# pragma version 0.4.3 +# pragma nonreentrancy on +# pragma optimize codesize + +""" +@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 ethereum.ercs import IERC20 +from contracts.interfaces import IAMM +from contracts.interfaces import ILlamalendController as IController + +from contracts import constants as c + +WAD: constant(uint256) = c.WAD + +CONTROLLER: public(immutable(IController)) +AMM: public(immutable(IAMM)) +BORROWED: public(immutable(IERC20)) +COLLATERAL: public(immutable(IERC20)) + +FRAC: public(immutable(uint256)) # fraction of position to repay (1e18 = 100%) +HEALTH_THRESHOLD: public(immutable(int256)) # trigger threshold on controller.health(user, false) + + +struct Position: + user: address + x: uint256 + y: uint256 + health: int256 + dx: uint256 # collateral estimated to be withdrawn for FRAC + dy: uint256 # borrowed needed for FRAC + + +@deploy +def __init__( + _controller: address, + _amm: address, + _borrowed: address, + _collateral: address, + _frac: uint256, # e.g. 5e16 == 5% + _health_threshold: int256, # e.g. 1e16 == 1% + ): + CONTROLLER = IController(_controller) + AMM = IAMM(_amm) + BORROWED = IERC20(_borrowed) + COLLATERAL = IERC20(_collateral) + + FRAC = _frac + HEALTH_THRESHOLD = _health_threshold + + self._approve(BORROWED, _controller) + self._approve(COLLATERAL, _controller) + + +@internal +def _approve(token: IERC20, spender: address): + if staticcall token.allowance(self, spender) == 0: + assert extcall token.approve(spender, max_value(uint256), default_return_value=True) + + +@internal +def _transferFrom(token: IERC20, _from: address, _to: address, amount: uint256): + if amount > 0: + assert extcall token.transferFrom(_from, _to, amount, default_return_value=True) + + +@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 = 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 + +# @external +# @view +# def users_to_liquidate(_from: uint256 = 0, _limit: uint256 = 0) -> DynArray[Position, 1000]: +# """ +# @notice Returns users eligible for partial self-liquidation through this zap. +# @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 +# """ +# n_loans: uint256 = CONTROLLER.n_loans() +# limit: uint256 = _limit if _limit != 0 else 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 = CONTROLLER.loans(ix) +# h: int256 = CONTROLLER.health(user, False) +# if CONTROLLER.approval(user, self) and h < HEALTH_THRESHOLD: +# xy: uint256[2] = AMM.get_sum_xy(user) +# dx: uint256 = self._collateral_from_liquidate(h, xy[1]) +# dy: uint256 = CONTROLLER.tokens_to_liquidate(user, FRAC) +# out.append(Position({ +# user: user, +# x: xy[0], +# y: xy[1], +# health: h, +# dx: dx, +# dy: dy, +# })) +# ix += 1 +# return out + + +@external +def repay_from_position(user: address, min_x: uint256): + """ + @notice Trigger partial self-liquidation of `user` using FRAC. + Caller supplies borrowed tokens; receives withdrawn collateral. + @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 + """ + assert staticcall CONTROLLER.approval(user, self), "not approved" + assert staticcall CONTROLLER.health(user, False) < HEALTH_THRESHOLD, "health too high" + + total_debt: uint256 = staticcall CONTROLLER.debt(user) + x_down: uint256 = staticcall AMM.get_x_down(user) + ratio: uint256 = unsafe_div(unsafe_mul(x_down, 10 ** 18), total_debt) + + assert ratio > 10 ** 18, "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_sub(unsafe_mul(to_repay, ratio), 10 ** 18) + + self._transferFrom(BORROWED, msg.sender, self, borrowed_from_sender) + + extcall CONTROLLER.liquidate(user, min_x, FRAC, empty(address), b"") + collateral_received: uint256 = staticcall COLLATERAL.balanceOf(self) + self._transferFrom(COLLATERAL, self, msg.sender, collateral_received) + + # sulprus amount goes into position repay + borrowed_amount: uint256 = staticcall BORROWED.balanceOf(self) + extcall CONTROLLER.repay(borrowed_amount, user, max_value(int256), empty(address), b"") diff --git a/tests/zaps/partial_liquidation/Partial_Liquidation_Zap_Evaluation.pdf b/tests/zaps/partial_liquidation/calculations/Partial_Liquidation_Zap_Evaluation.pdf similarity index 100% rename from tests/zaps/partial_liquidation/Partial_Liquidation_Zap_Evaluation.pdf rename to tests/zaps/partial_liquidation/calculations/Partial_Liquidation_Zap_Evaluation.pdf diff --git a/tests/zaps/partial_liquidation/calculations.py b/tests/zaps/partial_liquidation/calculations/calculations.py similarity index 99% rename from tests/zaps/partial_liquidation/calculations.py rename to tests/zaps/partial_liquidation/calculations/calculations.py index c319eaf8..af095531 100644 --- a/tests/zaps/partial_liquidation/calculations.py +++ b/tests/zaps/partial_liquidation/calculations/calculations.py @@ -182,5 +182,5 @@ def test_position(position, frac): except: continue -with open('./test_frac5.txt', 'w') as f: +with open('frac5_calc.txt', 'w') as f: f.write(res) diff --git a/tests/zaps/partial_liquidation/test_frac5.txt b/tests/zaps/partial_liquidation/calculations/frac5_calc.txt similarity index 100% rename from tests/zaps/partial_liquidation/test_frac5.txt rename to tests/zaps/partial_liquidation/calculations/frac5_calc.txt 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..d0141d52 --- /dev/null +++ b/tests/zaps/partial_liquidation/test_partial_repay_zap.py @@ -0,0 +1,119 @@ +import boa +import pytest + + +@pytest.fixture(scope="module") +def get_partial_repay_zap(admin, collateral_token, stablecoin, dummy_router): + def deploy_partial_repay_zap(controller_address, amm_address): + with boa.env.prank(admin): + return boa.load( + "contracts/zaps/PartialRepayZap.vy", + str(dummy_router.address), + controller_address, + amm_address, + stablecoin.address, + collateral_token.address, + 5 * 10 ** 16, + 1 * 10 ** 16, + ) + + return deploy_partial_repay_zap + + +@pytest.fixture(scope="module") +def controller_for_liquidation( + borrowed_token, + collateral_token, + market_controller, + market_amm, + price_oracle, + monetary_policy, + admin, +): + def f(sleep_time, user): + N = 5 + collateral_amount = 10**18 + + with boa.env.prank(admin): + market_controller.set_amm_fee(10**6) + monetary_policy.set_rate(int(1e18 * 1.0 / 365 / 86400)) # 100% APY + + debt = market_controller.max_borrowable(collateral_amount, N) + with boa.env.prank(user): + collateral_token._mint_for_testing(user, collateral_amount) + borrowed_token.approve(market_amm, 2**256 - 1) + borrowed_token.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) + # We put mostly USD into AMM, and its quantity remains constant while + # interest is accruing. Therefore, we will be at liquidation at some point + with boa.env.prank(user): + market_amm.exchange(0, 1, debt, 0) + health_1 = market_controller.health(user) + + assert health_0 <= health_1 # Earns fees on dynamic fee + + boa.env.time_travel(sleep_time) + + health_2 = market_controller.health(user) + # Still healthy but liquidation threshold satisfied + assert 0 < health_2 < market_controller.liquidation_discount() + + with boa.env.prank(admin): + # Stop charging fees to have enough coins to liquidate in existence a block before + monetary_policy.set_rate(0) + + return market_controller + + return f + + +def test_self_liquidate( + borrowed_token, + collateral_token, + controller_for_liquidation, + market_amm, + accounts, + get_partial_repay_zap, + dummy_router, +): + user = accounts[1] + liquidator = accounts[2] + controller = controller_for_liquidation(sleep_time=int(42 * 86400), user=user) + partial_repay_zap = get_partial_repay_zap(controller.address, controller.amm) + someone_else = str(partial_repay_zap.address) + + controller.approve(someone_else, True, sender=user) + + h = controller.health(user) / 10**16 + assert 0 < h < 1 + + frac = 0.05 + + h_norm = h * 10**16 + frac_norm = frac * 10**18 + collateral = controller.user_state(user)[0] + collateral_in = int( + ((10**18 + h_norm // 2) * (10**18 - frac_norm) // (10**18 + h_norm) + frac_norm) + * frac_norm + * collateral + // 10**36 + ) + stablecoin_out = 10000 * collateral_in + + # Ensure router has stablecoin + with boa.env.prank(liquidator): + collateral_token._mint_for_testing(dummy_router, 10**21) + collateral_token.approve(controller, 2**256 - 1) + controller.create_loan(10**20, controller.max_borrowable(10**20, 5), 5) + + partial_repay_zap.repay_from_position( + user, + 0, + sender=liquidator, + ) + + h = controller.health(user) / 10**16 + assert h > 3 From 47f1cbbde82db891a3e16ffffa6ab2705ef8324b Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 29 Aug 2025 16:26:39 +0200 Subject: [PATCH 145/413] test: add no-shrink profile --- tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 9e38133c..6132307b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ import boa import pytest -from hypothesis import settings +from hypothesis import settings, Phase from tests.utils.deploy import Protocol from tests.utils.deployers import ( ERC20_MOCK_DEPLOYER, @@ -18,8 +18,8 @@ TESTING_DECIMALS = [2, 6, 8, 9, 18] +settings.register_profile("no-shrink", settings(phases=list(Phase)[:4]), deadline=timedelta(seconds=1000)) settings.register_profile("default", deadline=timedelta(seconds=1000)) -# TODO add no shrink profile settings.load_profile(os.getenv(u"HYPOTHESIS_PROFILE", "default")) From 937264e65c4959094ce504900b09e7353b0b3c32 Mon Sep 17 00:00:00 2001 From: Oleg <40476427+amfet42@users.noreply.github.com> Date: Sun, 31 Aug 2025 00:18:26 +0300 Subject: [PATCH 146/413] Add partial repay zap fixes --- .../testing/ConstantMonetaryPolicyLending.vy | 33 + contracts/testing/ERC20Mock.vy | 12 +- contracts/zaps/PartialRepayZap.vy | 142 ++-- tests/utils/deployers.py | 1 + tests/zaps/conftest.py | 156 +++++ .../calculations/calculations.py | 632 ++++++++++++++---- .../test_partial_repay_zap.py | 114 ++-- 7 files changed, 853 insertions(+), 237 deletions(-) create mode 100644 contracts/testing/ConstantMonetaryPolicyLending.vy create mode 100644 tests/zaps/conftest.py diff --git a/contracts/testing/ConstantMonetaryPolicyLending.vy b/contracts/testing/ConstantMonetaryPolicyLending.vy new file mode 100644 index 00000000..a40420fc --- /dev/null +++ b/contracts/testing/ConstantMonetaryPolicyLending.vy @@ -0,0 +1,33 @@ +# @version 0.4.3 +""" +Although this monetary policy works, it's only intended to be used in tests +""" + +from ethereum.ercs import IERC20 + + +admin: public(address) +rate: public(uint256) + + +@deploy +def __init__(borrowed_token: IERC20, min_rate: uint256, max_rate: uint256): + self.admin = tx.origin + self.rate = min_rate + + +@external +def set_admin(admin: address): + assert msg.sender == self.admin + self.admin = admin + + +@external +def rate_write() -> uint256: + return self.rate + + +@external +def set_rate(rate: uint256): + assert msg.sender == self.admin + self.rate = rate diff --git a/contracts/testing/ERC20Mock.vy b/contracts/testing/ERC20Mock.vy index c2db01c1..45389f39 100644 --- a/contracts/testing/ERC20Mock.vy +++ b/contracts/testing/ERC20Mock.vy @@ -1,5 +1,8 @@ # pragma version 0.4.3 +from ethereum.ercs import IERC20 +implements: IERC20 + from snekmate.tokens import erc20 from snekmate.auth import ownable @@ -12,4 +15,11 @@ exports: erc20.__interface__ @deploy def __init__(decimals: uint256): ownable.__init__() - erc20.__init__("mock", "mock", convert(decimals, uint8), "mock", "mock") \ No newline at end of file + erc20.__init__("mock", "mock", convert(decimals, uint8), "mock", "mock") + + +@external +def _mint_for_testing(_target: address, _value: uint256) -> bool: + erc20._mint(_target, _value) + log IERC20.Transfer(sender=empty(address), receiver=_target, value=_value) + return True diff --git a/contracts/zaps/PartialRepayZap.vy b/contracts/zaps/PartialRepayZap.vy index f2868df1..46c2d64a 100644 --- a/contracts/zaps/PartialRepayZap.vy +++ b/contracts/zaps/PartialRepayZap.vy @@ -18,11 +18,6 @@ from contracts import constants as c WAD: constant(uint256) = c.WAD -CONTROLLER: public(immutable(IController)) -AMM: public(immutable(IAMM)) -BORROWED: public(immutable(IERC20)) -COLLATERAL: public(immutable(IERC20)) - FRAC: public(immutable(uint256)) # fraction of position to repay (1e18 = 100%) HEALTH_THRESHOLD: public(immutable(int256)) # trigger threshold on controller.health(user, false) @@ -36,26 +31,22 @@ struct Position: dy: uint256 # borrowed needed for FRAC +event PartialRepay: + controller: address + user: address + collateral_decrease: uint256 + borrowed_from_sender: uint256 + sulprus_repayed: uint256 + + @deploy def __init__( - _controller: address, - _amm: address, - _borrowed: address, - _collateral: address, _frac: uint256, # e.g. 5e16 == 5% _health_threshold: int256, # e.g. 1e16 == 1% ): - CONTROLLER = IController(_controller) - AMM = IAMM(_amm) - BORROWED = IERC20(_borrowed) - COLLATERAL = IERC20(_collateral) - FRAC = _frac HEALTH_THRESHOLD = _health_threshold - self._approve(BORROWED, _controller) - self._approve(COLLATERAL, _controller) - @internal def _approve(token: IERC20, spender: address): @@ -69,6 +60,12 @@ def _transferFrom(token: IERC20, _from: address, _to: address, amount: uint256): assert extcall token.transferFrom(_from, _to, amount, default_return_value=True) +@internal +def _transfer(token: IERC20, _to: address, amount: uint256): + if amount > 0: + assert extcall token.transfer(_to, amount, default_return_value=True) + + @internal @pure def _get_f_remove(frac: uint256, health_limit: uint256) -> uint256: @@ -86,67 +83,90 @@ def _get_f_remove(frac: uint256, health_limit: uint256) -> uint256: return f_remove -# @external -# @view -# def users_to_liquidate(_from: uint256 = 0, _limit: uint256 = 0) -> DynArray[Position, 1000]: -# """ -# @notice Returns users eligible for partial self-liquidation through this zap. -# @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 -# """ -# n_loans: uint256 = CONTROLLER.n_loans() -# limit: uint256 = _limit if _limit != 0 else 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 = CONTROLLER.loans(ix) -# h: int256 = CONTROLLER.health(user, False) -# if CONTROLLER.approval(user, self) and h < HEALTH_THRESHOLD: -# xy: uint256[2] = AMM.get_sum_xy(user) -# dx: uint256 = self._collateral_from_liquidate(h, xy[1]) -# dy: uint256 = CONTROLLER.tokens_to_liquidate(user, FRAC) -# out.append(Position({ -# user: user, -# x: xy[0], -# y: xy[1], -# health: h, -# dx: dx, -# dy: dy, -# })) -# ix += 1 -# return out + +@external +@view +def users_to_liquidate(_controller: address, _from: uint256 = 0, _limit: uint256 = 0) -> DynArray[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 + """ + CONTROLLER: IController = IController(_controller) + 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[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, False) + if staticcall CONTROLLER.approval(user, self) and h < HEALTH_THRESHOLD: + xy: uint256[2] = staticcall AMM.get_sum_xy(user) + to_repay: uint256 = staticcall CONTROLLER.tokens_to_liquidate(user, FRAC) + total_debt: uint256 = staticcall CONTROLLER.debt(user) + x_down: uint256 = staticcall AMM.get_x_down(user) + ratio: uint256 = unsafe_div(unsafe_mul(x_down, 10 ** 18), total_debt) + out.append(Position( + user=user, + x=xy[0], + y=xy[1], + health=h, + dx=unsafe_div(unsafe_mul(to_repay, ratio), 10 ** 18), + dy=unsafe_div(xy[1] * self._get_f_remove(FRAC, 0), WAD), + )) + ix += 1 + return out @external -def repay_from_position(user: address, min_x: uint256): +def repay_from_position(_controller: address, _user: address, _min_x: uint256): """ @notice Trigger partial self-liquidation of `user` using FRAC. Caller supplies borrowed tokens; receives withdrawn collateral. - @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 _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 """ - assert staticcall CONTROLLER.approval(user, self), "not approved" - assert staticcall CONTROLLER.health(user, False) < HEALTH_THRESHOLD, "health too high" + CONTROLLER: IController = IController(_controller) + AMM: IAMM = staticcall CONTROLLER.amm() + 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" + + self._approve(BORROWED, _controller) - total_debt: uint256 = staticcall CONTROLLER.debt(user) - x_down: uint256 = staticcall AMM.get_x_down(user) + total_debt: uint256 = staticcall CONTROLLER.debt(_user) + x_down: uint256 = staticcall AMM.get_x_down(_user) ratio: uint256 = unsafe_div(unsafe_mul(x_down, 10 ** 18), total_debt) assert ratio > 10 ** 18, "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_sub(unsafe_mul(to_repay, ratio), 10 ** 18) + to_repay: uint256 = staticcall CONTROLLER.tokens_to_liquidate(_user, FRAC) + borrowed_from_sender: uint256 = unsafe_div(unsafe_mul(to_repay, ratio), 10 ** 18) self._transferFrom(BORROWED, msg.sender, self, borrowed_from_sender) - extcall CONTROLLER.liquidate(user, min_x, FRAC, empty(address), b"") + extcall CONTROLLER.liquidate(_user, _min_x, FRAC, empty(address), b"") collateral_received: uint256 = staticcall COLLATERAL.balanceOf(self) - self._transferFrom(COLLATERAL, self, msg.sender, collateral_received) + self._transfer(COLLATERAL, msg.sender, collateral_received) # sulprus amount goes into position repay borrowed_amount: uint256 = staticcall BORROWED.balanceOf(self) - extcall CONTROLLER.repay(borrowed_amount, user, max_value(int256), empty(address), b"") + extcall CONTROLLER.repay(borrowed_amount, _user, max_value(int256), empty(address), b"") + + log PartialRepay( + controller=_controller, + user=_user, + collateral_decrease=collateral_received, + borrowed_from_sender=borrowed_from_sender, + sulprus_repayed=borrowed_amount, + ) diff --git a/tests/utils/deployers.py b/tests/utils/deployers.py index 8e388e2c..79499de8 100644 --- a/tests/utils/deployers.py +++ b/tests/utils/deployers.py @@ -59,6 +59,7 @@ # 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) diff --git a/tests/zaps/conftest.py b/tests/zaps/conftest.py new file mode 100644 index 00000000..88a4433b --- /dev/null +++ b/tests/zaps/conftest.py @@ -0,0 +1,156 @@ +import boa +import pytest + +from tests.utils.deploy import Blueprints, Protocol + +from tests.utils.deployers import ( + # Core contracts + STABLECOIN_DEPLOYER, + AMM_DEPLOYER, + MINT_CONTROLLER_DEPLOYER, + CONTROLLER_FACTORY_DEPLOYER, + # Lending contracts + VAULT_DEPLOYER, + LL_CONTROLLER_DEPLOYER, + LL_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, + # Testing contracts + WETH_DEPLOYER, + ERC20_MOCK_DEPLOYER, +) + + +@pytest.fixture(scope="module") +def tokens_for_vault(admin, stablecoin): + with boa.env.prank(admin): + token = ERC20_MOCK_DEPLOYER.deploy(18) + return stablecoin, token + + +@pytest.fixture(scope="module") +def collateral_token(tokens_for_vault): + return tokens_for_vault[1] + + +@pytest.fixture(scope="module") +def borrowed_token(tokens_for_vault): + return tokens_for_vault[0] + + +@pytest.fixture(scope="module") +def lending_market(borrowed_token, collateral_token, price_oracle, admin): + + class ProtocolWithConstantMP(Protocol): + def __init__(self, initial_price: int = 3000 * 10**18): + self.admin = admin + self.fee_receiver = admin + + # Deploy all blueprints + self.blueprints = Blueprints( + amm=AMM_DEPLOYER, + mint_controller=MINT_CONTROLLER_DEPLOYER, + ll_controller=LL_CONTROLLER_DEPLOYER, + ll_controller_view=LL_CONTROLLER_VIEW_DEPLOYER, + price_oracle=CRYPTO_FROM_POOL_DEPLOYER, + mpolicy=CONSTANT_MONETARY_POLICY_LENDING_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 vault implementation + self.vault_impl = VAULT_DEPLOYER.deploy() + + # Deploy lending factory + self.lending_factory = LENDING_FACTORY_DEPLOYER.deploy( + self.blueprints.amm.address, + self.blueprints.ll_controller.address, + self.vault_impl.address, + self.blueprints.price_oracle.address, + self.blueprints.ll_controller_view.address, + self.blueprints.mpolicy.address, + self.admin, + self.fee_receiver, + ) + + with boa.env.prank(admin): + result = ProtocolWithConstantMP().create_lending_market( + borrowed_token=borrowed_token, + collateral_token=collateral_token, + A=100, + fee=int(0.006 * 1e18), + loan_discount=int(0.09 * 1e18), + liquidation_discount=int(0.06 * 1e18), + price_oracle=price_oracle, + name="Test vault", + min_borrow_rate=int(0.005 * 1e18) // (365 * 86400), # 0.5% APR + max_borrow_rate=int(0.5 * 1e18) // (365 * 86400), # 50% APR + ) + return result + + +@pytest.fixture(scope="module") +def vault(lending_market): + return lending_market["vault"] + + +@pytest.fixture(scope="module") +def market_controller(lending_market, admin): + controller = lending_market["controller"] + with boa.env.prank(admin): + controller.set_borrow_cap(2**256 - 1) + return controller + + +@pytest.fixture(scope="module") +def market_amm(lending_market): + return lending_market["amm"] + + +@pytest.fixture(scope="module") +def market_mpolicy(market_controller): + return CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER.at(market_controller.monetary_policy()) + + +@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()) + boa.deal(borrowed_token, admin, amount) + borrowed_token.approve(vault.address, 2**256 - 1) + vault.deposit(amount) + return market_controller diff --git a/tests/zaps/partial_liquidation/calculations/calculations.py b/tests/zaps/partial_liquidation/calculations/calculations.py index af095531..efeff874 100644 --- a/tests/zaps/partial_liquidation/calculations/calculations.py +++ b/tests/zaps/partial_liquidation/calculations/calculations.py @@ -6,113 +6,501 @@ from web3 import HTTPProvider, Web3 import json -logger = logging.getLogger('warnings') +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"], + [ + 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"}]}]""" +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): @@ -138,34 +526,50 @@ def test_position(position, frac): 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}") + 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) + 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) + 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]) + collateral_used = user_state[0] - user_state2[0] frac_price = (debt_repayed * ratio) / collateral_used - spot_surplus = (collateral_used * spot_price - debt_repayed) + 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}") + 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}") + 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) @@ -176,11 +580,11 @@ def test_position(position, frac): for position in positions: try: - res += test_position(position, 0.05) + '\n' + "-----------------------------" + "\n" + res += test_position(position, 0.05) + "\n" + "-----------------------------" + "\n" except KeyboardInterrupt: break except: continue -with open('frac5_calc.txt', 'w') as f: +with open("frac5_calc.txt", "w") as f: f.write(res) diff --git a/tests/zaps/partial_liquidation/test_partial_repay_zap.py b/tests/zaps/partial_liquidation/test_partial_repay_zap.py index d0141d52..e49feadd 100644 --- a/tests/zaps/partial_liquidation/test_partial_repay_zap.py +++ b/tests/zaps/partial_liquidation/test_partial_repay_zap.py @@ -3,31 +3,19 @@ @pytest.fixture(scope="module") -def get_partial_repay_zap(admin, collateral_token, stablecoin, dummy_router): - def deploy_partial_repay_zap(controller_address, amm_address): - with boa.env.prank(admin): - return boa.load( - "contracts/zaps/PartialRepayZap.vy", - str(dummy_router.address), - controller_address, - amm_address, - stablecoin.address, - collateral_token.address, - 5 * 10 ** 16, - 1 * 10 ** 16, - ) - - return deploy_partial_repay_zap +def partial_repay_zap(admin): + with boa.env.prank(admin): + return boa.load("contracts/zaps/PartialRepayZap.vy", 5 * 10**16, 1 * 10**16) @pytest.fixture(scope="module") def controller_for_liquidation( borrowed_token, collateral_token, - market_controller, + filled_controller, market_amm, price_oracle, - monetary_policy, + market_mpolicy, admin, ): def f(sleep_time, user): @@ -35,85 +23,89 @@ def f(sleep_time, user): collateral_amount = 10**18 with boa.env.prank(admin): - market_controller.set_amm_fee(10**6) - monetary_policy.set_rate(int(1e18 * 1.0 / 365 / 86400)) # 100% APY + filled_controller.set_amm_fee(10**6) + market_mpolicy.set_rate(int(1e18 * 1.0 / 365 / 86400)) # 100% APY - debt = market_controller.max_borrowable(collateral_amount, N) + debt = filled_controller.max_borrowable(collateral_amount, N) with boa.env.prank(user): collateral_token._mint_for_testing(user, collateral_amount) borrowed_token.approve(market_amm, 2**256 - 1) - borrowed_token.approve(market_controller, 2**256 - 1) - collateral_token.approve(market_controller, 2**256 - 1) - market_controller.create_loan(collateral_amount, debt, N) + borrowed_token.approve(filled_controller, 2**256 - 1) + collateral_token.approve(filled_controller, 2**256 - 1) + filled_controller.create_loan(collateral_amount, debt, N) - health_0 = market_controller.health(user) + health_0 = filled_controller.health(user) # We put mostly USD into AMM, and its quantity remains constant while # interest is accruing. Therefore, we will be at liquidation at some point with boa.env.prank(user): market_amm.exchange(0, 1, debt, 0) - health_1 = market_controller.health(user) + health_1 = filled_controller.health(user) assert health_0 <= health_1 # Earns fees on dynamic fee boa.env.time_travel(sleep_time) - health_2 = market_controller.health(user) + health_2 = filled_controller.health(user) # Still healthy but liquidation threshold satisfied - assert 0 < health_2 < market_controller.liquidation_discount() + assert 0 < health_2 < filled_controller.liquidation_discount() with boa.env.prank(admin): # Stop charging fees to have enough coins to liquidate in existence a block before - monetary_policy.set_rate(0) + market_mpolicy.set_rate(0) - return market_controller + return filled_controller return f -def test_self_liquidate( +@pytest.mark.parametrize("is_approved", [True, False]) +def test_users_to_liquidate( borrowed_token, collateral_token, controller_for_liquidation, market_amm, accounts, - get_partial_repay_zap, - dummy_router, + partial_repay_zap, + is_approved, +): + user = accounts[1] + controller = controller_for_liquidation(sleep_time=int(36.1 * 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_repay_from_position( + borrowed_token, + collateral_token, + controller_for_liquidation, + market_amm, + accounts, + partial_repay_zap, ): user = accounts[1] liquidator = accounts[2] - controller = controller_for_liquidation(sleep_time=int(42 * 86400), user=user) - partial_repay_zap = get_partial_repay_zap(controller.address, controller.amm) + controller = controller_for_liquidation(sleep_time=int(36.1 * 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 < h < 1 - - frac = 0.05 - - h_norm = h * 10**16 - frac_norm = frac * 10**18 - collateral = controller.user_state(user)[0] - collateral_in = int( - ((10**18 + h_norm // 2) * (10**18 - frac_norm) // (10**18 + h_norm) + frac_norm) - * frac_norm - * collateral - // 10**36 - ) - stablecoin_out = 10000 * collateral_in - - # Ensure router has stablecoin - with boa.env.prank(liquidator): - collateral_token._mint_for_testing(dummy_router, 10**21) - collateral_token.approve(controller, 2**256 - 1) - controller.create_loan(10**20, controller.max_borrowable(10**20, 5), 5) + assert 0.9 < h < 1 - partial_repay_zap.repay_from_position( - user, - 0, - sender=liquidator, - ) + # 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.repay_from_position(controller.address, user, 0) h = controller.health(user) / 10**16 - assert h > 3 + assert h > 1 From 9360bb20b83318a82ba0aecaef7e16b2b0962349 Mon Sep 17 00:00:00 2001 From: Oleg <40476427+amfet42@users.noreply.github.com> Date: Sun, 31 Aug 2025 00:30:47 +0300 Subject: [PATCH 147/413] rollback erc20mock - remove _mint_for_testing --- contracts/testing/ERC20Mock.vy | 7 ------- tests/zaps/partial_liquidation/test_partial_repay_zap.py | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/contracts/testing/ERC20Mock.vy b/contracts/testing/ERC20Mock.vy index 45389f39..00f6b505 100644 --- a/contracts/testing/ERC20Mock.vy +++ b/contracts/testing/ERC20Mock.vy @@ -16,10 +16,3 @@ exports: erc20.__interface__ def __init__(decimals: uint256): ownable.__init__() erc20.__init__("mock", "mock", convert(decimals, uint8), "mock", "mock") - - -@external -def _mint_for_testing(_target: address, _value: uint256) -> bool: - erc20._mint(_target, _value) - log IERC20.Transfer(sender=empty(address), receiver=_target, value=_value) - return True diff --git a/tests/zaps/partial_liquidation/test_partial_repay_zap.py b/tests/zaps/partial_liquidation/test_partial_repay_zap.py index e49feadd..3eb4dfdd 100644 --- a/tests/zaps/partial_liquidation/test_partial_repay_zap.py +++ b/tests/zaps/partial_liquidation/test_partial_repay_zap.py @@ -28,7 +28,7 @@ def f(sleep_time, user): debt = filled_controller.max_borrowable(collateral_amount, N) with boa.env.prank(user): - collateral_token._mint_for_testing(user, collateral_amount) + boa.deal(collateral_token, user, collateral_amount) borrowed_token.approve(market_amm, 2**256 - 1) borrowed_token.approve(filled_controller, 2**256 - 1) collateral_token.approve(filled_controller, 2**256 - 1) From ed84c61401bef73cedb80be4861ea9b8a95d4547 Mon Sep 17 00:00:00 2001 From: Oleg <40476427+amfet42@users.noreply.github.com> Date: Sun, 31 Aug 2025 01:14:50 +0300 Subject: [PATCH 148/413] better function name --- contracts/zaps/PartialRepayZap.vy | 2 +- tests/zaps/partial_liquidation/test_partial_repay_zap.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/zaps/PartialRepayZap.vy b/contracts/zaps/PartialRepayZap.vy index 46c2d64a..82ce4bcc 100644 --- a/contracts/zaps/PartialRepayZap.vy +++ b/contracts/zaps/PartialRepayZap.vy @@ -125,7 +125,7 @@ def users_to_liquidate(_controller: address, _from: uint256 = 0, _limit: uint256 @external -def repay_from_position(_controller: address, _user: address, _min_x: uint256): +def liquidate_partial(_controller: address, _user: address, _min_x: uint256): """ @notice Trigger partial self-liquidation of `user` using FRAC. Caller supplies borrowed tokens; receives withdrawn collateral. diff --git a/tests/zaps/partial_liquidation/test_partial_repay_zap.py b/tests/zaps/partial_liquidation/test_partial_repay_zap.py index 3eb4dfdd..a68c8c6b 100644 --- a/tests/zaps/partial_liquidation/test_partial_repay_zap.py +++ b/tests/zaps/partial_liquidation/test_partial_repay_zap.py @@ -84,7 +84,7 @@ def test_users_to_liquidate( assert users_to_liquidate[0][0] == user -def test_repay_from_position( +def test_liquidate_partial( borrowed_token, collateral_token, controller_for_liquidation, @@ -105,7 +105,7 @@ def test_repay_from_position( 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.repay_from_position(controller.address, user, 0) + partial_repay_zap.liquidate_partial(controller.address, user, 0) h = controller.health(user) / 10**16 assert h > 1 From 57b0ec3c29c7424adbfd1ae9ac38d4b75387f3dc Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 11:47:11 +0200 Subject: [PATCH 149/413] chore: add sanity check --- contracts/Controller.vy | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index a923adc5..22e6e46a 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -193,6 +193,7 @@ 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, From d28954b608c60202c36bcf6a92c75448d8276eb7 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 12:35:07 +0200 Subject: [PATCH 150/413] test: simplify flashloan tests --- tests/flashloan/conftest.py | 60 ++-------------------------- tests/flashloan/test_debt_ceiling.py | 39 +++++++++--------- tests/flashloan/test_flashloan.py | 12 ++++-- 3 files changed, 29 insertions(+), 82 deletions(-) diff --git a/tests/flashloan/conftest.py b/tests/flashloan/conftest.py index 09a161fe..1490ce25 100644 --- a/tests/flashloan/conftest.py +++ b/tests/flashloan/conftest.py @@ -1,68 +1,15 @@ import boa import pytest from tests.utils.deployers import ( - STABLECOIN_DEPLOYER, - WETH_DEPLOYER, - CONTROLLER_FACTORY_DEPLOYER, FLASH_LENDER_DEPLOYER, - AMM_DEPLOYER, DUMMY_FLASH_BORROWER_DEPLOYER ) -@pytest.fixture(scope="session") -def stablecoin_pre(): - return STABLECOIN_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 WETH_DEPLOYER.deploy() - - -@pytest.fixture(scope="session") -def controller_factory_impl(): - 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_impl(admin): - with boa.env.prank(admin): - return FLASH_LENDER_DEPLOYER.deploy_as_blueprint() - - -@pytest.fixture(scope="session") -def amm_impl(admin): - with boa.env.prank(admin): - return AMM_DEPLOYER.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 the preconfigured mint factory from Protocol.""" + return proto.mint_factory @pytest.fixture(scope="module") @@ -75,7 +22,6 @@ def flash_lender(controller_factory, admin, max_flash_loan): with boa.env.prank(admin): fl = FLASH_LENDER_DEPLOYER.deploy(controller_factory.address) controller_factory.set_debt_ceiling(fl.address, max_flash_loan) - return fl diff --git a/tests/flashloan/test_debt_ceiling.py b/tests/flashloan/test_debt_ceiling.py index dc135ed2..0fbfcfb9 100644 --- a/tests/flashloan/test_debt_ceiling.py +++ b/tests/flashloan/test_debt_ceiling.py @@ -32,39 +32,36 @@ def test_empty_flash_lender(controller_factory, flash_lender, stablecoin, max_fl 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) + 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) + 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) + 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..2fa088c1 100644 --- a/tests/flashloan/test_flashloan.py +++ b/tests/flashloan/test_flashloan.py @@ -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) From c94fe04d7ef81238bed13428b9fd3ac34e6116ea Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 12:35:27 +0200 Subject: [PATCH 151/413] chore: more TODOs --- contracts/price_oracles/proxy/ProxyOracle.vy | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/price_oracles/proxy/ProxyOracle.vy b/contracts/price_oracles/proxy/ProxyOracle.vy index ede9cc52..266a5215 100644 --- a/contracts/price_oracles/proxy/ProxyOracle.vy +++ b/contracts/price_oracles/proxy/ProxyOracle.vy @@ -9,6 +9,8 @@ @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 From ba3780d8fcc707802bc63a4ad4042050c8eb75b6 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 13:22:08 +0200 Subject: [PATCH 152/413] test: fix price oracle tests --- tests/flashloan/conftest.py | 1 - tests/price_oracles/proxy/conftest.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/flashloan/conftest.py b/tests/flashloan/conftest.py index 1490ce25..40fcd747 100644 --- a/tests/flashloan/conftest.py +++ b/tests/flashloan/conftest.py @@ -8,7 +8,6 @@ @pytest.fixture(scope="module") def controller_factory(proto): - """Return the preconfigured mint factory from Protocol.""" return proto.mint_factory diff --git a/tests/price_oracles/proxy/conftest.py b/tests/price_oracles/proxy/conftest.py index 71538eb7..0d224039 100644 --- a/tests/price_oracles/proxy/conftest.py +++ b/tests/price_oracles/proxy/conftest.py @@ -13,7 +13,7 @@ def user(accounts): return accounts[0] -@pytest.fixture(scope="session") +@pytest.fixture(scope="module") def get_price_oracle(admin): def f(price): with boa.env.prank(admin): @@ -23,7 +23,7 @@ def f(price): return f -@pytest.fixture(scope="session") +@pytest.fixture(scope="module") def broken_price_oracle(admin): with boa.env.prank(admin): oracle = WETH_DEPLOYER.deploy() From 59ab9cce5e4c0acba5633ff18fb1ea6035a684a3 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 13:44:19 +0200 Subject: [PATCH 153/413] test: fix amm fixtures scope --- tests/amm/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/amm/conftest.py b/tests/amm/conftest.py index 19f1fd72..315ca022 100644 --- a/tests/amm/conftest.py +++ b/tests/amm/conftest.py @@ -11,7 +11,7 @@ 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): @@ -30,6 +30,6 @@ def f(collateral_token, borrowed_token): 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) From c32c2cc6e580dda7aa2fd0ec15b4b8eaacb259bb Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 13:47:10 +0200 Subject: [PATCH 154/413] test: use more decimals to tests things --- tests/amm/conftest.py | 5 ----- tests/conftest.py | 6 ++++++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/amm/conftest.py b/tests/amm/conftest.py index 315ca022..4ec23de7 100644 --- a/tests/amm/conftest.py +++ b/tests/amm/conftest.py @@ -6,11 +6,6 @@ PRICE = 3000 -@pytest.fixture(scope="session") -def borrowed_token(): - return ERC20_MOCK_DEPLOYER.deploy(6) - - @pytest.fixture(scope="module") def get_amm(price_oracle, admin, accounts): def f(collateral_token, borrowed_token): diff --git a/tests/conftest.py b/tests/conftest.py index 6132307b..9ea22cbd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -99,3 +99,9 @@ def token_mock(): @pytest.fixture(scope="module") def collateral_token(): return ERC20_MOCK_DEPLOYER.deploy(18) + + +@pytest.fixture(scope="module") +def borrowed_token(decimals): + """Parametrized borrowed token using the global `decimals` list.""" + return ERC20_MOCK_DEPLOYER.deploy(decimals) From 15a5813f4a25bc9d3b33897fed82f2f23952bb65 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 13:55:31 +0200 Subject: [PATCH 155/413] test: use more decimals in testing --- tests/amm/test_exchange_dy.py | 5 +++-- tests/amm/test_flip_dy.py | 5 ----- tests/amm/test_oracle_change_noloss.py | 5 ----- tests/amm/test_xdown_yup_invariants_dy.py | 5 ----- tests/lending/test_secondary_mpolicy.py | 5 ----- 5 files changed, 3 insertions(+), 22 deletions(-) diff --git a/tests/amm/test_exchange_dy.py b/tests/amm/test_exchange_dy.py index 98929ed9..643baccb 100644 --- a/tests/amm/test_exchange_dy.py +++ b/tests/amm/test_exchange_dy.py @@ -7,8 +7,9 @@ @pytest.fixture(scope="module") -def borrowed_token(): - return ERC20_MOCK_DEPLOYER.deploy(18) +def decimals(): + """Override global decimals to fix borrowed_token at 18 for this module.""" + return 18 @pytest.fixture(scope="module") diff --git a/tests/amm/test_flip_dy.py b/tests/amm/test_flip_dy.py index 00d025ca..ee2a786e 100644 --- a/tests/amm/test_flip_dy.py +++ b/tests/amm/test_flip_dy.py @@ -13,11 +13,6 @@ STEP = 0.01 -@pytest.fixture(scope="module") -def borrowed_token(): - return ERC20_MOCK_DEPLOYER.deploy(18) - - @pytest.fixture(scope="module") def amm(get_amm, borrowed_token, collateral_token): return get_amm(collateral_token, borrowed_token) diff --git a/tests/amm/test_oracle_change_noloss.py b/tests/amm/test_oracle_change_noloss.py index ccb94d36..c307bd82 100644 --- a/tests/amm/test_oracle_change_noloss.py +++ b/tests/amm/test_oracle_change_noloss.py @@ -8,11 +8,6 @@ from tests.utils.deployers import ERC20_MOCK_DEPLOYER -@pytest.fixture(scope="module") -def borrowed_token(): - return ERC20_MOCK_DEPLOYER.deploy(18) - - @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_xdown_yup_invariants_dy.py b/tests/amm/test_xdown_yup_invariants_dy.py index 96b184cf..4c055aa3 100644 --- a/tests/amm/test_xdown_yup_invariants_dy.py +++ b/tests/amm/test_xdown_yup_invariants_dy.py @@ -11,11 +11,6 @@ """ -@pytest.fixture(scope="module") -def borrowed_token(): - return ERC20_MOCK_DEPLOYER.deploy(18) - - @pytest.fixture(scope="module") def amm(get_amm, borrowed_token, collateral_token): return get_amm(collateral_token, borrowed_token) diff --git a/tests/lending/test_secondary_mpolicy.py b/tests/lending/test_secondary_mpolicy.py index 350f8a30..526218a8 100644 --- a/tests/lending/test_secondary_mpolicy.py +++ b/tests/lending/test_secondary_mpolicy.py @@ -35,11 +35,6 @@ def amm(): return MOCK_RATE_SETTER_DEPLOYER.deploy(RATE0) -@pytest.fixture(scope="module") -def borrowed_token(): - return ERC20_MOCK_DEPLOYER.deploy(18) - - @pytest.fixture(scope="module") def mp(factory, amm, borrowed_token): return SECONDARY_MONETARY_POLICY_DEPLOYER.deploy(factory, amm, borrowed_token, From cd3b28ad3abb364468827f191b524fe69ce959b6 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 14:51:23 +0200 Subject: [PATCH 156/413] chore: remove deprecated contracts --- contracts/zaps/deprecated/DeleverageZap.vy | 105 ------ contracts/zaps/deprecated/LeverageZap.vy | 329 ----------------- .../zaps/deprecated/LeverageZapNewRouter.vy | 328 ----------------- .../zaps/deprecated/LeverageZapSfrxETH.vy | 341 ------------------ .../zaps/deprecated/LeverageZapWstETH.vy | 341 ------------------ 5 files changed, 1444 deletions(-) delete mode 100644 contracts/zaps/deprecated/DeleverageZap.vy delete mode 100644 contracts/zaps/deprecated/LeverageZap.vy delete mode 100644 contracts/zaps/deprecated/LeverageZapNewRouter.vy delete mode 100644 contracts/zaps/deprecated/LeverageZapSfrxETH.vy delete mode 100644 contracts/zaps/deprecated/LeverageZapWstETH.vy 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] From da5718becb678053cd46c4caa8fc153ae0cb02ed Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 14:51:56 +0200 Subject: [PATCH 157/413] test: protocol can deploy custom mp --- tests/utils/deploy.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/tests/utils/deploy.py b/tests/utils/deploy.py index bd0aafdc..7d2d9cd5 100644 --- a/tests/utils/deploy.py +++ b/tests/utils/deploy.py @@ -193,7 +193,8 @@ def create_lending_market( price_oracle: VyperContract, name: str, min_borrow_rate: int, - max_borrow_rate: int + max_borrow_rate: int, + mpolicy_deployer: VyperDeployer | None = None, ) -> Dict[str, VyperContract]: """ Create a new lending market in the Lending Factory. @@ -226,10 +227,25 @@ def create_lending_market( max_borrow_rate ) + vault = VAULT_DEPLOYER.at(result[0]) + controller = LL_CONTROLLER_DEPLOYER.at(result[1]) + amm = AMM_DEPLOYER.at(result[2]) + + # Optionally override the market's monetary policy after creation. + # By default, factory uses self.blueprints.mpolicy (Semilog policy). + if mpolicy_deployer is not None: + with boa.env.prank(self.admin): + custom_mp = mpolicy_deployer.deploy( + borrowed_token.address, + min_borrow_rate, + max_borrow_rate, + ) + controller.set_monetary_policy(custom_mp) + return { - 'vault': VAULT_DEPLOYER.at(result[0]), - 'controller': LL_CONTROLLER_DEPLOYER.at(result[1]), - 'amm': AMM_DEPLOYER.at(result[2]) + 'vault': vault, + 'controller': controller, + 'amm': amm, } @@ -265,4 +281,4 @@ def create_lending_market( name="Test Vault", min_borrow_rate=5 * 10**15 // (365 * 86400), # 0.5% APR max_borrow_rate=50 * 10**16 // (365 * 86400) # 50% APR - ) \ No newline at end of file + ) From d3228f8625db225837f30f771a1115dedeb89e38 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 14:52:05 +0200 Subject: [PATCH 158/413] test: simplify zap fixture --- tests/zaps/conftest.py | 71 +++--------------------------------------- 1 file changed, 5 insertions(+), 66 deletions(-) diff --git a/tests/zaps/conftest.py b/tests/zaps/conftest.py index 88a4433b..fba83ee3 100644 --- a/tests/zaps/conftest.py +++ b/tests/zaps/conftest.py @@ -1,7 +1,7 @@ import boa import pytest -from tests.utils.deploy import Blueprints, Protocol +from tests.utils.deploy import Protocol from tests.utils.deployers import ( # Core contracts @@ -44,71 +44,10 @@ def borrowed_token(tokens_for_vault): @pytest.fixture(scope="module") -def lending_market(borrowed_token, collateral_token, price_oracle, admin): - - class ProtocolWithConstantMP(Protocol): - def __init__(self, initial_price: int = 3000 * 10**18): - self.admin = admin - self.fee_receiver = admin - - # Deploy all blueprints - self.blueprints = Blueprints( - amm=AMM_DEPLOYER, - mint_controller=MINT_CONTROLLER_DEPLOYER, - ll_controller=LL_CONTROLLER_DEPLOYER, - ll_controller_view=LL_CONTROLLER_VIEW_DEPLOYER, - price_oracle=CRYPTO_FROM_POOL_DEPLOYER, - mpolicy=CONSTANT_MONETARY_POLICY_LENDING_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 vault implementation - self.vault_impl = VAULT_DEPLOYER.deploy() - - # Deploy lending factory - self.lending_factory = LENDING_FACTORY_DEPLOYER.deploy( - self.blueprints.amm.address, - self.blueprints.ll_controller.address, - self.vault_impl.address, - self.blueprints.price_oracle.address, - self.blueprints.ll_controller_view.address, - self.blueprints.mpolicy.address, - self.admin, - self.fee_receiver, - ) - +def lending_market(proto, borrowed_token, collateral_token, price_oracle, admin): + """Create a lending market using a constant-rate lending policy via Protocol hook.""" with boa.env.prank(admin): - result = ProtocolWithConstantMP().create_lending_market( + return proto.create_lending_market( borrowed_token=borrowed_token, collateral_token=collateral_token, A=100, @@ -119,8 +58,8 @@ def __init_lend_markets(self): name="Test vault", min_borrow_rate=int(0.005 * 1e18) // (365 * 86400), # 0.5% APR max_borrow_rate=int(0.5 * 1e18) // (365 * 86400), # 50% APR + mpolicy_deployer=CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER, ) - return result @pytest.fixture(scope="module") From efb0b3d46b3ecc3691530abb147662cc43696dc0 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 14:52:29 +0200 Subject: [PATCH 159/413] feat: add token_lib --- contracts/Controller.vy | 4 +++- contracts/lib/token_lib.vy | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 contracts/lib/token_lib.vy diff --git a/contracts/Controller.vy b/contracts/Controller.vy index a923adc5..8da767bf 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -570,6 +570,7 @@ def calculate_debt_n1( @internal def transferFrom(token: IERC20, _from: address, _to: address, amount: uint256): + # TODO: use contracts.lib.token_lib.transferFrom if amount > 0: assert extcall token.transferFrom( _from, _to, amount, default_return_value=True @@ -578,6 +579,7 @@ def transferFrom(token: IERC20, _from: address, _to: address, amount: uint256): @internal def transfer(token: IERC20, _to: address, amount: uint256): + # TODO: use contracts.lib.token_lib.transfer if amount > 0: assert extcall token.transfer(_to, amount, default_return_value=True) @@ -1349,7 +1351,7 @@ def user_state(user: address) -> uint256[4]: def set_amm_fee(fee: uint256): """ @notice Set the AMM fee (factory admin only) - @dev Reentrant because AMM is nonreentrant TODO check this one + @dev Reentrant because AMM is nonreentrant @param fee The fee which should be no higher than MAX_AMM_FEE """ self._check_admin() diff --git a/contracts/lib/token_lib.vy b/contracts/lib/token_lib.vy new file mode 100644 index 00000000..0973a195 --- /dev/null +++ b/contracts/lib/token_lib.vy @@ -0,0 +1,19 @@ +from ethereum.ercs import IERC20 + + +@internal +def max_approve(token: IERC20, spender: address): + if staticcall token.allowance(self, spender) == 0: + assert extcall token.approve(spender, max_value(uint256), default_return_value=True) + + +@internal +def transferFrom(token: IERC20, _from: address, _to: address, amount: uint256): + if amount > 0: + assert extcall token.transferFrom(_from, _to, amount, default_return_value=True) + + +@internal +def transfer(token: IERC20, _to: address, amount: uint256): + if amount > 0: + assert extcall token.transfer(_to, amount, default_return_value=True) From 3ef85dcf52464bfce94c1b8ba6e167427757a465 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 14:52:47 +0200 Subject: [PATCH 160/413] feat: add liquidation lib --- contracts/ControllerView.vy | 23 ++-------------- contracts/lib/liquidation_lib.vy | 45 ++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 21 deletions(-) create mode 100644 contracts/lib/liquidation_lib.vy diff --git a/contracts/ControllerView.vy b/contracts/ControllerView.vy index deac1466..1e2cb1e8 100644 --- a/contracts/ControllerView.vy +++ b/contracts/ControllerView.vy @@ -16,6 +16,7 @@ from ethereum.ercs import IERC20 from ethereum.ercs import IERC20Detailed from contracts import Controller as core +import contracts.lib.liquidation_lib as liq from contracts import constants as c from snekmate.utils import math @@ -211,27 +212,7 @@ def users_to_liquidate( """ @notice Natspec for this function is available in its controller contract """ - n_loans: uint256 = self._n_loans() - limit: uint256 = _limit - if _limit == 0: - limit = 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 = self._loans(ix) - debt: uint256 = self._debt(user) - health: int256 = self._health(user, True) - if health < 0: - xy: uint256[2] = staticcall AMM.get_sum_xy(user) - out.append( - IController.Position( - user=user, x=xy[0], y=xy[1], debt=debt, health=health - ) - ) - ix += 1 - return out + return liq.users_with_health(IController(self), _from, _limit, 0, False, empty(address), True) @view diff --git a/contracts/lib/liquidation_lib.vy b/contracts/lib/liquidation_lib.vy new file mode 100644 index 00000000..763a0bc4 --- /dev/null +++ b/contracts/lib/liquidation_lib.vy @@ -0,0 +1,45 @@ +from contracts.interfaces import IMintController as 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 IMintController.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 + From 4eb438ccfe3c589577daff707a7190adaf9fe841 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 14:53:08 +0200 Subject: [PATCH 161/413] feat: add kwargs support for repay --- contracts/interfaces/ILlamalendController.vyi | 10 +++++++++- contracts/interfaces/IMintController.vyi | 8 +++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/contracts/interfaces/ILlamalendController.vyi b/contracts/interfaces/ILlamalendController.vyi index 8c51c0c4..eac1487a 100644 --- a/contracts/interfaces/ILlamalendController.vyi +++ b/contracts/interfaces/ILlamalendController.vyi @@ -7,6 +7,8 @@ from contracts.interfaces import IMonetaryPolicy from contracts.interfaces import IFactory from contracts.interfaces import IMintController +# TODO only keep diff + # Events event Repay: @@ -134,7 +136,13 @@ def set_amm_fee(fee: uint256): @external -def repay(_d_debt: uint256, _for: address, max_active_band: int256, callbacker: address, calldata: Bytes[10000]): +def repay( + _d_debt: uint256, + _for: address = msg.sender, + max_active_band: int256 = max_value(int256), + callbacker: address = empty(address), + calldata: Bytes[10000] = b"", +): ... diff --git a/contracts/interfaces/IMintController.vyi b/contracts/interfaces/IMintController.vyi index 5b435dd4..843d2745 100644 --- a/contracts/interfaces/IMintController.vyi +++ b/contracts/interfaces/IMintController.vyi @@ -155,7 +155,13 @@ def set_amm_fee(fee: uint256): @external -def repay(_d_debt: uint256, _for: address, max_active_band: int256, callbacker: address, calldata: Bytes[10000]): +def repay( + _d_debt: uint256, + _for: address = msg.sender, + max_active_band: int256 = max_value(int256), + callbacker: address = empty(address), + calldata: Bytes[10000] = b"", +): ... From a2022c9bbfc05a6c1ad593f77b0600e8f697cdfd Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 14:53:27 +0200 Subject: [PATCH 162/413] feat: add partial repay interface --- contracts/interfaces/IPartialRepayZap.vyi | 42 +++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 contracts/interfaces/IPartialRepayZap.vyi diff --git a/contracts/interfaces/IPartialRepayZap.vyi b/contracts/interfaces/IPartialRepayZap.vyi new file mode 100644 index 00000000..e2e292b7 --- /dev/null +++ b/contracts/interfaces/IPartialRepayZap.vyi @@ -0,0 +1,42 @@ +from contracts.interfaces import ILlamalendController as IController + + +event PartialRepay: + controller: address + 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: address, _from: uint256, _limit: uint256) -> DynArray[Position, 1000]: + ... + + +@external +def liquidate_partial(_controller: address, _user: address, _min_x: uint256): + ... + + +@view +@external +def FRAC() -> uint256: + ... + + +@view +@external +def HEALTH_THRESHOLD() -> int256: + ... + From 474e6450290ad85c278a3b9fb6e41e42b10f7a95 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 14:54:12 +0200 Subject: [PATCH 163/413] chore: add TODO --- contracts/zaps/LeverageZap.vy | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/zaps/LeverageZap.vy b/contracts/zaps/LeverageZap.vy index 2330453d..67d519cc 100644 --- a/contracts/zaps/LeverageZap.vy +++ b/contracts/zaps/LeverageZap.vy @@ -268,12 +268,14 @@ 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) From 64e752f4f55a1f3b1751238f9b866a97f28fffb2 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 14:54:31 +0200 Subject: [PATCH 164/413] perf: remove codesize opt --- contracts/zaps/PartialRepayZap.vy | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/zaps/PartialRepayZap.vy b/contracts/zaps/PartialRepayZap.vy index 82ce4bcc..ae56c4af 100644 --- a/contracts/zaps/PartialRepayZap.vy +++ b/contracts/zaps/PartialRepayZap.vy @@ -1,6 +1,5 @@ # pragma version 0.4.3 # pragma nonreentrancy on -# pragma optimize codesize """ @title LlamaLendPartialRepayZap From f2f4e20eb5301f3601b4d2478f4808da9a890f1e Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 14:54:41 +0200 Subject: [PATCH 165/413] docs: compiler limitation --- contracts/zaps/PartialRepayZap.vy | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/zaps/PartialRepayZap.vy b/contracts/zaps/PartialRepayZap.vy index ae56c4af..26e23127 100644 --- a/contracts/zaps/PartialRepayZap.vy +++ b/contracts/zaps/PartialRepayZap.vy @@ -15,6 +15,7 @@ from contracts.interfaces import ILlamalendController as IController from contracts import constants as c +# https://github.com/vyperlang/vyper/issues/4723 WAD: constant(uint256) = c.WAD FRAC: public(immutable(uint256)) # fraction of position to repay (1e18 = 100%) From df40fc1118100f5ba62ce99736d959aee32ed03e Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 14:56:57 +0200 Subject: [PATCH 166/413] ci: add zaps test --- .github/workflows/test.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c678a2cf..6b912823 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -18,6 +18,7 @@ jobs: - "tests/controller" - "tests/amm" - "tests/price_oracles" + - "tests/zaps" # - "tests/lending" # - "tests/stableborrow" # - "tests/swap" From 7c3477545f075cd71da4b6c59667f1881109aa01 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 15:01:03 +0200 Subject: [PATCH 167/413] fix: rename PartialRepay event field to surplus_repaid --- contracts/zaps/PartialRepayZap.vy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/zaps/PartialRepayZap.vy b/contracts/zaps/PartialRepayZap.vy index 26e23127..dcfb4805 100644 --- a/contracts/zaps/PartialRepayZap.vy +++ b/contracts/zaps/PartialRepayZap.vy @@ -36,7 +36,7 @@ event PartialRepay: user: address collateral_decrease: uint256 borrowed_from_sender: uint256 - sulprus_repayed: uint256 + surplus_repaid: uint256 @deploy @@ -159,7 +159,7 @@ def liquidate_partial(_controller: address, _user: address, _min_x: uint256): collateral_received: uint256 = staticcall COLLATERAL.balanceOf(self) self._transfer(COLLATERAL, msg.sender, collateral_received) - # sulprus amount goes into position repay + # surplus amount goes into position repay borrowed_amount: uint256 = staticcall BORROWED.balanceOf(self) extcall CONTROLLER.repay(borrowed_amount, _user, max_value(int256), empty(address), b"") @@ -168,5 +168,5 @@ def liquidate_partial(_controller: address, _user: address, _min_x: uint256): user=_user, collateral_decrease=collateral_received, borrowed_from_sender=borrowed_from_sender, - sulprus_repayed=borrowed_amount, + surplus_repaid=borrowed_amount, ) From fd3c6077f1e3ee6662bc45bf41cc88ace7641256 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 15:02:18 +0200 Subject: [PATCH 168/413] feat: implement IPartialRepayZap and use interface types --- contracts/zaps/PartialRepayZap.vy | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/contracts/zaps/PartialRepayZap.vy b/contracts/zaps/PartialRepayZap.vy index dcfb4805..ae1b4844 100644 --- a/contracts/zaps/PartialRepayZap.vy +++ b/contracts/zaps/PartialRepayZap.vy @@ -12,9 +12,12 @@ from ethereum.ercs import IERC20 from contracts.interfaces import IAMM from contracts.interfaces import ILlamalendController as IController +from contracts.interfaces import IPartialRepayZap as IZap from contracts import constants as c +implements: IZap + # https://github.com/vyperlang/vyper/issues/4723 WAD: constant(uint256) = c.WAD @@ -22,21 +25,7 @@ FRAC: public(immutable(uint256)) # fraction of position HEALTH_THRESHOLD: public(immutable(int256)) # trigger threshold on controller.health(user, false) -struct Position: - user: address - x: uint256 - y: uint256 - health: int256 - dx: uint256 # collateral estimated to be withdrawn for FRAC - dy: uint256 # borrowed needed for FRAC - - -event PartialRepay: - controller: address - user: address - collateral_decrease: uint256 - borrowed_from_sender: uint256 - surplus_repaid: uint256 + @deploy @@ -86,7 +75,7 @@ def _get_f_remove(frac: uint256, health_limit: uint256) -> uint256: @external @view -def users_to_liquidate(_controller: address, _from: uint256 = 0, _limit: uint256 = 0) -> DynArray[Position, 1000]: +def users_to_liquidate(_controller: address, _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 @@ -100,7 +89,7 @@ def users_to_liquidate(_controller: address, _from: uint256 = 0, _limit: uint256 n_loans: uint256 = staticcall CONTROLLER.n_loans() limit: uint256 = _limit if _limit != 0 else n_loans ix: uint256 = _from - out: DynArray[Position, 1000] = [] + out: DynArray[IZap.Position, 1000] = [] for i: uint256 in range(10**6): if ix >= n_loans or i == limit: break @@ -112,7 +101,7 @@ def users_to_liquidate(_controller: address, _from: uint256 = 0, _limit: uint256 total_debt: uint256 = staticcall CONTROLLER.debt(user) x_down: uint256 = staticcall AMM.get_x_down(user) ratio: uint256 = unsafe_div(unsafe_mul(x_down, 10 ** 18), total_debt) - out.append(Position( + out.append(IZap.Position( user=user, x=xy[0], y=xy[1], @@ -163,7 +152,7 @@ def liquidate_partial(_controller: address, _user: address, _min_x: uint256): borrowed_amount: uint256 = staticcall BORROWED.balanceOf(self) extcall CONTROLLER.repay(borrowed_amount, _user, max_value(int256), empty(address), b"") - log PartialRepay( + log IZap.PartialRepay( controller=_controller, user=_user, collateral_decrease=collateral_received, From a4d43d0095e7502b717f6c4d47a425c0d48d725a Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 15:03:06 +0200 Subject: [PATCH 169/413] refactor: use token_lib for ERC20 helpers --- contracts/zaps/PartialRepayZap.vy | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/contracts/zaps/PartialRepayZap.vy b/contracts/zaps/PartialRepayZap.vy index ae1b4844..0af54733 100644 --- a/contracts/zaps/PartialRepayZap.vy +++ b/contracts/zaps/PartialRepayZap.vy @@ -37,22 +37,7 @@ def __init__( HEALTH_THRESHOLD = _health_threshold -@internal -def _approve(token: IERC20, spender: address): - if staticcall token.allowance(self, spender) == 0: - assert extcall token.approve(spender, max_value(uint256), default_return_value=True) - - -@internal -def _transferFrom(token: IERC20, _from: address, _to: address, amount: uint256): - if amount > 0: - assert extcall token.transferFrom(_from, _to, amount, default_return_value=True) - - -@internal -def _transfer(token: IERC20, _to: address, amount: uint256): - if amount > 0: - assert extcall token.transfer(_to, amount, default_return_value=True) +import contracts.lib.token_lib as tkn @internal @@ -130,7 +115,7 @@ def liquidate_partial(_controller: address, _user: address, _min_x: uint256): assert staticcall CONTROLLER.approval(_user, self), "not approved" assert staticcall CONTROLLER.health(_user, False) < HEALTH_THRESHOLD, "health too high" - self._approve(BORROWED, _controller) + tkn.max_approve(BORROWED, _controller) total_debt: uint256 = staticcall CONTROLLER.debt(_user) x_down: uint256 = staticcall AMM.get_x_down(_user) @@ -142,11 +127,11 @@ def liquidate_partial(_controller: address, _user: address, _min_x: uint256): to_repay: uint256 = staticcall CONTROLLER.tokens_to_liquidate(_user, FRAC) borrowed_from_sender: uint256 = unsafe_div(unsafe_mul(to_repay, ratio), 10 ** 18) - self._transferFrom(BORROWED, msg.sender, self, borrowed_from_sender) + tkn.transferFrom(BORROWED, msg.sender, self, borrowed_from_sender) extcall CONTROLLER.liquidate(_user, _min_x, FRAC, empty(address), b"") collateral_received: uint256 = staticcall COLLATERAL.balanceOf(self) - self._transfer(COLLATERAL, msg.sender, collateral_received) + tkn.transfer(COLLATERAL, msg.sender, collateral_received) # surplus amount goes into position repay borrowed_amount: uint256 = staticcall BORROWED.balanceOf(self) From 38274290f2db87d5b4a3b49dcf47efd038bd53e9 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 15:04:00 +0200 Subject: [PATCH 170/413] refactor: reuse Controller._get_f_remove via module import --- contracts/zaps/PartialRepayZap.vy | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/zaps/PartialRepayZap.vy b/contracts/zaps/PartialRepayZap.vy index 0af54733..a6265a20 100644 --- a/contracts/zaps/PartialRepayZap.vy +++ b/contracts/zaps/PartialRepayZap.vy @@ -12,6 +12,7 @@ from ethereum.ercs import IERC20 from contracts.interfaces import IAMM from contracts.interfaces import ILlamalendController as IController +from contracts import Controller as ctrl from contracts.interfaces import IPartialRepayZap as IZap from contracts import constants as c @@ -91,8 +92,8 @@ def users_to_liquidate(_controller: address, _from: uint256 = 0, _limit: uint256 x=xy[0], y=xy[1], health=h, - dx=unsafe_div(unsafe_mul(to_repay, ratio), 10 ** 18), - dy=unsafe_div(xy[1] * self._get_f_remove(FRAC, 0), WAD), + dx=unsafe_div(xy[1] * ctrl._get_f_remove(FRAC, 0), WAD), + dy=unsafe_div(unsafe_mul(to_repay, ratio), 10 ** 18), )) ix += 1 return out From 1ccc49cf413fcfce4cf9eb204673856456be4e80 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 15:04:41 +0200 Subject: [PATCH 171/413] refactor: use IMintController.Position as canonical position type --- contracts/zaps/PartialRepayZap.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/zaps/PartialRepayZap.vy b/contracts/zaps/PartialRepayZap.vy index a6265a20..afdeb79c 100644 --- a/contracts/zaps/PartialRepayZap.vy +++ b/contracts/zaps/PartialRepayZap.vy @@ -11,7 +11,7 @@ from ethereum.ercs import IERC20 from contracts.interfaces import IAMM -from contracts.interfaces import ILlamalendController as IController +from contracts.interfaces import IMintController as IController from contracts import Controller as ctrl from contracts.interfaces import IPartialRepayZap as IZap From 51d7e3151733f73bab2869b0d4ffce2f0252462f Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 15:05:23 +0200 Subject: [PATCH 172/413] refactor: use WAD constant instead of 1e18 literals --- contracts/zaps/PartialRepayZap.vy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/zaps/PartialRepayZap.vy b/contracts/zaps/PartialRepayZap.vy index afdeb79c..bba69240 100644 --- a/contracts/zaps/PartialRepayZap.vy +++ b/contracts/zaps/PartialRepayZap.vy @@ -86,14 +86,14 @@ def users_to_liquidate(_controller: address, _from: uint256 = 0, _limit: uint256 to_repay: uint256 = staticcall CONTROLLER.tokens_to_liquidate(user, FRAC) total_debt: uint256 = staticcall CONTROLLER.debt(user) x_down: uint256 = staticcall AMM.get_x_down(user) - ratio: uint256 = unsafe_div(unsafe_mul(x_down, 10 ** 18), total_debt) + ratio: uint256 = unsafe_div(unsafe_mul(x_down, WAD), total_debt) out.append(IZap.Position( user=user, x=xy[0], y=xy[1], health=h, dx=unsafe_div(xy[1] * ctrl._get_f_remove(FRAC, 0), WAD), - dy=unsafe_div(unsafe_mul(to_repay, ratio), 10 ** 18), + dy=unsafe_div(unsafe_mul(to_repay, ratio), WAD), )) ix += 1 return out From 7d679aef16618f6f6b1f73e0b1e2634383542b08 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 15:06:05 +0200 Subject: [PATCH 173/413] refactor: replace remaining 1e18 math with WAD in ratio/borrow math --- contracts/zaps/PartialRepayZap.vy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/zaps/PartialRepayZap.vy b/contracts/zaps/PartialRepayZap.vy index bba69240..cb0c9a06 100644 --- a/contracts/zaps/PartialRepayZap.vy +++ b/contracts/zaps/PartialRepayZap.vy @@ -120,13 +120,13 @@ def liquidate_partial(_controller: address, _user: address, _min_x: uint256): total_debt: uint256 = staticcall CONTROLLER.debt(_user) x_down: uint256 = staticcall AMM.get_x_down(_user) - ratio: uint256 = unsafe_div(unsafe_mul(x_down, 10 ** 18), total_debt) + ratio: uint256 = unsafe_div(unsafe_mul(x_down, WAD), total_debt) - assert ratio > 10 ** 18, "position rekt" + 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), 10 ** 18) + borrowed_from_sender: uint256 = unsafe_div(unsafe_mul(to_repay, ratio), WAD) tkn.transferFrom(BORROWED, msg.sender, self, borrowed_from_sender) From 2658e2be12378b4f8d5898596a6aa93d0fd4d77d Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 15:07:03 +0200 Subject: [PATCH 174/413] refactor: simplify repay call by relying on default args --- contracts/zaps/PartialRepayZap.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/zaps/PartialRepayZap.vy b/contracts/zaps/PartialRepayZap.vy index cb0c9a06..e51c3399 100644 --- a/contracts/zaps/PartialRepayZap.vy +++ b/contracts/zaps/PartialRepayZap.vy @@ -136,7 +136,7 @@ def liquidate_partial(_controller: address, _user: address, _min_x: uint256): # surplus amount goes into position repay borrowed_amount: uint256 = staticcall BORROWED.balanceOf(self) - extcall CONTROLLER.repay(borrowed_amount, _user, max_value(int256), empty(address), b"") + extcall CONTROLLER.repay(borrowed_amount, _user) log IZap.PartialRepay( controller=_controller, From 722fd25a71d5d9ea896415cc61b1448381cdc856 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 1 Sep 2025 15:08:54 +0200 Subject: [PATCH 175/413] refactor: remove duplicated code --- contracts/zaps/PartialRepayZap.vy | 66 ++++++++++--------------------- 1 file changed, 21 insertions(+), 45 deletions(-) diff --git a/contracts/zaps/PartialRepayZap.vy b/contracts/zaps/PartialRepayZap.vy index e51c3399..3765c053 100644 --- a/contracts/zaps/PartialRepayZap.vy +++ b/contracts/zaps/PartialRepayZap.vy @@ -13,7 +13,9 @@ from ethereum.ercs import IERC20 from contracts.interfaces import IAMM from contracts.interfaces import IMintController as IController from contracts import Controller as ctrl +import contracts.lib.token_lib as tkn from contracts.interfaces import IPartialRepayZap as IZap +import contracts.lib.liquidation_lib as liq from contracts import constants as c @@ -26,9 +28,6 @@ FRAC: public(immutable(uint256)) # fraction of position HEALTH_THRESHOLD: public(immutable(int256)) # trigger threshold on controller.health(user, false) - - - @deploy def __init__( _frac: uint256, # e.g. 5e16 == 5% @@ -38,27 +37,6 @@ def __init__( HEALTH_THRESHOLD = _health_threshold -import contracts.lib.token_lib as tkn - - -@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 = 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 - - @external @view def users_to_liquidate(_controller: address, _from: uint256 = 0, _limit: uint256 = 0) -> DynArray[IZap.Position, 1000]: @@ -72,30 +50,28 @@ def users_to_liquidate(_controller: address, _from: uint256 = 0, _limit: uint256 CONTROLLER: IController = IController(_controller) AMM: IAMM = staticcall CONTROLLER.amm() - n_loans: uint256 = staticcall CONTROLLER.n_loans() - limit: uint256 = _limit if _limit != 0 else n_loans - ix: uint256 = _from + 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(10**6): - if ix >= n_loans or i == limit: + for i: uint256 in range(1000): + if i == len(base_positions): break - user: address = staticcall CONTROLLER.loans(ix) - h: int256 = staticcall CONTROLLER.health(user, False) - if staticcall CONTROLLER.approval(user, self) and h < HEALTH_THRESHOLD: - xy: uint256[2] = staticcall AMM.get_sum_xy(user) - to_repay: uint256 = staticcall CONTROLLER.tokens_to_liquidate(user, FRAC) - total_debt: uint256 = staticcall CONTROLLER.debt(user) - x_down: uint256 = staticcall AMM.get_x_down(user) - ratio: uint256 = unsafe_div(unsafe_mul(x_down, WAD), total_debt) - out.append(IZap.Position( - user=user, - x=xy[0], - y=xy[1], - health=h, - dx=unsafe_div(xy[1] * ctrl._get_f_remove(FRAC, 0), WAD), + pos: IController.Position = base_positions[i] + # Compute zap-specific estimates + to_repay: uint256 = staticcall CONTROLLER.tokens_to_liquidate(pos.user, FRAC) + x_down: uint256 = staticcall AMM.get_x_down(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), - )) - ix += 1 + ) + ) return out From c24d5331017fa36d3a69a0fb574a7efe9035a72a Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 2 Sep 2025 12:02:31 +0200 Subject: [PATCH 176/413] fix: use ln --- contracts/lending/LendingFactory.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/lending/LendingFactory.vy b/contracts/lending/LendingFactory.vy index 967bdc8b..f71b1e78 100644 --- a/contracts/lending/LendingFactory.vy +++ b/contracts/lending/LendingFactory.vy @@ -139,7 +139,7 @@ def _create( self.amm_impl, borrowed_token, 10**convert(18 - staticcall IERC20Detailed(borrowed_token).decimals(), uint256), collateral_token, 10**convert(18 - staticcall IERC20Detailed(collateral_token).decimals(), uint256), - A, isqrt(A_ratio * 10**18), math._log2(A_ratio, False), + A, isqrt(A_ratio * 10**18), math._wad_ln(convert(A_ratio, int256)), p, fee, convert(0, uint256), price_oracle, code_offset=3) controller: address = create_from_blueprint( From 0e8d416c6dd7185ef285488c1f40594d1721be1a Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 2 Sep 2025 16:48:56 +0200 Subject: [PATCH 177/413] test: simplify fixtures --- tests/conftest.py | 160 +++++++++++++++++- tests/controller/test_set_price_oracle.py | 49 ------ tests/lending/conftest.py | 14 +- tests/lending/test_bigfuzz.py | 6 +- tests/lending/test_health_in_trades.py | 6 +- tests/lending/test_monetary_policy.py | 4 +- tests/lending/test_shifted_trades.py | 2 +- .../lending/test_st_interest_conservation.py | 8 +- tests/lending/test_vault.py | 6 +- tests/utils/deploy.py | 20 ++- tests/zaps/conftest.py | 93 +--------- .../test_partial_repay_zap.py | 44 +++-- 12 files changed, 220 insertions(+), 192 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 9e38133c..af1f682b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,8 +6,9 @@ from hypothesis import settings from tests.utils.deploy import Protocol from tests.utils.deployers import ( - ERC20_MOCK_DEPLOYER, - DUMMY_PRICE_ORACLE_DEPLOYER + ERC20_MOCK_DEPLOYER, + CONSTANT_MONETARY_POLICY_DEPLOYER, + CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER, ) @@ -42,6 +43,40 @@ def factory(proto): def stablecoin(proto): return proto.crvUSD +@pytest.fixture(scope="module") +def collateral_token(): + # TODO hook decimals fixture + return ERC20_MOCK_DEPLOYER.deploy(18) + + +@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 + return stablecoin + + +@pytest.fixture(scope="module") +def lending_mpolicy_deployer(): + """Default monetary policy deployer for lending markets: Constant policy. + Tests can override to use a different lending policy. + """ + return CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER + + +@pytest.fixture(scope="module") +def monetary_policy(market_type, controller): + """Monetary policy contract for the current market. + Test modules can override by replacing the policy on the controller. + """ + if market_type == "lending": + # TODO make both controllers use the same policy through a constructor adapter + return CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER.at(controller.monetary_policy()) + else: + return CONSTANT_MONETARY_POLICY_DEPLOYER.at(controller.monetary_policy()) + @pytest.fixture(scope="module") def price_oracle(proto): @@ -73,6 +108,123 @@ 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 1000 + + +@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, + lending_mpolicy_deployer, + 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=proto.mint_monetary_policy, + A=amm_A, + amm_fee=amm_fee, + loan_discount=loan_discount, + liquidation_discount=liquidation_discount, + debt_ceiling=seed_liquidity, + ) + else: + 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_mpolicy_deployer, + ) + + +@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): + 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 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") @@ -96,6 +248,4 @@ def token_mock(): return ERC20_MOCK_DEPLOYER -@pytest.fixture(scope="module") -def collateral_token(): - return ERC20_MOCK_DEPLOYER.deploy(18) + diff --git a/tests/controller/test_set_price_oracle.py b/tests/controller/test_set_price_oracle.py index 061f71f3..15167ab7 100644 --- a/tests/controller/test_set_price_oracle.py +++ b/tests/controller/test_set_price_oracle.py @@ -13,53 +13,6 @@ def decimals(): # change behavior with different decimals return 18 -@pytest.fixture() -def mint_market(proto, collat): - return proto.create_mint_market( - collat, - proto.price_oracle, - proto.mint_monetary_policy, - A=1000, - amm_fee=10**16, - admin_fee=0, - loan_discount= int(0.09 * 10**18), - liquidation_discount=int(0.06 * 10**18), - debt_ceiling=1000 * 10**18 - ) - - -@pytest.fixture() -def lend_market(proto, collat): - return proto.create_lending_market( - borrowed_token=proto.crvUSD, # TODO param other tokens - collateral_token=collat, - A=1000, - fee=10**16, - loan_discount=int(0.09 * 10**18), - liquidation_discount=int(0.06 * 10**18), - price_oracle=proto.price_oracle, - name="Test Vault", - min_borrow_rate=10**15 // (365 * 86400), # 0.1% APR (per second) - max_borrow_rate=10**18 // (365 * 86400) # 100% APR (per second) - ) - -@pytest.fixture(params=["mint", "lending"]) -def market(request, mint_market, lend_market): - """Parametrized fixture that provides both mint and lending markets.""" - if request.param == "mint": - return mint_market - else: - return lend_market - -@pytest.fixture() -def controller(market): - """Parametrized controller fixture that works with both market types.""" - return market['controller'] - -@pytest.fixture() -def amm(market): - return market['amm'] - @pytest.fixture(scope="module") def new_oracle(admin): @@ -203,5 +156,3 @@ def test_same_price_different_oracle(controller, admin, amm): # 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/lending/conftest.py b/tests/lending/conftest.py index a44ee364..3bbe317c 100644 --- a/tests/lending/conftest.py +++ b/tests/lending/conftest.py @@ -63,9 +63,19 @@ def market_amm(lending_market): @pytest.fixture(scope="module") -def market_mpolicy(market_controller): +def monetary_policy(market_controller, borrowed_token, admin): from tests.utils.deployers import SEMILOG_MONETARY_POLICY_DEPLOYER - return SEMILOG_MONETARY_POLICY_DEPLOYER.at(market_controller.monetary_policy()) + # Override default policy with Semilog for lending test suite + min_borrow_rate = int(0.005 * 1e18) // (365 * 86400) # 0.5% APR + max_borrow_rate = int(0.5 * 1e18) // (365 * 86400) # 50% APR + with boa.env.prank(admin): + mp = SEMILOG_MONETARY_POLICY_DEPLOYER.deploy( + borrowed_token.address, + min_borrow_rate, + max_borrow_rate, + ) + market_controller.set_monetary_policy(mp) + return mp @pytest.fixture(scope="module") diff --git a/tests/lending/test_bigfuzz.py b/tests/lending/test_bigfuzz.py index 2d92494c..bf266b7e 100644 --- a/tests/lending/test_bigfuzz.py +++ b/tests/lending/test_bigfuzz.py @@ -409,9 +409,9 @@ 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) + 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): @@ -430,7 +430,7 @@ def minted_redeemed(self): def test_big_fuzz( - vault, borrowed_token, collateral_token, market_mpolicy, accounts, admin, market_amm, market_controller, + vault, borrowed_token, collateral_token, monetary_policy, accounts, admin, market_amm, market_controller, price_oracle, fake_leverage): BigFuzz.TestCase.settings = settings(max_examples=2000, stateful_step_count=20) # Or quick check diff --git a/tests/lending/test_health_in_trades.py b/tests/lending/test_health_in_trades.py index 761d2ea7..af55ccb9 100644 --- a/tests/lending/test_health_in_trades.py +++ b/tests/lending/test_health_in_trades.py @@ -23,7 +23,7 @@ def __init__(self): 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) @@ -89,7 +89,7 @@ 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,7 +98,7 @@ def health(self): assert h > 0 -def test_adiabatic_follow(market_amm, filled_controller, market_mpolicy, collateral_token, borrowed_token, price_oracle, accounts, admin): +def test_adiabatic_follow(market_amm, filled_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) diff --git a/tests/lending/test_monetary_policy.py b/tests/lending/test_monetary_policy.py index 6f84d5ef..bcce303e 100644 --- a/tests/lending/test_monetary_policy.py +++ b/tests/lending/test_monetary_policy.py @@ -8,7 +8,7 @@ @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): +def test_monetary_policy(filled_controller, collateral_token, borrowed_token, monetary_policy, admin, fill): available = borrowed_token.balanceOf(filled_controller) to_borrow = int(fill * available) c_amount = 2 * 3000 * to_borrow * 10**(18 - borrowed_token.decimals()) // 10**(18 - collateral_token.decimals()) @@ -23,7 +23,7 @@ def test_monetary_policy(filled_controller, collateral_token, borrowed_token, ma return else: filled_controller.create_loan(c_amount, to_borrow, 5) - rate = market_mpolicy.rate(filled_controller) + rate = monetary_policy.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 diff --git a/tests/lending/test_shifted_trades.py b/tests/lending/test_shifted_trades.py index 15f6e1e2..ff2839f9 100644 --- a/tests/lending/test_shifted_trades.py +++ b/tests/lending/test_shifted_trades.py @@ -21,7 +21,7 @@ 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(market_amm, filled_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 32c8dda5..7cdbc6d4 100644 --- a/tests/lending/test_st_interest_conservation.py +++ b/tests/lending/test_st_interest_conservation.py @@ -204,9 +204,9 @@ 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): 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): @@ -225,14 +225,14 @@ def debt_payable(self): 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): +def test_stateful_lendborrow(vault, market_amm, market_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, market_amm, market_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_vault.py b/tests/lending/test_vault.py index 03102c2d..e2a74a0d 100644 --- a/tests/lending/test_vault.py +++ b/tests/lending/test_vault.py @@ -9,11 +9,11 @@ DEAD_SHARES = 1000 -def test_vault_creation(vault, market_controller, market_amm, market_mpolicy, factory, price_oracle, +def test_vault_creation(vault, market_controller, market_amm, monetary_policy, 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 + assert market_controller.monetary_policy() == monetary_policy.address n = factory.market_count() assert n > 0 assert factory.vaults(n - 1) == vault.address @@ -22,7 +22,7 @@ def test_vault_creation(vault, market_controller, market_amm, market_mpolicy, fa 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 + assert factory.monetary_policies(n - 1) == monetary_policy.address assert factory.vaults(factory.vaults_index(vault.address)) == vault.address diff --git a/tests/utils/deploy.py b/tests/utils/deploy.py index 7d2d9cd5..9796f098 100644 --- a/tests/utils/deploy.py +++ b/tests/utils/deploy.py @@ -27,7 +27,7 @@ # Monetary policies CONSTANT_MONETARY_POLICY_DEPLOYER, - SEMILOG_MONETARY_POLICY_DEPLOYER, + CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER, # Testing contracts WETH_DEPLOYER, @@ -75,7 +75,7 @@ def __init__( ll_controller=LL_CONTROLLER_DEPLOYER, ll_controller_view=LL_CONTROLLER_VIEW_DEPLOYER, price_oracle=CRYPTO_FROM_POOL_DEPLOYER, - mpolicy=SEMILOG_MONETARY_POLICY_DEPLOYER + mpolicy=CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER ) # Deploy core infrastructure @@ -139,7 +139,6 @@ def create_mint_market( monetary_policy: VyperContract, A: int, amm_fee: int, - admin_fee: int, loan_discount: int, liquidation_discount: int, debt_ceiling: int @@ -153,7 +152,6 @@ def create_mint_market( monetary_policy: Monetary policy contract for this market A: AMM amplification parameter (e.g., 100) fee: Trading fee (e.g., 10**16 for 1%) - admin_fee: Admin fee share (e.g., 0) 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) @@ -165,7 +163,7 @@ def create_mint_market( collateral_token.address, A, amm_fee, - admin_fee, + 0, # admin fee deprecated for mint markets price_oracle.address, monetary_policy.address, loan_discount, @@ -194,6 +192,7 @@ def create_lending_market( name: str, min_borrow_rate: int, max_borrow_rate: int, + seed_amount: int = 1000 * 10**18, mpolicy_deployer: VyperDeployer | None = None, ) -> Dict[str, VyperContract]: """ @@ -224,7 +223,8 @@ def create_lending_market( price_oracle.address, name, min_borrow_rate, - max_borrow_rate + max_borrow_rate, + sender=self.admin ) vault = VAULT_DEPLOYER.at(result[0]) @@ -242,6 +242,13 @@ def create_lending_market( ) controller.set_monetary_policy(custom_mp) + # 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, @@ -260,7 +267,6 @@ def create_lending_market( proto.mint_monetary_policy, A=100, amm_fee=10**16, - admin_fee=0, loan_discount=9 * 10**16, # 9% liquidation_discount=6 * 10**16, # 6% debt_ceiling=10**6 * 10**18 diff --git a/tests/zaps/conftest.py b/tests/zaps/conftest.py index fba83ee3..b1df4e94 100644 --- a/tests/zaps/conftest.py +++ b/tests/zaps/conftest.py @@ -1,95 +1,12 @@ -import boa import pytest -from tests.utils.deploy import Protocol - -from tests.utils.deployers import ( - # Core contracts - STABLECOIN_DEPLOYER, - AMM_DEPLOYER, - MINT_CONTROLLER_DEPLOYER, - CONTROLLER_FACTORY_DEPLOYER, - # Lending contracts - VAULT_DEPLOYER, - LL_CONTROLLER_DEPLOYER, - LL_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, - # Testing contracts - WETH_DEPLOYER, - ERC20_MOCK_DEPLOYER, -) - - -@pytest.fixture(scope="module") -def tokens_for_vault(admin, stablecoin): - with boa.env.prank(admin): - token = ERC20_MOCK_DEPLOYER.deploy(18) - return stablecoin, token - - -@pytest.fixture(scope="module") -def collateral_token(tokens_for_vault): - return tokens_for_vault[1] - - -@pytest.fixture(scope="module") -def borrowed_token(tokens_for_vault): - return tokens_for_vault[0] - - -@pytest.fixture(scope="module") -def lending_market(proto, borrowed_token, collateral_token, price_oracle, admin): - """Create a lending market using a constant-rate lending policy via Protocol hook.""" - with boa.env.prank(admin): - return proto.create_lending_market( - borrowed_token=borrowed_token, - collateral_token=collateral_token, - A=100, - fee=int(0.006 * 1e18), - loan_discount=int(0.09 * 1e18), - liquidation_discount=int(0.06 * 1e18), - price_oracle=price_oracle, - name="Test vault", - min_borrow_rate=int(0.005 * 1e18) // (365 * 86400), # 0.5% APR - max_borrow_rate=int(0.5 * 1e18) // (365 * 86400), # 50% APR - mpolicy_deployer=CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER, - ) - - -@pytest.fixture(scope="module") -def vault(lending_market): - return lending_market["vault"] - - -@pytest.fixture(scope="module") -def market_controller(lending_market, admin): - controller = lending_market["controller"] - with boa.env.prank(admin): - controller.set_borrow_cap(2**256 - 1) - return controller - - -@pytest.fixture(scope="module") -def market_amm(lending_market): - return lending_market["amm"] - @pytest.fixture(scope="module") -def market_mpolicy(market_controller): - return CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER.at(market_controller.monetary_policy()) +def amm_A(): + return 100 @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()) - boa.deal(borrowed_token, admin, amount) - borrowed_token.approve(vault.address, 2**256 - 1) - vault.deposit(amount) - return market_controller +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/test_partial_repay_zap.py b/tests/zaps/partial_liquidation/test_partial_repay_zap.py index a68c8c6b..9a850ff6 100644 --- a/tests/zaps/partial_liquidation/test_partial_repay_zap.py +++ b/tests/zaps/partial_liquidation/test_partial_repay_zap.py @@ -12,10 +12,9 @@ def partial_repay_zap(admin): def controller_for_liquidation( borrowed_token, collateral_token, - filled_controller, - market_amm, - price_oracle, - market_mpolicy, + controller, + amm, + monetary_policy, admin, ): def f(sleep_time, user): @@ -23,53 +22,50 @@ def f(sleep_time, user): collateral_amount = 10**18 with boa.env.prank(admin): - filled_controller.set_amm_fee(10**6) - market_mpolicy.set_rate(int(1e18 * 1.0 / 365 / 86400)) # 100% APY + controller.set_amm_fee(10**6) + monetary_policy.set_rate(int(1e18 * 1.0 / 365 / 86400)) # 100% APY - debt = filled_controller.max_borrowable(collateral_amount, N) + debt = controller.max_borrowable(collateral_amount, N) with boa.env.prank(user): boa.deal(collateral_token, user, collateral_amount) - borrowed_token.approve(market_amm, 2**256 - 1) - borrowed_token.approve(filled_controller, 2**256 - 1) - collateral_token.approve(filled_controller, 2**256 - 1) - filled_controller.create_loan(collateral_amount, debt, N) + 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 = filled_controller.health(user) + health_0 = controller.health(user) # We put mostly USD into AMM, and its quantity remains constant while # interest is accruing. Therefore, we will be at liquidation at some point with boa.env.prank(user): - market_amm.exchange(0, 1, debt, 0) - health_1 = filled_controller.health(user) + amm.exchange(0, 1, debt, 0) + health_1 = controller.health(user) assert health_0 <= health_1 # Earns fees on dynamic fee boa.env.time_travel(sleep_time) - health_2 = filled_controller.health(user) + health_2 = controller.health(user) # Still healthy but liquidation threshold satisfied - assert 0 < health_2 < filled_controller.liquidation_discount() + assert 0 < health_2 < controller.liquidation_discount() with boa.env.prank(admin): # Stop charging fees to have enough coins to liquidate in existence a block before - market_mpolicy.set_rate(0) + monetary_policy.set_rate(0) - return filled_controller + return controller return f @pytest.mark.parametrize("is_approved", [True, False]) def test_users_to_liquidate( - borrowed_token, - collateral_token, controller_for_liquidation, - market_amm, accounts, partial_repay_zap, is_approved, ): user = accounts[1] - controller = controller_for_liquidation(sleep_time=int(36.1 * 86400), user=user) + controller = controller_for_liquidation(sleep_time=int(33 * 86400), user=user) if is_approved: someone_else = str(partial_repay_zap.address) @@ -86,15 +82,13 @@ def test_users_to_liquidate( def test_liquidate_partial( borrowed_token, - collateral_token, controller_for_liquidation, - market_amm, accounts, partial_repay_zap, ): user = accounts[1] liquidator = accounts[2] - controller = controller_for_liquidation(sleep_time=int(36.1 * 86400), user=user) + 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) From 79cdbe0c5d77915c9bd20d5d010bced4c9685ff4 Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 2 Sep 2025 19:58:35 +0200 Subject: [PATCH 178/413] test: fix absolute off by one --- tests/amm/test_xdown_yup_invariants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/amm/test_xdown_yup_invariants.py b/tests/amm/test_xdown_yup_invariants.py index 12839fea..1a5f09f7 100644 --- a/tests/amm/test_xdown_yup_invariants.py +++ b/tests/amm/test_xdown_yup_invariants.py @@ -111,7 +111,7 @@ def test_immediate_above_p0(amm, price_oracle, collateral_token, borrowed_token, 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 y0 == pytest.approx(deposit_amount, rel=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) From 582bf7262d5ef9c80be2dcd67809af53534f9413 Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 2 Sep 2025 19:58:51 +0200 Subject: [PATCH 179/413] chore: remove unstable tests from ci --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 6b912823..11bd27c0 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -16,9 +16,9 @@ jobs: - "tests/flashloan" - "tests/lm_callback" - "tests/controller" - - "tests/amm" - "tests/price_oracles" - "tests/zaps" + # - "tests/amm" # - "tests/lending" # - "tests/stableborrow" # - "tests/swap" From f56c4ca9f6fa4402b07bdb578f916915a356e32c Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 2 Sep 2025 20:05:43 +0200 Subject: [PATCH 180/413] chore: update .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1d71aec1..cb1f7c0e 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ venv .vscode .venv prof -.prof \ No newline at end of file +.prof +.pytest_cache \ No newline at end of file From b91ca376dca7430115b5f7f767e5ef54d3ec1253 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 3 Sep 2025 12:42:39 +0200 Subject: [PATCH 181/413] ci: run all tests --- .github/workflows/test.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 11bd27c0..3cc29fa5 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -11,6 +11,7 @@ jobs: name: ${{ matrix.folder }} (${{ matrix.venom.name }}) runs-on: ubuntu-latest strategy: + fail-fast: false matrix: folder: - "tests/flashloan" @@ -18,10 +19,10 @@ jobs: - "tests/controller" - "tests/price_oracles" - "tests/zaps" - # - "tests/amm" - # - "tests/lending" - # - "tests/stableborrow" - # - "tests/swap" + - "tests/amm" + - "tests/lending" + - "tests/stableborrow" + - "tests/swap" venom: - { name: "standard mode", value: false } # - { name: "venom mode", value: true } From 377c0a77cb53bb18ed905ab149aaf755a59ddef9 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 3 Sep 2025 12:59:31 +0200 Subject: [PATCH 182/413] test: fix fixture scope --- tests/stableborrow/conftest.py | 8 ++++---- tests/swap/conftest.py | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/stableborrow/conftest.py b/tests/stableborrow/conftest.py index a90e9c9d..cc6e7ec6 100644 --- a/tests/stableborrow/conftest.py +++ b/tests/stableborrow/conftest.py @@ -27,7 +27,7 @@ def stablecoin(stablecoin_pre, admin): return stablecoin_pre.deploy('Curve USD', 'crvUSD') -@pytest.fixture(scope="session") +@pytest.fixture(scope="module") def weth(admin): with boa.env.prank(admin): return WETH_DEPLOYER.deploy() @@ -44,13 +44,13 @@ def controller_prefactory(controller_factory_impl, stablecoin, weth, admin, acco return controller_factory_impl.deploy(stablecoin.address, admin, accounts[0], weth.address) -@pytest.fixture(scope="session") +@pytest.fixture(scope="module") def controller_impl(admin): with boa.env.prank(admin): return MINT_CONTROLLER_DEPLOYER.deploy_as_blueprint() -@pytest.fixture(scope="session") +@pytest.fixture(scope="module") def amm_impl(admin): with boa.env.prank(admin): return AMM_DEPLOYER.deploy_as_blueprint() @@ -64,7 +64,7 @@ def controller_factory(controller_prefactory, amm_impl, controller_impl, stablec return controller_prefactory -@pytest.fixture(scope="session") +@pytest.fixture(scope="module") def monetary_policy(admin): with boa.env.prank(admin): policy = CONSTANT_MONETARY_POLICY_DEPLOYER.deploy(admin) diff --git a/tests/swap/conftest.py b/tests/swap/conftest.py index 299acc2d..dcfcf9a3 100644 --- a/tests/swap/conftest.py +++ b/tests/swap/conftest.py @@ -9,32 +9,32 @@ from tests.utils.constants import ZERO_ADDRESS -@pytest.fixture(scope="session") +@pytest.fixture(scope="module") def swap_impl(admin): with boa.env.prank(admin): return STABLESWAP_DEPLOYER.deploy() -@pytest.fixture(scope="session") +@pytest.fixture(scope="module") def swap_deployer(swap_impl, admin): with boa.env.prank(admin): deployer = SWAP_FACTORY_DEPLOYER.deploy(swap_impl.address) return deployer -@pytest.fixture(scope="session") +@pytest.fixture(scope="module") def redeemable_coin(admin): with boa.env.prank(admin): return ERC20_MOCK_DEPLOYER.deploy(6) -@pytest.fixture(scope="session") +@pytest.fixture(scope="module") def volatile_coin(admin): with boa.env.prank(admin): return ERC20_MOCK_DEPLOYER.deploy(18) -@pytest.fixture(scope="session") +@pytest.fixture(scope="module") def swap(swap_deployer, swap_impl, redeemable_coin, volatile_coin, admin): with boa.env.prank(admin): n = swap_deployer.n() @@ -47,7 +47,7 @@ def swap(swap_deployer, swap_impl, redeemable_coin, volatile_coin, admin): return swap -@pytest.fixture(scope="session") +@pytest.fixture(scope="module") def swap_w_d(swap, redeemable_coin, volatile_coin, accounts, admin): with boa.env.prank(admin): boa.deal(redeemable_coin, admin, 10**6 * 10**6) From 9a5bd706d8863daf913efa9ee9e61818c0132a45 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 3 Sep 2025 13:05:33 +0200 Subject: [PATCH 183/413] test: remove out of scope tests These should be in stableswap-ng repo if anything --- .github/workflows/test.yaml | 1 - .gitignore | 3 +- tests/swap/__init__.py | 0 tests/swap/conftest.py | 62 ------------------------------------- tests/swap/test_price.py | 47 ---------------------------- 5 files changed, 2 insertions(+), 111 deletions(-) delete mode 100644 tests/swap/__init__.py delete mode 100644 tests/swap/conftest.py delete mode 100644 tests/swap/test_price.py diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 3cc29fa5..5d963d05 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -22,7 +22,6 @@ jobs: - "tests/amm" - "tests/lending" - "tests/stableborrow" - - "tests/swap" venom: - { name: "standard mode", value: false } # - { name: "venom mode", value: true } diff --git a/.gitignore b/.gitignore index cb1f7c0e..4205667e 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ venv .venv prof .prof -.pytest_cache \ No newline at end of file +.pytest_cache +AGENTS.md \ No newline at end of file diff --git a/tests/swap/__init__.py b/tests/swap/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/swap/conftest.py b/tests/swap/conftest.py deleted file mode 100644 index dcfcf9a3..00000000 --- a/tests/swap/conftest.py +++ /dev/null @@ -1,62 +0,0 @@ -import boa -import pytest -from boa.interpret import VyperContract -from tests.utils.deployers import ( - STABLESWAP_DEPLOYER, - SWAP_FACTORY_DEPLOYER, - ERC20_MOCK_DEPLOYER -) -from tests.utils.constants import ZERO_ADDRESS - - -@pytest.fixture(scope="module") -def swap_impl(admin): - with boa.env.prank(admin): - return STABLESWAP_DEPLOYER.deploy() - - -@pytest.fixture(scope="module") -def swap_deployer(swap_impl, admin): - with boa.env.prank(admin): - deployer = SWAP_FACTORY_DEPLOYER.deploy(swap_impl.address) - return deployer - - -@pytest.fixture(scope="module") -def redeemable_coin(admin): - with boa.env.prank(admin): - return ERC20_MOCK_DEPLOYER.deploy(6) - - -@pytest.fixture(scope="module") -def volatile_coin(admin): - with boa.env.prank(admin): - return ERC20_MOCK_DEPLOYER.deploy(18) - - -@pytest.fixture(scope="module") -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="module") -def swap_w_d(swap, redeemable_coin, volatile_coin, accounts, admin): - with boa.env.prank(admin): - boa.deal(redeemable_coin, admin, 10**6 * 10**6) - boa.deal(volatile_coin, 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 1b84eb77..00000000 --- a/tests/swap/test_price.py +++ /dev/null @@ -1,47 +0,0 @@ -import boa -import pytest -from hypothesis import given, settings -from hypothesis import strategies as st -from math import exp - - -@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): - boa.deal(from_coin, 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 p1 == pytest.approx(p2, rel=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): - boa.deal(from_coin, 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 swap_w_d.last_price() == pytest.approx(p, rel=1e-5) - assert swap_w_d.price_oracle() == pytest.approx(10**18, rel=1e-5) - boa.env.time_travel(dt) - w = exp(-dt / 866) - p1 = int(10**18 * w + p * (1 - w)) - assert swap_w_d.price_oracle() == pytest.approx(p1, rel=1e-5) From de867b6d47331499b2030418d82f958dc98483fa Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 3 Sep 2025 14:18:18 +0200 Subject: [PATCH 184/413] test: add unit tests for max_borrowable --- tests/lending/test_fuzz_max_borrowable.py | 70 +++++++++++++++++ tests/lending/test_max_borrowable.py | 91 ++++++----------------- tests/utils/__init__.py | 2 + 3 files changed, 93 insertions(+), 70 deletions(-) create mode 100644 tests/lending/test_fuzz_max_borrowable.py diff --git a/tests/lending/test_fuzz_max_borrowable.py b/tests/lending/test_fuzz_max_borrowable.py new file mode 100644 index 00000000..bd9cefb6 --- /dev/null +++ b/tests/lending/test_fuzz_max_borrowable.py @@ -0,0 +1,70 @@ +import boa +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st + + +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, 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) \ No newline at end of file diff --git a/tests/lending/test_max_borrowable.py b/tests/lending/test_max_borrowable.py index 5bc3057e..2611d9b1 100644 --- a/tests/lending/test_max_borrowable.py +++ b/tests/lending/test_max_borrowable.py @@ -1,70 +1,21 @@ -import boa -import pytest -from hypothesis import given, settings -from hypothesis import strategies as st - - -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() - boa.deal(collateral_token, 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 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, 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() - boa.deal(collateral_token, 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.borrowed_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/utils/__init__.py b/tests/utils/__init__.py index 7c39f71d..c10e3b9f 100644 --- a/tests/utils/__init__.py +++ b/tests/utils/__init__.py @@ -2,6 +2,8 @@ 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) From 569542e3977f552de182584cb30092404f7c8503 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 3 Sep 2025 14:19:13 +0200 Subject: [PATCH 185/413] test: add vault and fix some conftest --- tests/conftest.py | 46 +++++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index caa819f4..70d5cc58 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -54,28 +54,27 @@ 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 + # TODO should parametrize to use other tokens in lending return stablecoin @pytest.fixture(scope="module") -def lending_mpolicy_deployer(): - """Default monetary policy deployer for lending markets: Constant policy. - Tests can override to use a different lending policy. - """ +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): - """Monetary policy contract for the current market. - Test modules can override by replacing the policy on the controller. - """ - if market_type == "lending": - # TODO make both controllers use the same policy through a constructor adapter - return CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER.at(controller.monetary_policy()) - else: - return CONSTANT_MONETARY_POLICY_DEPLOYER.at(controller.monetary_policy()) +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") @@ -151,7 +150,8 @@ def market( price_oracle, seed_liquidity, borrowed_token, - lending_mpolicy_deployer, + mint_monetary_policy, + lending_monetary_policy, amm_A, amm_fee, loan_discount, @@ -163,7 +163,7 @@ def market( return proto.create_mint_market( collateral_token=collateral_token, price_oracle=price_oracle, - monetary_policy=proto.mint_monetary_policy, + monetary_policy=mint_monetary_policy.address, A=amm_A, amm_fee=amm_fee, loan_discount=loan_discount, @@ -183,7 +183,7 @@ def market( min_borrow_rate=min_borrow_rate, max_borrow_rate=max_borrow_rate, seed_amount=seed_liquidity, - mpolicy_deployer=lending_mpolicy_deployer, + mpolicy_deployer=lending_monetary_policy, ) @@ -199,14 +199,22 @@ def controller(market, market_type, admin, 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. From 260ebbc82eeb58c75f23acda2a43a04ebb4df771 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 3 Sep 2025 14:21:58 +0200 Subject: [PATCH 186/413] ci: fuzzing fails faster --- .github/workflows/test.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 5d963d05..9c55394e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -5,6 +5,7 @@ on: [push] env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # RPC_ETHEREUM: ${{ secrets.RPC_ETHEREUM }} + HYPOTHESIS_PROFILE: no-shrink jobs: tests: From 2beb66ad00f18182c416e25dabaef90e35b55c9c Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 3 Sep 2025 14:22:56 +0200 Subject: [PATCH 187/413] chore: add TODO --- tests/zaps/partial_liquidation/test_partial_repay_zap.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/zaps/partial_liquidation/test_partial_repay_zap.py b/tests/zaps/partial_liquidation/test_partial_repay_zap.py index 9a850ff6..4d77aad5 100644 --- a/tests/zaps/partial_liquidation/test_partial_repay_zap.py +++ b/tests/zaps/partial_liquidation/test_partial_repay_zap.py @@ -5,6 +5,7 @@ @pytest.fixture(scope="module") def partial_repay_zap(admin): with boa.env.prank(admin): + # TODO this needs to be moved to deployers return boa.load("contracts/zaps/PartialRepayZap.vy", 5 * 10**16, 1 * 10**16) From ad9587c760f34bbb86938cd8fffaf09845b66eee Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 3 Sep 2025 14:28:15 +0200 Subject: [PATCH 188/413] test: add set borrow cap tests --- tests/lending/conftest.py | 94 ++++------------------------ tests/lending/test_set_borrow_cap.py | 16 +++++ tests/utils/deploy.py | 16 +++-- 3 files changed, 39 insertions(+), 87 deletions(-) create mode 100644 tests/lending/test_set_borrow_cap.py diff --git a/tests/lending/conftest.py b/tests/lending/conftest.py index 3bbe317c..eda102b5 100644 --- a/tests/lending/conftest.py +++ b/tests/lending/conftest.py @@ -1,97 +1,29 @@ import boa import pytest -from tests.utils.deployers import FAKE_LEVERAGE_DEPLOYER, ERC20_MOCK_DEPLOYER - -@pytest.fixture(scope="module", params=[True, False]) -def tokens_for_vault(admin, stablecoin, decimals, request): - stablecoin_is_borrowed = request.param - with boa.env.prank(admin): - token = ERC20_MOCK_DEPLOYER.deploy(decimals) - if stablecoin_is_borrowed: - borrowed_token = stablecoin - collateral_token = token - else: - borrowed_token = token - collateral_token = stablecoin - return borrowed_token, collateral_token +from tests.utils.deployers import FAKE_LEVERAGE_DEPLOYER +from tests.utils.deployers import SEMILOG_MONETARY_POLICY_DEPLOYER @pytest.fixture(scope="module") -def collateral_token(tokens_for_vault): - return tokens_for_vault[1] +def market_type(): + """Force lending-only markets for tests in this folder.""" + return "lending" @pytest.fixture(scope="module") -def borrowed_token(tokens_for_vault): - return tokens_for_vault[0] +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 lending_market(proto, borrowed_token, collateral_token, price_oracle, admin): +def fake_leverage(collateral_token, borrowed_token, controller, admin): with boa.env.prank(admin): - result = proto.create_lending_market( - borrowed_token=borrowed_token, - collateral_token=collateral_token, - A=100, - fee=int(0.006 * 1e18), - loan_discount=int(0.09 * 1e18), - liquidation_discount=int(0.06 * 1e18), - price_oracle=price_oracle, - name="Test vault", - min_borrow_rate=int(0.005 * 1e18) // (365 * 86400), # 0.5% APR - max_borrow_rate=int(0.5 * 1e18) // (365 * 86400) # 50% APR - ) - return result - - -@pytest.fixture(scope="module") -def vault(lending_market): - return lending_market['vault'] - - -@pytest.fixture(scope="module") -def market_controller(lending_market, admin): - controller = lending_market['controller'] - with boa.env.prank(admin): - controller.set_borrow_cap(2**256 - 1) - return controller - - -@pytest.fixture(scope="module") -def market_amm(lending_market): - return lending_market['amm'] - - -@pytest.fixture(scope="module") -def monetary_policy(market_controller, borrowed_token, admin): - from tests.utils.deployers import SEMILOG_MONETARY_POLICY_DEPLOYER - # Override default policy with Semilog for lending test suite - min_borrow_rate = int(0.005 * 1e18) // (365 * 86400) # 0.5% APR - max_borrow_rate = int(0.5 * 1e18) // (365 * 86400) # 50% APR - with boa.env.prank(admin): - mp = SEMILOG_MONETARY_POLICY_DEPLOYER.deploy( + leverage = FAKE_LEVERAGE_DEPLOYER.deploy( borrowed_token.address, - min_borrow_rate, - max_borrow_rate, + collateral_token.address, + controller.address, + 3000 * 10**18, ) - market_controller.set_monetary_policy(mp) - return mp - - -@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()) - boa.deal(borrowed_token, 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 = FAKE_LEVERAGE_DEPLOYER.deploy(borrowed_token.address, collateral_token.address, - market_controller.address, 3000 * 10**18) boa.deal(collateral_token, leverage.address, 1000 * 10**collateral_token.decimals()) return leverage diff --git a/tests/lending/test_set_borrow_cap.py b/tests/lending/test_set_borrow_cap.py new file mode 100644 index 00000000..cc26415c --- /dev/null +++ b/tests/lending/test_set_borrow_cap.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", _strict=True) + 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/utils/deploy.py b/tests/utils/deploy.py index 9796f098..20200526 100644 --- a/tests/utils/deploy.py +++ b/tests/utils/deploy.py @@ -232,14 +232,18 @@ def create_lending_market( amm = AMM_DEPLOYER.at(result[2]) # Optionally override the market's monetary policy after creation. - # By default, factory uses self.blueprints.mpolicy (Semilog policy). + # Important: SemilogMonetaryPolicy expects FACTORY to be msg.sender at deploy time. + # Deploy under the LendingFactory as sender, then set via controller (admin = factory admin). if mpolicy_deployer is not None: + # Deploy with factory as msg.sender so FACTORY immutable is correct + custom_mp = mpolicy_deployer.deploy( + borrowed_token.address, + min_borrow_rate, + max_borrow_rate, + sender=self.lending_factory.address + ) + # Set on controller with admin privileges with boa.env.prank(self.admin): - custom_mp = mpolicy_deployer.deploy( - borrowed_token.address, - min_borrow_rate, - max_borrow_rate, - ) controller.set_monetary_policy(custom_mp) # Seed lending markets by depositing borrowed token into the vault From f2aa88a5083a104a75915e74e9c5bc5320049a3f Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 3 Sep 2025 14:54:35 +0200 Subject: [PATCH 189/413] test: fix broken zaps tests --- .../testing/ConstantMonetaryPolicyLending.vy | 18 +++------------ tests/conftest.py | 2 +- tests/utils/deploy.py | 22 +++++++++---------- 3 files changed, 15 insertions(+), 27 deletions(-) diff --git a/contracts/testing/ConstantMonetaryPolicyLending.vy b/contracts/testing/ConstantMonetaryPolicyLending.vy index a40420fc..453ab922 100644 --- a/contracts/testing/ConstantMonetaryPolicyLending.vy +++ b/contracts/testing/ConstantMonetaryPolicyLending.vy @@ -1,27 +1,15 @@ -# @version 0.4.3 -""" -Although this monetary policy works, it's only intended to be used in tests -""" +# pragma version 0.4.3 from ethereum.ercs import IERC20 - -admin: public(address) rate: public(uint256) - @deploy def __init__(borrowed_token: IERC20, min_rate: uint256, max_rate: uint256): - self.admin = tx.origin + # Testing policy: no admin mechanics; initialize to provided min_rate self.rate = min_rate -@external -def set_admin(admin: address): - assert msg.sender == self.admin - self.admin = admin - - @external def rate_write() -> uint256: return self.rate @@ -29,5 +17,5 @@ def rate_write() -> uint256: @external def set_rate(rate: uint256): - assert msg.sender == self.admin + # Testing policy: callable by anyone in tests self.rate = rate diff --git a/tests/conftest.py b/tests/conftest.py index 70d5cc58..3f09b81e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -163,7 +163,7 @@ def market( return proto.create_mint_market( collateral_token=collateral_token, price_oracle=price_oracle, - monetary_policy=mint_monetary_policy.address, + monetary_policy=mint_monetary_policy, A=amm_A, amm_fee=amm_fee, loan_discount=loan_discount, diff --git a/tests/utils/deploy.py b/tests/utils/deploy.py index 20200526..49921410 100644 --- a/tests/utils/deploy.py +++ b/tests/utils/deploy.py @@ -28,6 +28,7 @@ # Monetary policies CONSTANT_MONETARY_POLICY_DEPLOYER, CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER, + SEMILOG_MONETARY_POLICY_DEPLOYER, # Testing contracts WETH_DEPLOYER, @@ -232,19 +233,18 @@ def create_lending_market( amm = AMM_DEPLOYER.at(result[2]) # Optionally override the market's monetary policy after creation. - # Important: SemilogMonetaryPolicy expects FACTORY to be msg.sender at deploy time. - # Deploy under the LendingFactory as sender, then set via controller (admin = factory admin). if mpolicy_deployer is not None: - # Deploy with factory as msg.sender so FACTORY immutable is correct - custom_mp = mpolicy_deployer.deploy( - borrowed_token.address, - min_borrow_rate, - max_borrow_rate, - sender=self.lending_factory.address - ) + # Always deploy with FACTORY as msg.sender so policies that bind FACTORY at + # deploy time (e.g., SemilogMonetaryPolicy) initialize correctly. + # TODO report issue to boa (sender not available for constructor) + with boa.env.prank(self.lending_factory.address): + custom_mp = mpolicy_deployer.deploy( + borrowed_token.address, + min_borrow_rate, + max_borrow_rate, + ) # Set on controller with admin privileges - with boa.env.prank(self.admin): - controller.set_monetary_policy(custom_mp) + controller.set_monetary_policy(custom_mp, sender=self.admin) # Seed lending markets by depositing borrowed token into the vault if seed_amount and seed_amount > 0: From ea9322c255304f247004c73f05575bebf737d24f Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 3 Sep 2025 15:28:38 +0200 Subject: [PATCH 190/413] test: use correct fixture name --- tests/lending/test_bigfuzz.py | 162 +++++++++--------- .../lending/test_health_calculator_create.py | 12 +- .../test_health_calculator_stateful.py | 10 +- tests/lending/test_health_in_trades.py | 6 +- tests/lending/test_monetary_policy.py | 12 +- tests/lending/test_shifted_trades.py | 2 +- 6 files changed, 102 insertions(+), 102 deletions(-) diff --git a/tests/lending/test_bigfuzz.py b/tests/lending/test_bigfuzz.py index bf266b7e..191178fb 100644 --- a/tests/lending/test_bigfuzz.py +++ b/tests/lending/test_bigfuzz.py @@ -42,23 +42,23 @@ class BigFuzz(RuleBasedStateMachine): def __init__(self): super().__init__() - self.A = self.market_amm.A() + 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.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): @@ -94,34 +94,34 @@ def create_loan(self, y, n, ratio, uid): user = self.accounts[uid] with boa.env.prank(user): boa.deal(self.collateral_token, user, y) - max_debt = self.market_controller.max_borrowable(y, n) + 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)): + 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 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,7 +130,7 @@ 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 @@ -141,51 +141,51 @@ def repay(self, ratio, uid): 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])) + (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() + 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 @@ -199,7 +199,7 @@ 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): @@ -208,59 +208,59 @@ def borrow_more(self, y, ratio, uid): 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): 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: boa.deal(self.borrowed_token, user, amount) - self.market_amm.exchange(0, 1, amount, 0) + self.amm.exchange(0, 1, amount, 0) else: boa.deal(self.collateral_token, user, amount) - self.market_amm.exchange(1, 0, amount, 0) + self.amm.exchange(1, 0, amount, 0) @rule(r=ratio, is_pump=is_pump, uid=user_id) def trade(self, r, is_pump, uid): @@ -269,50 +269,50 @@ def trade(self, r, is_pump, uid): if is_pump: amount = int(r * self.borrowed_token.totalSupply()) boa.deal(self.borrowed_token, user, amount) - self.market_amm.exchange(0, 1, amount, 0) + self.amm.exchange(0, 1, amount, 0) else: amount = int(r * self.collateral_token.totalSupply()) boa.deal(self.collateral_token, user, amount) - self.market_amm.exchange(1, 0, amount, 0) + 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: boa.deal(self.borrowed_token, user, diff) if emode == USE_FRACTION: try: - self.market_controller.liquidate( + 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)) try: - self.market_controller.liquidate( + 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): @@ -324,8 +324,8 @@ 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: boa.deal(self.borrowed_token, liquidator, diff) @@ -333,64 +333,64 @@ def liquidate(self, uid, luid, emode, frac): with boa.env.prank(liquidator): with boa.reverts(): if emode == USE_FRACTION: - self.market_controller.liquidate( + 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( + 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)) 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) with boa.env.prank(liquidator): if health >= health_limit: with boa.reverts(): if emode == USE_FRACTION: - self.market_controller.liquidate( + 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( + 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)) else: if emode == USE_FRACTION: try: - self.market_controller.liquidate( + 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)) try: - self.market_controller.liquidate( + 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) @@ -419,18 +419,18 @@ 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.lent() <= self.market_controller.repaid() # 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.repaid() + self.market_controller.total_debt() >= self.market_controller.lent() + assert self.controller.repaid() + self.controller.total_debt() >= self.controller.lent() def test_big_fuzz( - vault, borrowed_token, collateral_token, monetary_policy, accounts, admin, market_amm, market_controller, + 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 diff --git a/tests/lending/test_health_calculator_create.py b/tests/lending/test_health_calculator_create.py index 5dda2587..3d1687ea 100644 --- a/tests/lending/test_health_calculator_create.py +++ b/tests/lending/test_health_calculator_create.py @@ -8,13 +8,13 @@ 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): +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 @@ -22,10 +22,10 @@ def test_health_calculator_create(market_amm, filled_controller, collateral_toke 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 be241ffc..998dbdd2 100644 --- a/tests/lending/test_health_calculator_stateful.py +++ b/tests/lending/test_health_calculator_stateful.py @@ -24,8 +24,8 @@ class StatefulLendBorrow(RuleBasedStateMachine): def __init__(self): super().__init__() - self.controller = self.filled_controller - self.amm = self.market_amm + self.controller = self.controller + self.amm = self.amm self.debt_ceiling = self.controller.borrowed_balance() self.collateral_mul = 10**(18 - self.collateral_token.decimals()) self.borrowed_mul = 10**(18 - self.borrowed_token.decimals()) @@ -251,14 +251,14 @@ def health(self): assert self.controller.health(user) > 0 -def test_stateful_lendborrow(market_amm, filled_controller, collateral_token, borrowed_token, accounts): +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() @@ -266,7 +266,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 af55ccb9..93bc1c85 100644 --- a/tests/lending/test_health_in_trades.py +++ b/tests/lending/test_health_in_trades.py @@ -18,8 +18,8 @@ class AdiabaticTrader(RuleBasedStateMachine): def __init__(self): super().__init__() self.collateral = self.collateral_token - self.amm = self.market_amm - self.controller = self.filled_controller + 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): @@ -98,7 +98,7 @@ def health(self): assert h > 0 -def test_adiabatic_follow(market_amm, filled_controller, monetary_policy, collateral_token, borrowed_token, price_oracle, accounts, admin): +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) diff --git a/tests/lending/test_monetary_policy.py b/tests/lending/test_monetary_policy.py index bcce303e..bc08bce6 100644 --- a/tests/lending/test_monetary_policy.py +++ b/tests/lending/test_monetary_policy.py @@ -8,22 +8,22 @@ @given(fill=st.floats(min_value=0.0, max_value=2.0)) -def test_monetary_policy(filled_controller, collateral_token, borrowed_token, monetary_policy, 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()) if to_borrow > 0 and c_amount > 0: with boa.env.prank(admin): - collateral_token.approve(filled_controller, 2**256 - 1) + 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 = monetary_policy.rate(filled_controller) + controller.create_loan(c_amount, to_borrow, 5) + rate = monetary_policy.rate(controller.address) 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 diff --git a/tests/lending/test_shifted_trades.py b/tests/lending/test_shifted_trades.py index ff2839f9..76ea5849 100644 --- a/tests/lending/test_shifted_trades.py +++ b/tests/lending/test_shifted_trades.py @@ -21,7 +21,7 @@ def trade_to_price(self, p): super().trade_to_price(int(p * (1 + self.price_shift))) -def test_adiabatic_shifted(market_amm, filled_controller, monetary_policy, 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) From 4f1c0c0d276915d4efcfd5b76b0db95b7ef6aece Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 3 Sep 2025 15:34:40 +0200 Subject: [PATCH 191/413] ci: split stabilize from stableborrow --- .github/workflows/test.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 9c55394e..8d4cfc02 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -22,7 +22,8 @@ jobs: - "tests/zaps" - "tests/amm" - "tests/lending" - - "tests/stableborrow" + - "tests/stableborrow --ignore=tests/stableborrow/stabilize" + - "tests/stableborrow/stabilize" venom: - { name: "standard mode", value: false } # - { name: "venom mode", value: true } From 3f3acb88176d4d33d887f3b40cffa00e146f24f1 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 3 Sep 2025 16:06:19 +0200 Subject: [PATCH 192/413] chore: bump testing contracts to vyper 0.4.3 --- contracts/testing/BlockCounter.vy | 4 +- contracts/testing/ChainlinkAggregatorMock.vy | 5 +- contracts/testing/ConstantMonetaryPolicy.vy | 4 +- .../testing/CryptoWithStablePriceWsteth.vy | 48 ++++++++--------- contracts/testing/DummyFlashBorrower.vy | 12 ++--- contracts/testing/DummyLMCallback.vy | 8 +-- contracts/testing/DummyPriceOracle.vy | 4 +- contracts/testing/ERC20CRV.vy | 44 ++++++++-------- contracts/testing/FakeLeverage.vy | 34 ++++++------- contracts/testing/MockFactory.vy | 8 +-- contracts/testing/MockMarket.vy | 2 +- contracts/testing/MockPegKeeper.vy | 4 +- contracts/testing/MockRateOracle.vy | 4 +- contracts/testing/SwapFactory.vy | 51 ++++++++++++------- contracts/testing/TricryptoMock.vy | 4 +- contracts/testing/WETH.vy | 13 +++-- 16 files changed, 132 insertions(+), 117 deletions(-) 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/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/DummyFlashBorrower.vy b/contracts/testing/DummyFlashBorrower.vy index ffbdf227..a75e8ba7 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 ethereum.ercs 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/FakeLeverage.vy b/contracts/testing/FakeLeverage.vy index fd9606cb..5e832595 100644 --- a/contracts/testing/FakeLeverage.vy +++ b/contracts/testing/FakeLeverage.vy @@ -1,21 +1,21 @@ -# @version 0.3.10 -from vyper.interfaces import ERC20 +# pragma version 0.4.3 +from ethereum.ercs import IERC20 -STABLECOIN: immutable(ERC20) -COLLATERAL: immutable(ERC20) +STABLECOIN: immutable(IERC20) +COLLATERAL: immutable(IERC20) price: 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 +23,28 @@ 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, calldata: Bytes[10**4]) -> uint256[2]: - min_amount: uint256 = _abi_decode(calldata, (uint256)) - assert STABLECOIN.balanceOf(self) >= debt - amount_out: uint256 = debt * 10**18 / self.price + 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, calldata: Bytes[10**4]) -> uint256[2]: - frac: uint256 = _abi_decode(calldata, (uint256)) - s_diff: uint256 = (debt - stablecoins) * frac / 10**18 + 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, calldata: Bytes[10**4]) -> uint256[2]: - return [STABLECOIN.balanceOf(self), collateral] + 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/SwapFactory.vy b/contracts/testing/SwapFactory.vy index 80098268..ea5f68d4 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[Bytes[4], 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]) @@ -44,24 +53,32 @@ def __init__(impl: address): def deploy(coin_a: ERC20, coin_b: ERC20) -> address: pool: Swap = Swap(create_minimal_proxy_to(IMPL)) 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], + "TestName", + "TST", + [address(coin_a), address(coin_b), empty(address), empty(address)], + [10**(18 - coin_a.decimals()) * 10**18, 10**(18 - coin_b.decimals()) * 10**18, 0, 0], 100, - 0) - self.pools[self.n] = pool.address + 0, + ) + self.pools[self.n] = address(pool) self.n += 1 - return pool.address + return address(pool) @external def deploy_ng(coin_a: ERC20, coin_b: ERC20) -> address: - assert self.factory_ng.address != empty(address), "Factory not set" + assert address(self.factory_ng) != empty(address), "Factory not set" pool: address = self.factory_ng.deploy_plain_pool( - 'TestName-ng', 'TST-ng', - [coin_a.address, coin_b.address], - 100, 0, 10000000000, 866, 0, [1, 1], - [method_id("get00()", output_type=bytes4), method_id("get11()", output_type=bytes4)], + "TestName-ng", + "TST-ng", + [address(coin_a), address(coin_b)], + 100, + 0, + 10000000000, + 866, + 0, + [1, 1], + [method_id("get00()"), method_id("get11()")], [self.rate_oracle, self.rate_oracle], ) self.pools[self.n] = pool 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/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 From 38916db2cbac0de958bb06d2a15b7b848fbb9bbb Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 3 Sep 2025 16:10:34 +0200 Subject: [PATCH 193/413] test: fix incorrectly ported vyper contract --- contracts/testing/SwapFactory.vy | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/testing/SwapFactory.vy b/contracts/testing/SwapFactory.vy index ea5f68d4..8437d11e 100644 --- a/contracts/testing/SwapFactory.vy +++ b/contracts/testing/SwapFactory.vy @@ -43,7 +43,7 @@ admin: public(address) factory_ng: FactoryNG rate_oracle: address -@external +@deploy def __init__(impl: address): IMPL = impl self.admin = msg.sender @@ -52,26 +52,26 @@ def __init__(impl: address): @external def deploy(coin_a: ERC20, coin_b: ERC20) -> address: pool: Swap = Swap(create_minimal_proxy_to(IMPL)) - pool.initialize( + extcall pool.initialize( "TestName", "TST", - [address(coin_a), address(coin_b), empty(address), empty(address)], - [10**(18 - coin_a.decimals()) * 10**18, 10**(18 - coin_b.decimals()) * 10**18, 0, 0], + [coin_a.address, coin_b.address, empty(address), empty(address)], + [10**(18 - staticcall coin_a.decimals()) * 10**18, 10**(18 - staticcall coin_b.decimals()) * 10**18, 0, 0], 100, 0, ) - self.pools[self.n] = address(pool) + self.pools[self.n] = pool.address self.n += 1 - return address(pool) + return pool.address @external def deploy_ng(coin_a: ERC20, coin_b: ERC20) -> address: - assert address(self.factory_ng) != empty(address), "Factory not set" - pool: address = self.factory_ng.deploy_plain_pool( + assert self.factory_ng.address != empty(address), "Factory not set" + pool: address = extcall self.factory_ng.deploy_plain_pool( "TestName-ng", "TST-ng", - [address(coin_a), address(coin_b)], + [coin_a.address, coin_b.address], 100, 0, 10000000000, From 5f113563983a39b22c3a534bfa558494bd70b86d Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 4 Sep 2025 14:36:00 +0200 Subject: [PATCH 194/413] test: use boa version with bugfix --- pyproject.toml | 2 +- uv.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0f781f30..87a5eb4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ dev = [ ] [tool.uv.sources] -titanoboa = { git = "https://github.com/vyperlang/titanoboa", rev = "a508f2adac3c05a5bfcc09b7d64f9be46c0bab17" } +titanoboa = { git = "https://github.com/vyperlang/titanoboa", rev = "master" } [tool.uv] package = false # This is not a Python package diff --git a/uv.lock b/uv.lock index ead77f28..90d7820e 100644 --- a/uv.lock +++ b/uv.lock @@ -424,7 +424,7 @@ dev = [ { 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/vyperlang/titanoboa?rev=a508f2adac3c05a5bfcc09b7d64f9be46c0bab17" }, + { name = "titanoboa", git = "https://github.com/vyperlang/titanoboa?rev=master" }, ] [[package]] @@ -1554,7 +1554,7 @@ wheels = [ [[package]] name = "titanoboa" version = "0.2.7" -source = { git = "https://github.com/vyperlang/titanoboa?rev=a508f2adac3c05a5bfcc09b7d64f9be46c0bab17#a508f2adac3c05a5bfcc09b7d64f9be46c0bab17" } +source = { git = "https://github.com/vyperlang/titanoboa?rev=master#5360bbae07950ddfd44bc61fd14f656d036c0315" } dependencies = [ { name = "eth-abi" }, { name = "eth-account" }, From 6973c3245f7f0977d85e0a7cdd1d0265bacf9e10 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 4 Sep 2025 14:59:50 +0200 Subject: [PATCH 195/413] test: fix porting errors in mock --- contracts/testing/SwapFactory.vy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/testing/SwapFactory.vy b/contracts/testing/SwapFactory.vy index 8437d11e..786e31b7 100644 --- a/contracts/testing/SwapFactory.vy +++ b/contracts/testing/SwapFactory.vy @@ -24,7 +24,7 @@ interface FactoryNG: _ma_exp_time: uint256, _implementation_idx: uint256, _asset_types: DynArray[uint8, MAX_COINS], - _method_ids: DynArray[Bytes[4], MAX_COINS], + _method_ids: DynArray[bytes4, MAX_COINS], _oracles: DynArray[address, MAX_COINS], ) -> address: nonpayable @@ -78,7 +78,7 @@ def deploy_ng(coin_a: ERC20, coin_b: ERC20) -> address: 866, 0, [1, 1], - [method_id("get00()"), method_id("get11()")], + [method_id("get00()", output_type=bytes4), method_id("get11()", output_type=bytes4)], [self.rate_oracle, self.rate_oracle], ) self.pools[self.n] = pool From 4e5a58a6821f692a86ef702cf6eeae920e37a8a7 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 4 Sep 2025 15:25:02 +0200 Subject: [PATCH 196/413] test: port stableswap to 0.4 to enable evals --- contracts/Stableswap.vy | 208 +++++++++++------------ tests/stableborrow/stabilize/conftest.py | 1 - 2 files changed, 104 insertions(+), 105 deletions(-) diff --git a/contracts/Stableswap.vy b/contracts/Stableswap.vy index 07bc2620..85ad292f 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 ethereum.ercs 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 @@ -294,7 +294,7 @@ def permit( if _owner.is_contract: 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 @@ -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 @@ -491,8 +491,8 @@ def _ma_price() -> uint256: last_ema_price: uint256 = shift(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 +536,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 +557,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 +572,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 +593,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 +619,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 +638,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: @@ -679,12 +679,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 +705,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 +713,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 +746,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 +767,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 +794,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,16 +813,16 @@ 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) @@ -830,7 +830,7 @@ def exchange( @external -@nonreentrant('lock') +@nonreentrant def remove_liquidity( _burn_amount: uint256, _min_amounts: uint256[N_COINS], @@ -847,13 +847,13 @@ 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 @@ -866,7 +866,7 @@ def remove_liquidity( @external -@nonreentrant('lock') +@nonreentrant def remove_liquidity_imbalance( _amounts: uint256[N_COINS], _max_burn_amount: uint256, @@ -885,26 +885,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,7 +912,7 @@ 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 + burn_amount: uint256 = ((D0 - D2) * total_supply // D0) + 1 assert burn_amount > 1 # dev: zero tokens burned assert burn_amount <= _max_burn_amount, "Slippage screwed you" @@ -932,12 +932,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 +948,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 +985,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 +1025,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,13 +1043,13 @@ 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 + assert extcall IERC20(self.coins[i]).transfer(_receiver, dy[0], default_return_value=True) # dev: failed transfer log RemoveLiquidityOne(msg.sender, _burn_amount, dy[0], total_supply) self.save_p_from_price(dy[2]) @@ -1059,7 +1059,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 @@ -1082,7 +1082,7 @@ def ramp_A(_future_A: uint256, _future_time: uint256): @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 @@ -1096,7 +1096,7 @@ def stop_ramp_A(): @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,12 +1105,12 @@ 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 @@ -1121,7 +1121,7 @@ def commit_new_fee(_new_fee: uint256): @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 @@ -1133,12 +1133,12 @@ def apply_new_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/tests/stableborrow/stabilize/conftest.py b/tests/stableborrow/stabilize/conftest.py index 099a97ea..68882222 100644 --- a/tests/stableborrow/stabilize/conftest.py +++ b/tests/stableborrow/stabilize/conftest.py @@ -85,7 +85,6 @@ def rate_oracle(swap_impl, admin): 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 = 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}') From 04f7f935ab833da4da19dd442e2aedc7bb7f740d Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 4 Sep 2025 17:47:59 +0200 Subject: [PATCH 197/413] test: enable eval for vvm --- pyproject.toml | 2 +- tests/conftest.py | 21 ++++++++++++++++++++- uv.lock | 4 ++-- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 87a5eb4c..f675b19b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ dev = [ ] [tool.uv.sources] -titanoboa = { git = "https://github.com/vyperlang/titanoboa", rev = "master" } +titanoboa = { git = "https://github.com/AlbertoCentonze/titanoboa", rev = "3fdd1d37b6afc64cd2db91352b86cbf658996934" } [tool.uv] package = false # This is not a Python package diff --git a/tests/conftest.py b/tests/conftest.py index 3f09b81e..019188bd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ import os from datetime import timedelta +import uuid import boa import pytest @@ -7,10 +8,28 @@ from tests.utils.deploy import Protocol from tests.utils.deployers import ( ERC20_MOCK_DEPLOYER, - CONSTANT_MONETARY_POLICY_DEPLOYER, CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER, ) +def _patch_vvm_eval(): + def _eval(self, line: str): + name = f"_boa_eval_{uuid.uuid4().hex}" + assert line.count("\n") == 0 + body = "".join(f" {line}") + src = f"def {name}():\n{body}" + + self.inject_function(src) + func = getattr(self, name) + return func() + + + boa.contracts.vvm.vvm_contract.VVMContract.eval = _eval + return True + + +_patch_vvm_eval() + + boa.env.enable_fast_mode() diff --git a/uv.lock b/uv.lock index 90d7820e..71485909 100644 --- a/uv.lock +++ b/uv.lock @@ -424,7 +424,7 @@ dev = [ { 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/vyperlang/titanoboa?rev=master" }, + { name = "titanoboa", git = "https://github.com/AlbertoCentonze/titanoboa?rev=3fdd1d37b6afc64cd2db91352b86cbf658996934" }, ] [[package]] @@ -1554,7 +1554,7 @@ wheels = [ [[package]] name = "titanoboa" version = "0.2.7" -source = { git = "https://github.com/vyperlang/titanoboa?rev=master#5360bbae07950ddfd44bc61fd14f656d036c0315" } +source = { git = "https://github.com/AlbertoCentonze/titanoboa?rev=3fdd1d37b6afc64cd2db91352b86cbf658996934#3fdd1d37b6afc64cd2db91352b86cbf658996934" } dependencies = [ { name = "eth-abi" }, { name = "eth-account" }, From 0fc4c2e2e7c1a280386a1f0f3fb634924646f117 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 4 Sep 2025 17:55:23 +0200 Subject: [PATCH 198/413] test: fix eval --- tests/conftest.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 019188bd..b30d83ad 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,15 +16,14 @@ def _eval(self, line: str): name = f"_boa_eval_{uuid.uuid4().hex}" assert line.count("\n") == 0 body = "".join(f" {line}") - src = f"def {name}():\n{body}" + src = f"@external\ndef {name}():\n{body}" self.inject_function(src) func = getattr(self, name) return func() - + # TODO DANGER: this doesn't handle returned value correctly boa.contracts.vvm.vvm_contract.VVMContract.eval = _eval - return True _patch_vvm_eval() From aa4cce6fad8d9d73d43accdf9682b35096fef153 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 4 Sep 2025 18:26:23 +0200 Subject: [PATCH 199/413] test: more eval fixes --- pyproject.toml | 2 +- uv.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f675b19b..9231a48e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ dev = [ ] [tool.uv.sources] -titanoboa = { git = "https://github.com/AlbertoCentonze/titanoboa", rev = "3fdd1d37b6afc64cd2db91352b86cbf658996934" } +titanoboa = { git = "https://github.com/AlbertoCentonze/titanoboa", rev = "vvm-eval" } [tool.uv] package = false # This is not a Python package diff --git a/uv.lock b/uv.lock index 71485909..1be8e9a1 100644 --- a/uv.lock +++ b/uv.lock @@ -424,7 +424,7 @@ dev = [ { 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=3fdd1d37b6afc64cd2db91352b86cbf658996934" }, + { name = "titanoboa", git = "https://github.com/AlbertoCentonze/titanoboa?rev=vvm-eval" }, ] [[package]] @@ -1554,7 +1554,7 @@ wheels = [ [[package]] name = "titanoboa" version = "0.2.7" -source = { git = "https://github.com/AlbertoCentonze/titanoboa?rev=3fdd1d37b6afc64cd2db91352b86cbf658996934#3fdd1d37b6afc64cd2db91352b86cbf658996934" } +source = { git = "https://github.com/AlbertoCentonze/titanoboa?rev=vvm-eval#5f9a6515fe1279d29e29ec21910d49754a314a7a" } dependencies = [ { name = "eth-abi" }, { name = "eth-account" }, From 4702eb989d645318dca5de477a976e307caab10e Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 4 Sep 2025 19:09:14 +0200 Subject: [PATCH 200/413] test: use eval version with return_type --- tests/conftest.py | 18 -- uv.lock | 787 +++++++++++++++++++++++----------------------- 2 files changed, 401 insertions(+), 404 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b30d83ad..e00d4c96 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,24 +11,6 @@ CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER, ) -def _patch_vvm_eval(): - def _eval(self, line: str): - name = f"_boa_eval_{uuid.uuid4().hex}" - assert line.count("\n") == 0 - body = "".join(f" {line}") - src = f"@external\ndef {name}():\n{body}" - - self.inject_function(src) - func = getattr(self, name) - return func() - - # TODO DANGER: this doesn't handle returned value correctly - boa.contracts.vvm.vvm_contract.VVMContract.eval = _eval - - -_patch_vvm_eval() - - boa.env.enable_fast_mode() diff --git a/uv.lock b/uv.lock index 1be8e9a1..fbb14a6b 100644 --- a/uv.lock +++ b/uv.lock @@ -39,72 +39,72 @@ wheels = [ [[package]] name = "bitarray" -version = "3.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/ee/3b2fcbac3a4192e5d079aaa1850dff2f9ac625861c4c644819c2b34292ec/bitarray-3.6.0.tar.gz", hash = "sha256:20febc849a1f858e6a57a7d47b323fe9e727c579ddd526d317ad8831748a66a8", size = 147946 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/0e/52b66c2c36e938b0fc5160ba92034e949b1f7ec8c9f0b7d53188ebb3e8dc/bitarray-3.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6841c08b51417f8ffe398b2828fc0593440c99525c868f640e0302476745320b", size = 144421 }, - { url = "https://files.pythonhosted.org/packages/f4/67/904b50387cdd8caa8ad66aed03d90bf0acb38d3d1393ead5349a643b2618/bitarray-3.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a04b7a9017b8d0341ebbe77f61b74df1cf1b714f42b671a06f4912dc93d82597", size = 140919 }, - { url = "https://files.pythonhosted.org/packages/3b/d2/4371eba6e4626a663aca00bbf2a83c95498de794dd4536a7f355f177c878/bitarray-3.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:664d462a4c0783fd755fe3440f07b7e46d149859c96caacadf3f28890f19a8de", size = 314230 }, - { url = "https://files.pythonhosted.org/packages/1e/a1/e2ecc61a1441dfb2a8ba7048bbe8a7e75a394a08e4a1ffd9ec530ccbb180/bitarray-3.6.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e997d22e0d1e08c8752f61675a75d93659f7aa4dbeaee54207f8d877817b4a0c", size = 330869 }, - { url = "https://files.pythonhosted.org/packages/d7/5f/68e05c382bbbff1ff9f2c2cc343543bc755a0b7d20948a8ebb08a7e0e417/bitarray-3.6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6755cfcfa7d8966e704d580c831e39818f85e7b2b7852ad22708973176f0009e", size = 322249 }, - { url = "https://files.pythonhosted.org/packages/0f/20/ab8784e2968deb6fba2d2bac6b9190efc1360cc2a6ed57482ad82f5757cf/bitarray-3.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4798f6744fa2633666e17b4ea8ff70250781b52a25afdbf5ffb5e176c58848f1", size = 316564 }, - { url = "https://files.pythonhosted.org/packages/ac/1d/f28a148501f5fec33ad6e7f714bb4953ac3f80b8af4839c66767e874cd7a/bitarray-3.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:efa5834ba5e6c70b22afdca3894097e5a592d8d483c976359654ba990477799a", size = 304445 }, - { url = "https://files.pythonhosted.org/packages/ce/e6/175f048b2e2c26253fbae7e8fc2fdb772bb74e03b2a8b9bfde8acc2f509d/bitarray-3.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d47e2bdeba4fb1986af2ba395ce51223f4d460e6e77119439e78f2b592cafade", size = 311974 }, - { url = "https://files.pythonhosted.org/packages/b8/4b/03633ca351c8cce4902eea78cb1bbd75e4226c31da19e25df4e605372626/bitarray-3.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2a324e3007afb5c667026f5235b35efe3c4a95f1b83cd93aa9fce67b42f08e7c", size = 305521 }, - { url = "https://files.pythonhosted.org/packages/ef/cc/4a327a89b534f64f52d0ef232d3e12d179686914a0103636ddc1f77e89f7/bitarray-3.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:080a7bf55c432abdae74f25dc3dbff407418346aeae1d43e31f65e8ef114f785", size = 329032 }, - { url = "https://files.pythonhosted.org/packages/1b/ab/0000b27c58955b09c56db37fce88d7cc54fd46fee5c472002e2d08be7cb3/bitarray-3.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3eb1390a8b062fe9125e5cc4c5eba990b5d383eec54f2b996e7ce73ac43150f9", size = 327309 }, - { url = "https://files.pythonhosted.org/packages/6c/7d/0ebb9dc7a5bc7c6bb764c43b5e551234e3fdb5d4f06b8cfa4eb4dc7df27c/bitarray-3.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2020102a40edd094c0aa80e09203af71c533c41f76ce3237c99fd194a473ea33", size = 313309 }, - { url = "https://files.pythonhosted.org/packages/b7/f5/830d9f20cfb78759b2adfff01b695b40ebe26d76c256b6b228a4865ac26e/bitarray-3.6.0-cp310-cp310-win32.whl", hash = "sha256:01d6dc548e7fe5c66913c2274f44855b0f8474935acff7811e84fe1f4024c94f", size = 137611 }, - { url = "https://files.pythonhosted.org/packages/c6/65/a180faa2573711ff1d00db3ed203aa88281502da62d8088aa13d39bef56a/bitarray-3.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:8d759cecfa8aab4a1eb4e23b6420126b15c7743e85b33f389916bb98c4ecbb84", size = 144329 }, - { url = "https://files.pythonhosted.org/packages/90/bf/987f01842b3239fd5bb36b6f3aeba3070731d72f8d700a6a317f4731a0d7/bitarray-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7c20d6e6cafce5027e7092beb2ac6eec0d71045d6318b34f36e1387a8c8859a3", size = 144418 }, - { url = "https://files.pythonhosted.org/packages/ac/7e/97902cf29a46d14c2b0a938c0f83bb1de1e117a56df61a88676d278359e8/bitarray-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cf36cadeb9c989f760a13058dbc455e5406ec3d2d247c705c8d4bc6dd1b0fcc6", size = 140917 }, - { url = "https://files.pythonhosted.org/packages/a3/f6/0afe766d4bfc22403861f49a050b8ff9719a1783be25c85376cad2cbf06b/bitarray-3.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30ba4fba3de1dca653de41c879349ec6ca521d85cff6a7ca5d2fdd8f76c93781", size = 321827 }, - { url = "https://files.pythonhosted.org/packages/4d/01/a0a24b2605a8f682283a083ae2c97531446523ef67387189a9570ac5daa2/bitarray-3.6.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77d2368a06a86a18919c05a9b4b0ee9869f770e6a5f414b0fecc911870fe3974", size = 339057 }, - { url = "https://files.pythonhosted.org/packages/55/2f/0684beeaa874f321f0157206f3e625820935eb79837ca49a32aff2834097/bitarray-3.6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a39be79a7c36e9a2e20376261c30deb3cdca86b50f7462ae9ff10a755c6720b9", size = 331191 }, - { url = "https://files.pythonhosted.org/packages/42/82/ead4f1b1aa6ce4112cc59b515453dcf194d7e83cdd5e0c34eb422df9b6c1/bitarray-3.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4695fcd37478988b1d0a16d5bc0df56dcb677fd5db37f1893d993fd3ebef914b", size = 324416 }, - { url = "https://files.pythonhosted.org/packages/90/95/9558548842d249ba87caec43450cded1933e9ca0dec54bfb7cefbd146807/bitarray-3.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52328192d454ca2ddad09fbc088872b014c74b22ecdd5164717dc7e6442014fa", size = 312571 }, - { url = "https://files.pythonhosted.org/packages/d7/cb/f9aad4e436d1bcda7f348a85458b197f54b3e2f9626caba850a79d09caf3/bitarray-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96117212229905da864794df9ea7bd54987c30a5dcbab3432edc3f344231adae", size = 320005 }, - { url = "https://files.pythonhosted.org/packages/28/e4/9830c088551eb9c055871843ed15cece9917249f60f6db7093c5c58cff46/bitarray-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:68f6e64d4867ee79e25c49d7f35b2b1f04a6d6f778176dcf5b759f3b17a02b2b", size = 313190 }, - { url = "https://files.pythonhosted.org/packages/49/53/fde12dcf5eae0aabebc7b97e2226b2cdc9785ada2f38e9256b95c7402d71/bitarray-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:29ed022189a7997de46cb9bd4e2e49d6163d4f8d78dea72ac5a0e0293b856810", size = 337055 }, - { url = "https://files.pythonhosted.org/packages/41/f2/dfb6e9a9bb94ccbe5b369a839df477ab3b73e90b7ba8c92974c26e7e26ba/bitarray-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e71c9dba78671d38a549e3b2d52514f50e199f9d7e18ed9b0180adeef0d04130", size = 335649 }, - { url = "https://files.pythonhosted.org/packages/d8/30/4092ddf8ae0c566b9b157d0859d784e24f3b6d6ed90f437da304e29d3ee0/bitarray-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ddb319f869d497ef2d3d56319360b61284a9a1d8b3de3bc936748698acfda6be", size = 321512 }, - { url = "https://files.pythonhosted.org/packages/d3/a5/1cf6b545a18c71c6984f55758c2ee9595441e537ea424855fb3fdb89597e/bitarray-3.6.0-cp311-cp311-win32.whl", hash = "sha256:25060e7162e44242a449ed1a14a4e94b5aef340812754c443459f19c7954be91", size = 137767 }, - { url = "https://files.pythonhosted.org/packages/4b/66/4cd301ceb0b66281440011e539df82150b36457234ed8a6149136a8ab0ae/bitarray-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2d951002b11962b26afb31f758c18ad39771f287b100fa5adb1d09a47eaaf5b", size = 144547 }, - { url = "https://files.pythonhosted.org/packages/36/1b/91ee39eb1f52261d8734453bf4480b77edb440e86fd8d006ccf66a14b497/bitarray-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9616ea14917d06736339cf36bb9eaf4eb52110a74136b0dc5eff94e92417d22", size = 144173 }, - { url = "https://files.pythonhosted.org/packages/73/5d/edadc94cbdbfd333795d3b52aecb0106eeb163da965485363b34af9ddebe/bitarray-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7e2e1ff784c2cdfd863bad31985851427f2d2796e445cec85080c7510cba4315", size = 140917 }, - { url = "https://files.pythonhosted.org/packages/0e/cd/6a0d3d53118a148b9db13cf0d70a7fc8b61d77af1f85cb24a0c2dae62839/bitarray-3.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:911b4a16dce370657e5b8d8b6ba0fbb50dd5e2b24c4416f4b9e664503d3f0502", size = 324769 }, - { url = "https://files.pythonhosted.org/packages/3e/85/d12273fc975f40da1baf8dbc50b240782010d18ade62b28d8b03a21dfb1c/bitarray-3.6.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b47843f2f288fa746dead4394591a3432a358aaad48240283fa230d6e74b0e7", size = 341502 }, - { url = "https://files.pythonhosted.org/packages/f6/30/193980f3942280f22b2f602380c26e68d3b6e56791c27afac5497b72262b/bitarray-3.6.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8f95daf0ce2b24815ddf62667229ba5dfc0cfee43eb43b2549766170d0f24ae9", size = 333815 }, - { url = "https://files.pythonhosted.org/packages/8a/1c/a35fe5f1eda54d1db3d71b56e39a3b928f1d99546507496a1aef53dceabc/bitarray-3.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c15b9e37bbca59657e4dcc63ad068c821a4676def15f04742c406748a0a11b9c", size = 327371 }, - { url = "https://files.pythonhosted.org/packages/95/aa/b0d164489914a7c9d02f219911c05a57c7859e826fa6c0803c55339589e2/bitarray-3.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9d247fcc33c90f2758f4162693250341e3f38cd094f64390076ef33ad0887f9", size = 315020 }, - { url = "https://files.pythonhosted.org/packages/70/2c/8c69c9c85e7d3d23418ce10067f083d9e4bdf63a88244eb7d004d348d37d/bitarray-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:84bb57010a1ab76cf880424a2e0bce8dd26989849d2122ff073aa11bfc271c27", size = 322487 }, - { url = "https://files.pythonhosted.org/packages/0b/85/878fea379e568bf62175273e19b6c1a8b0b30a202f1885da693e5e037a06/bitarray-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:27d13c7b886afc5d2fc49d6e92f9c96b1f0a14dc7b5502520c29f3da7550d401", size = 316112 }, - { url = "https://files.pythonhosted.org/packages/9e/dc/68d0c84a9c26ae2a291165c7915b45611db16f7143955a5fdd5aaf481a91/bitarray-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c4e75bbf9ade3d2cdf1b607a8b353b17d9b3cf54e88b2a5a773f50ae6f1bfbc", size = 339348 }, - { url = "https://files.pythonhosted.org/packages/be/7c/cf5de46403c4176b96f064614686a584bd90ed37d63ba374a51f0313db6b/bitarray-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:975a118aa019d745f1398613b27fd8789f60a8cea057a00cdc1abedee123ffe6", size = 338601 }, - { url = "https://files.pythonhosted.org/packages/0f/8c/a725109141bc5a2e66a551d56461811d7fa506ace1a473a6455a658f5f03/bitarray-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9ed4a2852b3de7a64884afcc6936db771707943249a060aec8e551c16361d478", size = 324777 }, - { url = "https://files.pythonhosted.org/packages/69/1b/15d438b6cfbee8f01208fa0e47282fb0dcf5df501523d779a85f77584ce2/bitarray-3.6.0-cp312-cp312-win32.whl", hash = "sha256:5dd9edcab8979a50c2c4dec6d5b66789fb6f630bb52ab90a4548111075a75e48", size = 137813 }, - { url = "https://files.pythonhosted.org/packages/d3/80/14af316fe46be0d6c2624d75f138b5d29bf19846fd4e6faab4ca15c0f8b8/bitarray-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:552a93be286ca485914777461b384761519db313e0a7f3012dca424c9610a4d5", size = 144766 }, - { url = "https://files.pythonhosted.org/packages/90/2c/21066c7a97b2c88037b0fc04480fa13b0031c30c6f70452dc9c84fb2b087/bitarray-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3f96f57cea35ba19fd23a20b38fa0dfa3d87d582507129b8c8e314aa298f59b", size = 144156 }, - { url = "https://files.pythonhosted.org/packages/34/a5/9cc42ea0c440ac1c2a65375688ac5891da12b3820f4a32440791d25ed668/bitarray-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:81e84054b22babcd6c5cc1eac0de2bfc1054ecdf742720cbfb36efbe89ec6c30", size = 140916 }, - { url = "https://files.pythonhosted.org/packages/d7/66/709d259d855528213b1099facddb08d6108cb0074cf88dc357cdd07bacff/bitarray-3.6.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca643295bf5441dd38dadf7571ca4b63961820eedbffbe46ceba0893bf226203", size = 324713 }, - { url = "https://files.pythonhosted.org/packages/6c/67/831e366ea4f0d52d622482b8475f87040cbc210d8f5f383935a4cc6363fe/bitarray-3.6.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:139963494fc3dd5caee5e38c0a03783ef50be118565e94b1dbb0210770f0b32d", size = 341300 }, - { url = "https://files.pythonhosted.org/packages/66/c9/197375b63ca768ac8b1e624f27dc0eccdd451f94c6b9bf8950500d8da134/bitarray-3.6.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:243825f56b58bef28bfc602992a8c6d09bbc625628c195498d6020120d632a09", size = 333724 }, - { url = "https://files.pythonhosted.org/packages/e1/23/96c882d798b8bc9d5354ad1fba18ad3ad4f3c0a661a296c8e51ca2941e0f/bitarray-3.6.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:583b46b3ba44121de5e87e95ae379932dc5fd2e37ebdf2c11a6d7975891425c1", size = 327276 }, - { url = "https://files.pythonhosted.org/packages/20/8e/51751fe0e6f9fe7980b0467b471ba9ab8d1713a2a6576980d18143511656/bitarray-3.6.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f0be27d06732e2833b672a8fcc32fa195bdb22161eb88f8890de15e30264a01", size = 314903 }, - { url = "https://files.pythonhosted.org/packages/49/7a/e4db9876e6e8bb261e64a384d3adb4372f13099b356e559cec85d022b897/bitarray-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:507e567aee4806576e20752f22533e8b7ec61e7e75062a7ce9222a0675aa0da6", size = 322551 }, - { url = "https://files.pythonhosted.org/packages/aa/5a/9460070e6cb671067cc2e115a82da6fc9ef0958542b98b07a5ed4a05a97b/bitarray-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:22188943a29072b684cd7c99e0b2cfc0af317cea3366c583d820507e6d1f2ed4", size = 316128 }, - { url = "https://files.pythonhosted.org/packages/34/6f/f5d78c8e908750b9c3d5839eca2af5f6e99d6c7fe8a0498ef79a1af90bd8/bitarray-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f92462ea3888c99439f58f7561ecd5dd4cf8b8b1b259ccf5376667b8c46ee747", size = 339337 }, - { url = "https://files.pythonhosted.org/packages/0d/d3/f740b601eae4e28e22d8560877fe9881f1b7a96fcb23b186e8580d328929/bitarray-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3800f3c8c9780f281cf590543fd4b3278fea6988202273a260ecc58136895efb", size = 338607 }, - { url = "https://files.pythonhosted.org/packages/4e/81/b9451089eea0ef66996852d2694b0f5afc0a76b1bc45c9a4f8204ae8674d/bitarray-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a50a66fa34dd7f9dcdbc7602a1b7bf6f9ab030b4f43e892324193423d9ede180", size = 324788 }, - { url = "https://files.pythonhosted.org/packages/82/e8/80620fc60ad34bff647881a4f25c15b992c524e0f7af9c7c6c573b03556e/bitarray-3.6.0-cp313-cp313-win32.whl", hash = "sha256:afa24e5750c9b89ad5a7efef037efe49f4e339f20a94bf678c422c0c71e1207a", size = 137841 }, - { url = "https://files.pythonhosted.org/packages/3b/ee/303be88b847da29a067babc690e231d7838520dc1af57d14dad5a7ca095c/bitarray-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4c5e7edf1e7bcbde3b52058f171a411e2a24a081b3e951d685dfea4c3c383d5", size = 144820 }, - { url = "https://files.pythonhosted.org/packages/94/25/d98d86777779c422cdc1a295848dfdb04b967d5b54e6824c009bd69842cd/bitarray-3.6.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a763dd33d6e27c9b4db3f8089a5fa39179a8a3cf48ce702b24a857d7c621333c", size = 139790 }, - { url = "https://files.pythonhosted.org/packages/f7/37/4a85461ea71e0d735ec4e231a19057df156242f6b51c4d860367d8342ffc/bitarray-3.6.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8cf44b012e7493127ce7ca6e469138ac96b3295a117877d5469aabe7c8728d87", size = 136433 }, - { url = "https://files.pythonhosted.org/packages/27/f3/873ccde5d86369268bceda91056f1baa87f3920f504390c5fd7a451ac964/bitarray-3.6.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e297fd2e58afe17e33dd80c231c3a9d850279a2a8625aed1d39f9be9534809e", size = 144952 }, - { url = "https://files.pythonhosted.org/packages/cb/be/2cb43c165c6a239a9d662279f0be7ffb0903f6f889c1df3bafb3387fea44/bitarray-3.6.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fc8bc65f964c7278deb1b7a69379dab3ecc90095f252deb17365637ebb274d", size = 145693 }, - { url = "https://files.pythonhosted.org/packages/a9/86/1ffd91a77a1a5fa86bbd32d8c3c34bb066595574d3eb60c83a98f556d45f/bitarray-3.6.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa3c925502bd0b957a96a5619134bcdc0382ef73cffd40bad218ced3586bcf8d", size = 147466 }, - { url = "https://files.pythonhosted.org/packages/a9/ff/30a1d2492c1d259a17084015c7b82cc31c49dd60f47f3c7a7b3ee923f45b/bitarray-3.6.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9f7796959c9c036a115d34696563f75d4a2912d3b97c15c15f2a36bdd5496ce9", size = 143118 }, +version = "3.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/b6/282f5f0331b3877d4e79a8aa1cf63b5113a10f035a39bef1fa1dfe9e9e09/bitarray-3.7.1.tar.gz", hash = "sha256:795b1760418ab750826420ae24f06f392c08e21dc234f0a369a69cc00444f8ec", size = 150474 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/98/bafe556fe4d97a975fa5c31965aaa282388cc91073aca57a2de206745b11/bitarray-3.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a05982bb49c73463cb0f0f4bed2d8da82631708a2c2d1926107ba99651b419ec", size = 147651 }, + { url = "https://files.pythonhosted.org/packages/03/87/639c1e4d869ecd7c23d517c326bfee7ab43ade5d5bd0f6ad3373edc861a8/bitarray-3.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d30e7daaf228e3d69cdd8b02c0dd4199cec034c4b93c80109f56f4675a6db957", size = 143967 }, + { url = "https://files.pythonhosted.org/packages/24/e9/8248a05b35f3e3667ceb103febb0d687d3f7314e4692b2048d21ed943a4e/bitarray-3.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:160f449bb91686f8fc9984200e78b8d793b79e382decf7eb1dc9948d7c21b36f", size = 319901 }, + { url = "https://files.pythonhosted.org/packages/de/e8/47f9d8eebb793b6828baf76027b9eefc4e5e09f32b84a25821c4bc19c3c4/bitarray-3.7.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6542e1cfe060badd160cd383ad93a84871595c14bb05fb8129f963248affd946", size = 339005 }, + { url = "https://files.pythonhosted.org/packages/61/73/2c4695e5acd89d9904c5b3bea7b5b06df86dea15653eee6008881d18a632/bitarray-3.7.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b723f9d10f7d8259f010b87fa66e924bb4d67927d9dcff4526a755e9ee84fef4", size = 329495 }, + { url = "https://files.pythonhosted.org/packages/0f/d9/dc17b9f5b7b750dc9183db0520e197f1ca635dedd48e37ad00ca450d2fab/bitarray-3.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca4b6298c89b92d6b0a67dfc5f98d68ae92b08101d227263ef2033b9c9a03a72", size = 322141 }, + { url = "https://files.pythonhosted.org/packages/a7/45/8fb00265c1b0313070e0a4b09a2f585fd3ee174aaa5352d971069983c983/bitarray-3.7.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:567d6891cb1ddbfd0051fcff3cb1bb86efc82ec818d9c5f98c37d59c1d23cc96", size = 310422 }, + { url = "https://files.pythonhosted.org/packages/f6/77/04cb016694ae16ffe1a146f1a764b79e71f3ddbc7b9d78069594507c9762/bitarray-3.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:37a6a8382864a1defb5b370b66a635e04358c7334054457bbbb8645610cd95b2", size = 314796 }, + { url = "https://files.pythonhosted.org/packages/b5/4f/8e15934995c5362e645ea27d9521e6b29953dc9f8df59e74525c8022e347/bitarray-3.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:01e3ba46c2dee6d47a4ab22561a01d8ee6772f681defc9fcb357097a055e48cf", size = 311222 }, + { url = "https://files.pythonhosted.org/packages/f4/d2/9cc6df1ab5b9d10904bf78820e2427cf9b373376ca82af64a0b31eff7b31/bitarray-3.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:477b9456eb7d70f385dc8f097a1d66ee40771b62e47b3b3e33406dcfbc1c6a3b", size = 339685 }, + { url = "https://files.pythonhosted.org/packages/ed/6d/b79e5e545a928270445c6916cf2d7613a8a8434eee8de023c900a0a08e15/bitarray-3.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2965fd8ba31b04c42e4b696fad509dc5ab50663efca6eb06bb3b6d08587f3a09", size = 339660 }, + { url = "https://files.pythonhosted.org/packages/e9/33/8b836518ba16a85c75c177aa0d6658e843b4b0c1ec5994fb9f1b28e9440d/bitarray-3.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc76ad7453816318d794248fba4032967eaffd992d76e5d1af10ef9d46589770", size = 320079 }, + { url = "https://files.pythonhosted.org/packages/7b/8e/87603ccf798c99296fdb26b9297350f44f87cb2aced76d3b8b0446ac8cd2/bitarray-3.7.1-cp310-cp310-win32.whl", hash = "sha256:d3f38373d9b2629dedc559e647010541cc4ec4ad9bea560e2eb1017e6a00d9ef", size = 141228 }, + { url = "https://files.pythonhosted.org/packages/50/06/7003c5520d2bb36edb68b016b1a83ddd5946da67b9d9982b12a8ef68d706/bitarray-3.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:e39f5e85e1e3d7d84ac2217cd095b3678306c979e991532df47012880e02215d", size = 147988 }, + { url = "https://files.pythonhosted.org/packages/c6/0b/6fc7221d6d6508b2648f2b99dda9188dc46640023e6c2d3fb78070013901/bitarray-3.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac39319e6322c2c093a660c02cea6bb3b1ae53d049b573d4781df8896e443e04", size = 147645 }, + { url = "https://files.pythonhosted.org/packages/43/96/122ef83579cde311e77d5da284b71dfb5ab1c38250b6a97a4f4adae4ef5a/bitarray-3.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a43f4631ecb87bedc510568fef67db53f2a20c4a5953a9d1e07457e7b1d14911", size = 143971 }, + { url = "https://files.pythonhosted.org/packages/f6/f9/cd0e27f8399b930fcea8b87b36de0ba8c88e8f953dbc98e81ca322352d24/bitarray-3.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffd112646486a31ea5a45aa1eca0e2cd90b6a12f67e848e50349e324c24cc2e7", size = 327521 }, + { url = "https://files.pythonhosted.org/packages/35/ad/f64f4be628536404c9576a0a40b10f5304bb37a69fb6cb37987e9ae92782/bitarray-3.7.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db0441e80773d747a1ed9edfb9f75e7acb68ce8627583bbb6f770b7ec49f0064", size = 347583 }, + { url = "https://files.pythonhosted.org/packages/e6/82/98774e33b3286fd83c6e48f5fb4e362d39b531011b4e1dd5aeba9dfdd3b8/bitarray-3.7.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef5a99a8d1a5c47b4cf85925d1420fc4ee584c98be8efc548651447b3047242f", size = 338572 }, + { url = "https://files.pythonhosted.org/packages/02/cc/aadc3bf1382d9660f755d74b3275c866a20e01ad2062cc777b2378423e97/bitarray-3.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb7af369df317527d697c5bb37ab944bb9a17ea1a5e82e47d5c7c638f3ccdd6", size = 329984 }, + { url = "https://files.pythonhosted.org/packages/42/ba/f9db45b9d6d01793afe62190c3f58bfe1969bd5798612663225560c24d94/bitarray-3.7.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eda67136343db96752e58ef36ac37116f36cba40961e79fd0e9bd858f5a09b38", size = 318777 }, + { url = "https://files.pythonhosted.org/packages/5e/1b/18d11fe8f3192be5c2986d0faada5b3c9c0e43082ba031c12c75ebc64fd2/bitarray-3.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:79038bf1a7b13d243e51f4b6909c6997c2ba2bffc45bcae264704308a2d17198", size = 322772 }, + { url = "https://files.pythonhosted.org/packages/dc/20/3aaf1c21af0f8dca623d06f12ce44fb45f94c10c6550e8d2e57d811b1881/bitarray-3.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d12c45da97b2f31d0233e15f8d68731cfa86264c9f04b2669b9fdf46aaf68e1f", size = 318773 }, + { url = "https://files.pythonhosted.org/packages/b0/80/2d066264b1f3b3c495e12c55a9d0955733e890388d63ba75c408bb936fb7/bitarray-3.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:64d1143e90299ba8c967324840912a63a903494b1870a52f6675bda53dc332f7", size = 347391 }, + { url = "https://files.pythonhosted.org/packages/e6/4b/819d5614433881ae779a6b23dd74d399c790777e3f084a270851059a77b2/bitarray-3.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c4e04c12f507942f1ddf215cb3a08c244d24051cdd2ba571060166ce8a92be16", size = 347719 }, + { url = "https://files.pythonhosted.org/packages/52/63/a278c08f1e47711f71e396135c0d6d38811f551613b84af8ac7901bfaea9/bitarray-3.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ddc646cec4899a137c134b13818469e4178a251d77f9f4b23229267e3da78cfb", size = 328197 }, + { url = "https://files.pythonhosted.org/packages/aa/73/6a74193cf565b01747ebd7979752060128e6c1423378471b04d8ed28b6f0/bitarray-3.7.1-cp311-cp311-win32.whl", hash = "sha256:a23b5f13f9b292004e94b0b13fead4dae79c7512db04dc817ff2c2478298e04a", size = 141377 }, + { url = "https://files.pythonhosted.org/packages/13/03/7bbaadf90b282c7f1bc21c3c4855ce869d3ecd444071b1dab55baaec9328/bitarray-3.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:acc56700963f63307ac096689d4547e8061028a66bb78b90e42c5da2898898fb", size = 148203 }, + { url = "https://files.pythonhosted.org/packages/89/27/46b5b4dabecf84f750587cded3640658448d27c59f4dd2cbaa589085f43a/bitarray-3.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b99a0347bc6131046c19e056a113daa34d7df99f1f45510161bc78bc8461a470", size = 147349 }, + { url = "https://files.pythonhosted.org/packages/f9/1e/7f61150577127a1540136ba8a63ba17c661a17e721e03404fcd5833a4a05/bitarray-3.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d7e274ac1975e55ebfb8166cce27e13dc99120c1d6ce9e490d7a716b9be9abb5", size = 143922 }, + { url = "https://files.pythonhosted.org/packages/ca/b2/7c852472df8c644d05530bc0ad586fead5f23a9d176873c2c54f57e16b4e/bitarray-3.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b9a2eb7d2e0e9c2f25256d2663c0a2a4798fe3110e3ddbbb1a7b71740b4de08", size = 330277 }, + { url = "https://files.pythonhosted.org/packages/7b/38/681340eea0997c48ef2dbf1acb0786090518704ca32f9a2c3c669bdea08e/bitarray-3.7.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e15e70a3cf5bb519e2448524d689c02ff6bcd4750587a517e2bffee06065bf27", size = 349562 }, + { url = "https://files.pythonhosted.org/packages/c4/f4/6fc43f896af85c5b10a74b1d8a87c05915464869594131a2d7731707a108/bitarray-3.7.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c65257899bb8faf6a111297b4ff0066324a6b901318582c0453a01422c3bcd5a", size = 341249 }, + { url = "https://files.pythonhosted.org/packages/89/c7/1f71164799cacd44964ead87e1fc7e2f0ddec6d0519515a82d54eb8c8a13/bitarray-3.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38b0261483c59bb39ae9300ad46bf0bbf431ab604266382d986a349c96171b36", size = 332874 }, + { url = "https://files.pythonhosted.org/packages/95/cd/4d7c19064fa7fe94c2818712695fa186a1d0bb9c5cb0cf34693df81d3202/bitarray-3.7.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d2b1ed363a4ef5622dccbf7822f01b51195062c4f382b28c9bd125d046d0324c", size = 321107 }, + { url = "https://files.pythonhosted.org/packages/1e/d2/7d5ffe491c70614c0eb4a0186666efe925a02e25ed80ebd19c5fcb1c62e8/bitarray-3.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:dfde50ae55e075dcd5801e2c3ea0e749c849ed2cbbee991af0f97f1bdbadb2a6", size = 324999 }, + { url = "https://files.pythonhosted.org/packages/11/d9/95fb87ec72c01169dad574baf7bc9e0d2bb73975d7ea29a83920a38646f4/bitarray-3.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45660e2fabcdc1bab9699a468b312f47956300d41d6a2ea91c8f067572aaf38a", size = 321816 }, + { url = "https://files.pythonhosted.org/packages/6b/3d/57ac96bbd125df75219c59afa297242054c09f22548aff028a8cefa8f120/bitarray-3.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7b4a41dc183d7d16750634f65566205990f94144755a39f33da44c0350c3e1a8", size = 349342 }, + { url = "https://files.pythonhosted.org/packages/a9/14/d28f7456d2c3b3f7898186498b6d7fd3eecab267c300fb333fc2a8d55965/bitarray-3.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8b8e07374d60040b24d1a158895d9758424db13be63d4b2fe1870e37f9dec009", size = 350501 }, + { url = "https://files.pythonhosted.org/packages/bb/a4/0f803dc446e602b21e61315f5fa2cdec02a65340147b08f7efadba559f38/bitarray-3.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f31d8c2168bf2a52e4539232392352832c2296e07e0e14b6e06a44da574099ba", size = 331362 }, + { url = "https://files.pythonhosted.org/packages/c9/03/25e4c4b91a33f1eae0a9e9b2b11f1eaed14e37499abbde154ff33888f5f5/bitarray-3.7.1-cp312-cp312-win32.whl", hash = "sha256:fe1f1f4010244cb07f6a079854a12e1627e4fb9ea99d672f2ceccaf6653ca514", size = 141474 }, + { url = "https://files.pythonhosted.org/packages/25/53/98efa8ee389e4cbd91fc7c87bfebd4e11d6f8a027eb3f9be42d1addf1f51/bitarray-3.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:f41a4b57cbc128a699e9d716a56c90c7fc76554e680fe2962f49cc4d8688b051", size = 148458 }, + { url = "https://files.pythonhosted.org/packages/97/7f/16d59c041b0208bc1003fcfbf466f1936b797440e6119ce0adca7318af48/bitarray-3.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e62892645f6a214eefb58a42c3ed2501af2e40a797844e0e09ec1e400ce75f3d", size = 147343 }, + { url = "https://files.pythonhosted.org/packages/1a/fb/5add457d3faa0e17fde5e220bb33c0084355b9567ff9bcba2fe70fef3626/bitarray-3.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3092f6bbf4a75b1e6f14a5b1030e27c435f341afeb23987115e45a25cc68ba91", size = 143904 }, + { url = "https://files.pythonhosted.org/packages/95/b9/c5ab584bb8d0ba1ec72eaac7fc1e712294db77a6230c033c9b15a2de33ae/bitarray-3.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:851398428f5604c53371b72c5e0a28163274264ada4a08cd1eafe65fde1f68d0", size = 330206 }, + { url = "https://files.pythonhosted.org/packages/f0/cd/a4d95232a2374ce55e740fbb052a1e3a9aa52e96c7597d9152b1c9d79ecc/bitarray-3.7.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa05460dc4f57358680b977b4a254d331b24c8beb501319b998625fd6a22654b", size = 349372 }, + { url = "https://files.pythonhosted.org/packages/69/6c/8fb54cea100bd9358a7478d392042845800e809ab3a00873f2f0ae3d0306/bitarray-3.7.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ad0df7886cb9d6d2ff75e87d323108a0e32bdca5c9918071681864129ce8ea8", size = 341120 }, + { url = "https://files.pythonhosted.org/packages/bd/eb/dcbb1782bf93afa2baccbc1206bb1053f61fe999443e9180e7d9be322565/bitarray-3.7.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55c31bc3d2c9e48741c812ee5ce4607c6f33e33f339831c214d923ffc7777d21", size = 332759 }, + { url = "https://files.pythonhosted.org/packages/e2/f2/164aed832c5ece367d5347610cb7e50e5706ca1a882b9f172cb84669f591/bitarray-3.7.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44f468fb4857fff86c65bec5e2fb67067789e40dad69258e9bb78fc6a6df49e7", size = 320992 }, + { url = "https://files.pythonhosted.org/packages/35/35/fd51da63ad364d5c03690bb895e34b20c9bedce10c6d0b4d7ed7677c4b09/bitarray-3.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:340c524c7c934b61d1985d805bffe7609180fb5d16ece6ce89b51aa535b936f2", size = 324987 }, + { url = "https://files.pythonhosted.org/packages/a3/f3/3f4f31a80f343c6c3360ca4eac04f471bf009b6346de745016f8b4990bad/bitarray-3.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0751596f60f33df66245b2dafa3f7fbe13cb7ac91dd14ead87d8c2eec57cb3ed", size = 321816 }, + { url = "https://files.pythonhosted.org/packages/f5/60/26ce8cff96255198581cb88f9566820d6b3c262db4c185995cc5537b3d07/bitarray-3.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e501bd27c795105aaba02b5212ecd1bb552ca2ee2ede53e5a8cb74deee0e2052", size = 349354 }, + { url = "https://files.pythonhosted.org/packages/dc/f8/e2edda9c37ba9be5349beb145dcad14d8d339f7de293b4b2bd770227c5a7/bitarray-3.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fe2493d3f49e314e573022ead4d8c845c9748979b7eb95e815429fe947c4bde2", size = 350491 }, + { url = "https://files.pythonhosted.org/packages/c0/c5/b82dd6bd8699ad818c13ae02b6acfc6c38c9278af1f71005b5d0c5f29338/bitarray-3.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f1575cc0f66aa70a0bb5cb57c8d9d1b7d541d920455169c6266919bf804dc20", size = 331367 }, + { url = "https://files.pythonhosted.org/packages/51/82/03613ad262d6e2a76b906dd279de26694910a95e4ed8ebde57c9fd3f3aa7/bitarray-3.7.1-cp313-cp313-win32.whl", hash = "sha256:da3dfd2776226e15d3288a3a24c7975f9ee160ba198f2efa66bc28c5ba76d792", size = 141481 }, + { url = "https://files.pythonhosted.org/packages/f1/7e/1730701a865fd1e4353900d5821c96e68695aed88d121f8783aea14c4e74/bitarray-3.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f604bffd06b170637f8a48ddcf42074ed1e1980366ac46058e065ce04bfe2a", size = 148450 }, + { url = "https://files.pythonhosted.org/packages/58/1f/80316ba4ed605d005efeb0b09c97cecde2c66ac4deae9d1d698670e1525f/bitarray-3.7.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c9bf2bf29854f165a47917b8782b6cf3a7d602971bf454806208d0cbb96f797a", size = 143423 }, + { url = "https://files.pythonhosted.org/packages/9e/c3/52a491e18ba41911455f145906b20898fe8e7955d0bcc5b20207bf2aba09/bitarray-3.7.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:002b73bf4a9f7b3ecb02260bd4dd332a6ee4d7f74ee9779a1ef342a36244d0cf", size = 139870 }, + { url = "https://files.pythonhosted.org/packages/46/df/4674d16f39841fc71db6ecc6298390cbb91a7dd8c4eccd55248a4ddced06/bitarray-3.7.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:481239cd0966f965c2b8fa78b88614be5f12a64e7773bb5feecc567d39bb2dd5", size = 148773 }, + { url = "https://files.pythonhosted.org/packages/9b/85/9cd8bc811ab446491a5bdc47a70d6d51adb21e3b005b549d2fd5e04f5c7f/bitarray-3.7.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f583a1fb180a123c00064fab1a3bfb9d43e574b6474be1be3f6469e0331e3e2e", size = 149609 }, + { url = "https://files.pythonhosted.org/packages/ea/84/e413c51313a4093ed67f657d21519c5fc592bdb9129c0ab8c7bad226e2b8/bitarray-3.7.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3db0648536f3e08afa7ceb928153c39913f98fd50a5c3adf92a4d0d4268f213e", size = 151343 }, + { url = "https://files.pythonhosted.org/packages/a5/4f/921176e539866a8f7428d92962861bbfa6104f2cea0cbdd578abe5768a83/bitarray-3.7.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3875578748b484638f6ea776f534e9088cfb15eee131aac051036cba40fd5d05", size = 146847 }, ] [[package]] @@ -118,39 +118,51 @@ wheels = [ [[package]] name = "cbor2" -version = "5.6.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/aa/ba55b47d51d27911981a18743b4d3cebfabccbb0598c09801b734cec4184/cbor2-5.6.5.tar.gz", hash = "sha256:b682820677ee1dbba45f7da11898d2720f92e06be36acec290867d5ebf3d7e09", size = 100886 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/f2/eada1d3fcaeddbd0f9a25381eed6b150f2858cf615d33ef7c14f9f86218a/cbor2-5.6.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e16c4a87fc999b4926f5c8f6c696b0d251b4745bc40f6c5aee51d69b30b15ca2", size = 66473 }, - { url = "https://files.pythonhosted.org/packages/62/d8/33ec188eeff9c5e2e8ee8e214d15a0edf8fd001eefa01730e27834cfd410/cbor2-5.6.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87026fc838370d69f23ed8572939bd71cea2b3f6c8f8bb8283f573374b4d7f33", size = 67421 }, - { url = "https://files.pythonhosted.org/packages/fa/88/9b5fc312f21a10e048c348cead933e9f891cc09a295757957d8a5726641f/cbor2-5.6.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a88f029522aec5425fc2f941b3df90da7688b6756bd3f0472ab886d21208acbd", size = 253856 }, - { url = "https://files.pythonhosted.org/packages/bb/41/debb6f35d240caa8078a4a27c03e7eed09737575c564d619e1ee0e829616/cbor2-5.6.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9d15b638539b68aa5d5eacc56099b4543a38b2d2c896055dccf7e83d24b7955", size = 242074 }, - { url = "https://files.pythonhosted.org/packages/0b/ba/5c07b001bb26bd91d96e4b3b720676a909e1acdbaf7d250a47c5d7a5a0f7/cbor2-5.6.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:47261f54a024839ec649b950013c4de5b5f521afe592a2688eebbe22430df1dc", size = 241723 }, - { url = "https://files.pythonhosted.org/packages/b2/d5/27eced2cee4725bb876875f26d659063e2b7750d0dfb7572f451b569c4d1/cbor2-5.6.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:559dcf0d897260a9e95e7b43556a62253e84550b77147a1ad4d2c389a2a30192", size = 240534 }, - { url = "https://files.pythonhosted.org/packages/20/55/bd657fdedb0d046762f9bf567b262b233175995d40302cdaa71d9aadcec8/cbor2-5.6.5-cp310-cp310-win_amd64.whl", hash = "sha256:5b856fda4c50c5bc73ed3664e64211fa4f015970ed7a15a4d6361bd48462feaf", size = 66291 }, - { url = "https://files.pythonhosted.org/packages/bd/b7/ef045245180510305648fd604244d3bb1ecf1b20de68f42ab5bc20198024/cbor2-5.6.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:863e0983989d56d5071270790e7ed8ddbda88c9e5288efdb759aba2efee670bc", size = 66452 }, - { url = "https://files.pythonhosted.org/packages/41/20/5a9d93f86b1e8fd9d9db33aff39c0e3a8459e0803ec24bd837d8b56d4a1d/cbor2-5.6.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5cff06464b8f4ca6eb9abcba67bda8f8334a058abc01005c8e616728c387ad32", size = 67421 }, - { url = "https://files.pythonhosted.org/packages/0f/1e/2010f6d02dd117df88df64baf3eeca6aa6614cc81bdd6bfabf615889cf1f/cbor2-5.6.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4c7dbcdc59ea7f5a745d3e30ee5e6b6ff5ce7ac244aa3de6786391b10027bb3", size = 260756 }, - { url = "https://files.pythonhosted.org/packages/e1/84/e177d9bef4749d14f31c513b25e341ac84e403e2ffa2bde562eac9e6184b/cbor2-5.6.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34cf5ab0dc310c3d0196caa6ae062dc09f6c242e2544bea01691fe60c0230596", size = 249210 }, - { url = "https://files.pythonhosted.org/packages/38/75/ebfdbb281104b46419fe7cb65979de9927b75acebcb6afa0af291f728cd2/cbor2-5.6.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6797b824b26a30794f2b169c0575301ca9b74ae99064e71d16e6ba0c9057de51", size = 249138 }, - { url = "https://files.pythonhosted.org/packages/b2/1e/12d887fb1a8227a16181eeec5d43057e251204626d73e1c20a77046ac1b1/cbor2-5.6.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:73b9647eed1493097db6aad61e03d8f1252080ee041a1755de18000dd2c05f37", size = 247156 }, - { url = "https://files.pythonhosted.org/packages/6f/76/478c12193de9517ce691bb8a3f7c00eafdd6a1bc3f7f23282ecdd99d02ec/cbor2-5.6.5-cp311-cp311-win_amd64.whl", hash = "sha256:6e14a1bf6269d25e02ef1d4008e0ce8880aa271d7c6b4c329dba48645764f60e", size = 66319 }, - { url = "https://files.pythonhosted.org/packages/57/af/84ced14c541451696825b7b8ccbb7668f688372ad8ee74aaca4311e79672/cbor2-5.6.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e25c2aebc9db99af7190e2261168cdde8ed3d639ca06868e4f477cf3a228a8e9", size = 67553 }, - { url = "https://files.pythonhosted.org/packages/f2/d6/f63a840c68fed4de67d5441947af2dc695152cc488bb0e57312832fb923a/cbor2-5.6.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fde21ac1cf29336a31615a2c469a9cb03cf0add3ae480672d4d38cda467d07fc", size = 67569 }, - { url = "https://files.pythonhosted.org/packages/77/ac/5fb79db6e882ec29680f4a974d35c098020a1b4709cad077667a8c3f4676/cbor2-5.6.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8947c102cac79d049eadbd5e2ffb8189952890df7cbc3ee262bbc2f95b011a9", size = 276610 }, - { url = "https://files.pythonhosted.org/packages/cf/cb/70751377d94112001d46c311b5c40b45f34863dfa78a6bc71b71f40c8c7f/cbor2-5.6.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38886c41bebcd7dca57739439455bce759f1e4c551b511f618b8e9c1295b431b", size = 270004 }, - { url = "https://files.pythonhosted.org/packages/f1/90/08800367e920aef31b93bd7b0cd6fadcb3a3f2243f4ed77a0d1c76f22b99/cbor2-5.6.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae2b49226224e92851c333b91d83292ec62eba53a19c68a79890ce35f1230d70", size = 264913 }, - { url = "https://files.pythonhosted.org/packages/a8/9c/76b11a5ea7548bccb0dfef3e8fb3ede48bfeb39348f0c217519e0c40d33a/cbor2-5.6.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2764804ffb6553283fc4afb10a280715905a4cea4d6dc7c90d3e89c4a93bc8d", size = 266751 }, - { url = "https://files.pythonhosted.org/packages/10/18/3866693a87c90cb12f7942e791d0f03a40ba44887dde7b7fc85319647efe/cbor2-5.6.5-cp312-cp312-win_amd64.whl", hash = "sha256:a3ac50485cf67dfaab170a3e7b527630e93cb0a6af8cdaa403054215dff93adf", size = 66739 }, - { url = "https://files.pythonhosted.org/packages/2b/69/77e93caae71d1baee927c9762e702c464715d88073133052c74ecc9d37d4/cbor2-5.6.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f0d0a9c5aabd48ecb17acf56004a7542a0b8d8212be52f3102b8218284bd881e", size = 67647 }, - { url = "https://files.pythonhosted.org/packages/84/83/cb941d4fd10e4696b2c0f6fb2e3056d9a296e5765b2000a69e29a507f819/cbor2-5.6.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61ceb77e6aa25c11c814d4fe8ec9e3bac0094a1f5bd8a2a8c95694596ea01e08", size = 67657 }, - { url = "https://files.pythonhosted.org/packages/5c/3f/e16a1e29994483c751b714cdf61d2956290b0b30e94690fa714a9f155c5c/cbor2-5.6.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97a7e409b864fecf68b2ace8978eb5df1738799a333ec3ea2b9597bfcdd6d7d2", size = 275863 }, - { url = "https://files.pythonhosted.org/packages/64/04/f64bda3eea649fe6644c59f13d0e1f4666d975ce305cadf13835233b2a26/cbor2-5.6.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f6d69f38f7d788b04c09ef2b06747536624b452b3c8b371ab78ad43b0296fab", size = 269131 }, - { url = "https://files.pythonhosted.org/packages/f4/8d/0d5ad3467f70578b032b3f52eb0f01f0327d5ae6b1f9e7d4d4e01a73aa95/cbor2-5.6.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f91e6d74fa6917df31f8757fdd0e154203b0dd0609ec53eb957016a2b474896a", size = 264728 }, - { url = "https://files.pythonhosted.org/packages/77/cb/9b4f7890325eaa374c21fcccfee61a099ccb9ea0bc0f606acf7495f9568c/cbor2-5.6.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5ce13a27ef8fddf643fc17a753fe34aa72b251d03c23da6a560c005dc171085b", size = 266314 }, - { url = "https://files.pythonhosted.org/packages/a8/cd/793dc041395609f5dd1edfdf0aecde504dc0fd35ed67eb3b2db79fb8ef4d/cbor2-5.6.5-cp313-cp313-win_amd64.whl", hash = "sha256:54c72a3207bb2d4480c2c39dad12d7971ce0853a99e3f9b8d559ce6eac84f66f", size = 66792 }, - { url = "https://files.pythonhosted.org/packages/9b/ef/1c4698cac96d792005ef0611832f38eaee477c275ab4b02cbfc4daba7ad3/cbor2-5.6.5-py3-none-any.whl", hash = "sha256:3038523b8fc7de312bb9cdcbbbd599987e64307c4db357cd2030c472a6c7d468", size = 23752 }, +version = "5.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/89/01df16cdc9c60c07956756c90fe92c684021003079e358a78e213bce45a2/cbor2-5.7.0.tar.gz", hash = "sha256:3f6d843f4db4d0ec501c46453c22a4fbebb1abfb5b740e1bcab34c615cd7406b", size = 102374 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/94/92870469f92b53f7d18a6a6def540ecd9deec0cc25448b0d28b1847a7820/cbor2-5.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:305edac16524df843d53ba086bc194c0975388e755ed177eb84e0324e3d705ec", size = 67903 }, + { url = "https://files.pythonhosted.org/packages/ea/cd/c6bbf2c11f48f08cbbd31647ce2b6b081f25e4723d5260fde0a6520a6e2f/cbor2-5.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e00d7250e528a9a1bfd3b294799bdae96c158f72d95be58a3fbf97dab2467bbe", size = 68780 }, + { url = "https://files.pythonhosted.org/packages/7f/b7/17fe6e502b9884f3a7bdf6e9331688e290a546110dd14575aa09527b238f/cbor2-5.7.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b6e229b36147051ea1063ba0cd6225bfe6b5398ca0ac7b33fa91407ca75081", size = 254198 }, + { url = "https://files.pythonhosted.org/packages/fd/fb/6cc1dd51beb35f899f9f6621fd0df9ec733e1f4c27f334212107f7a05822/cbor2-5.7.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:06f8e9d8fa6d0ed7b8a81f4613a60e740be9bd5087de620e3b64007832dfd815", size = 247202 }, + { url = "https://files.pythonhosted.org/packages/34/2d/e51cd823cb918eed3180bbe0169955ccefa03817c0e4788024e9e56d3657/cbor2-5.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90c1236e5bfad19493183f9a9ecd3705e3ad3eb02fce6d6381c12ece86147b15", size = 250001 }, + { url = "https://files.pythonhosted.org/packages/de/45/79a0320a4562537df3e589526d636963a06a95a66d7b6c366bdc8d9b3755/cbor2-5.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:afc6c6611b7529136ea4a04cf8711786640b687859c9688ae172726c305a83a1", size = 243743 }, + { url = "https://files.pythonhosted.org/packages/9b/6f/73baf2ba006fb297c7d022858352ae393ee3837be2d7cfa01b6d2519492b/cbor2-5.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:be635795908d59f46303ab266fc6f3aa6543c742fa112fd1cd2e5307b91c6de4", size = 68045 }, + { url = "https://files.pythonhosted.org/packages/40/87/0c7c8d27738e8d7dca49dfb4092ef7fdfdec7ca64f27e344b79d8a80bd92/cbor2-5.7.0-cp310-cp310-win_arm64.whl", hash = "sha256:16a21233f11874b7067b136cb2910333b36c1dee455a42a7c8473a104753cf4a", size = 63963 }, + { url = "https://files.pythonhosted.org/packages/45/a4/97c1b5eaf28129543ae6f90b9afc244241f5bb1701a2cf9368a2de9eb258/cbor2-5.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73532e725501915f95d589500e0a11813c9a3fd735d3cdb6c4dd320e6a2f12e1", size = 67895 }, + { url = "https://files.pythonhosted.org/packages/91/60/cb2fa00871e94e75d9b84f4edd173b6644af080333c00712e8e31bd201fe/cbor2-5.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b785d954879e3800a074efa45c882a1cc6459476ab0d354c74e1dca92b17ede3", size = 68783 }, + { url = "https://files.pythonhosted.org/packages/6d/52/6092e45a6b164d22bc2828aa14f422fbcf5af4bd9201a3e27b62b512f828/cbor2-5.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fc1005b412ace94bbf905a8c4214e639557568551d9b5474645789e976e91e4", size = 261081 }, + { url = "https://files.pythonhosted.org/packages/0f/12/9eef73bcfcdd110df7e585f8034399d1cf2006dc7576487e0509ba50e4f8/cbor2-5.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d981d691dd721dd712fc824d04a01347955a206107fbee1d75803fa58de172c7", size = 254132 }, + { url = "https://files.pythonhosted.org/packages/8c/e1/00bc92353ecacef0b8eaba285168ca517c18539c12ddebe06a9bbab03e47/cbor2-5.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b3b9730928163d02e7f1267e48a39ed75560ff3e56cdc6768d670d3e96028623", size = 256825 }, + { url = "https://files.pythonhosted.org/packages/25/01/454c93f360c404a3e6531ce0e2e4cc5818c5746a10b1cb295b6abd3057cd/cbor2-5.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:01cc4d840943b4c2e468b8560830235d044a8cb45e5d66ba3ae933c3e21b8d49", size = 250639 }, + { url = "https://files.pythonhosted.org/packages/94/1a/87be8d39d934810b095024bd22017c679abd51d89cb62d179b89327f491e/cbor2-5.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:6abe31e742ccf966778d77ce99d7c6136ca0f8424446dfdabcc3491f015e84d4", size = 68085 }, + { url = "https://files.pythonhosted.org/packages/52/00/dce08a1549e2a4008cecb0069e8d8de45779f60852aca12a5e7d0039ce41/cbor2-5.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:c48dff8f6aacd76fc0680c48ef35e5912e3d758a9f41305a35e847f382b60eea", size = 63969 }, + { url = "https://files.pythonhosted.org/packages/b1/b1/d54c41b1bc71b8dea0bad3409d2a497df35f7b5ae5db70c1cc9ebc8d556d/cbor2-5.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7ad36f0537b75c1aa2c7a462cbdbeec5e8ba02802ea985e0b9fe5deee3b946f4", size = 69020 }, + { url = "https://files.pythonhosted.org/packages/f4/e0/45368d5d78b520caaa9ca5a09f55365bc9933d43bce978a528922654ca9f/cbor2-5.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5fc9b335cf28e63d9eed4ae03d1e8f90f1a6b287cabc8d29bfddf73fa70643e9", size = 68950 }, + { url = "https://files.pythonhosted.org/packages/1e/6a/9aed5b716407c1d48425ba55c6022a01a9abdbf58a691f50416461fa371d/cbor2-5.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16bea83598a1eeedbd50c2e9fdf3685bae78ca9d9ec8cd8010777db14a315578", size = 285685 }, + { url = "https://files.pythonhosted.org/packages/a8/6e/3499eaa2b858c7695a447b6311303f06ffc90fc2c45851337121661f1f5c/cbor2-5.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e837825a16e60ace6e284095aa9fbe504bf87a8f4494bf7d95931e37fb01a70", size = 284948 }, + { url = "https://files.pythonhosted.org/packages/d1/3e/ae67866ef65717665e0acf2873d466c5d4a1d965b0d0348f2269b73f28fb/cbor2-5.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:27396c5e275ff7c7cd87fe8aaadf781e6194903921f250934af7c86d5efec82e", size = 276375 }, + { url = "https://files.pythonhosted.org/packages/b6/3d/2f8e9671111661dd571de206344ecd7706f6d458aab191e06834c89aa58e/cbor2-5.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c84bfef78c4e9c81eb0a10cec340222ba4e39498a63fc2e3d5f982a3f4efa4a7", size = 277680 }, + { url = "https://files.pythonhosted.org/packages/85/03/27a9fefa4e084c1129d7180727791a166629fdae39e0609508401d322626/cbor2-5.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:f64270a24aaadb15dd31cbd64a98d99fca8e0398a65b1570ba07f3c259eb5516", size = 68354 }, + { url = "https://files.pythonhosted.org/packages/25/d9/b856d078696542a0d7486d1ece5c936e937bebe5b114674db18d76feb131/cbor2-5.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:73ef321d7b580f08c9fadc41c3d2a218aa3f01e163be9793c6969aadee07f57a", size = 63896 }, + { url = "https://files.pythonhosted.org/packages/5c/2f/25da2b08f7a3d7b3f72e678a373092619821ab706f3f720d29e567a426df/cbor2-5.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7654e77b7f6be029fb37a074b175483a4a8ae3fe5e2a91008926625aa91aef2c", size = 69046 }, + { url = "https://files.pythonhosted.org/packages/4b/b5/d324166a5a1feed61aeb32fed70182306796b67cedaf65c91671c8674ea2/cbor2-5.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9bd76624b090faa6900739025d798a4e3130da80dbae15391b42b3d4672a4022", size = 69061 }, + { url = "https://files.pythonhosted.org/packages/1f/f9/180e953da537602d8530910f5a5f76c3d7215829d145d93f97fa43324dd7/cbor2-5.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:428d58b54a7b32ede869e79c294d686f826dcfdab9de7f92135dd3ce12e313b8", size = 284642 }, + { url = "https://files.pythonhosted.org/packages/17/eb/7d79831a5081d25002e36a1b2685210ae8783582d1a99fae350b2b1b899c/cbor2-5.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a91b6912e2ff64f33464f67ec6528cf2e26c06a5f3cc3fb1954f94aa58d68670", size = 283690 }, + { url = "https://files.pythonhosted.org/packages/38/43/1403610711ea6b9b957d86bd15fd0585a3917a3d9f8bafbb2cb1ad016361/cbor2-5.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9faeec4525fe3103a71f0fd3d6fe9a49ea6ff4ade8cb7cf1c395001b906a01e5", size = 276305 }, + { url = "https://files.pythonhosted.org/packages/77/06/df4a5c7c16df3b604bd560234aff686da443bf70a124c5e3f80dff954e5a/cbor2-5.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:361315ccd8022c44bb501877fd9b236479c975f1a7aed69c8541bd609c0a8908", size = 277416 }, + { url = "https://files.pythonhosted.org/packages/84/aa/62288bac4e501e25d04d50bb79ac46d4a6678ff9545941436a702c654eba/cbor2-5.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:293c4a5d6a9a69fcecf595a47430dc3b11f4a3355089b1fe300d0ac48c5776c5", size = 68378 }, + { url = "https://files.pythonhosted.org/packages/b6/d6/8358c144767731ffa03c16bb1222b59cb3be632833c70a2132cbe2ed8300/cbor2-5.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:52d6e1a9b2f4475540063d7b966b1b2e93ac497e08ab9a1514fd6330f8db5b4c", size = 63966 }, + { url = "https://files.pythonhosted.org/packages/99/32/b653a2a3cfb283bdf0539dbd79d3bafa528aaa26fbe44796897d167e733d/cbor2-5.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f4f0464425ff809b1dd737db8c65a937516aba5eb3794cb1433f7eb8eb7a6535", size = 68993 }, + { url = "https://files.pythonhosted.org/packages/c9/90/79d38f7f645a33e44b87f9333f74c04d01006a11f5291d2e8686815fe731/cbor2-5.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:866d993ebc9c4e4018ab001503dafc4145bb6ec91e1eddf12b8d7b6898021201", size = 69248 }, + { url = "https://files.pythonhosted.org/packages/46/ca/59d65f12ef14c54c564f0e4363d9dd049a90d5b0e2a0dab0183062268a36/cbor2-5.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc7a11433ea1c45b6d47484bef99e822fd8a40b4cfbcdc1e00378a7e8704e317", size = 283739 }, + { url = "https://files.pythonhosted.org/packages/19/51/5da8661b1aa7a4b7afe06724994b23eca6f7912d2cca705721dbd4aa764a/cbor2-5.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e33242570cb4542302dcb6cf429cc9abe315ff7ebb370de2828eed22a8b00fe8", size = 281246 }, + { url = "https://files.pythonhosted.org/packages/d4/2f/565f5f215a9d4211c23e94c5b1761d697d248603ae11ecf83a9a70e99382/cbor2-5.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:855fe80517071028a5804a29b607864b8d90bbb2223331ab2d8cae94b979d61f", size = 275442 }, + { url = "https://files.pythonhosted.org/packages/84/11/307a558f6ddc3bd0fc539ac65696acb0253554c88bab5da7d459706eb20e/cbor2-5.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:87170260845c2ea3d74288f667e0bc81c8a6bbc72ff60265d19c59b3e76be266", size = 275372 }, + { url = "https://files.pythonhosted.org/packages/92/f0/960b7050a53b8d60f92e6e4c1ce670f9c50ab2ff48468e83b2bef0399b38/cbor2-5.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:a2b591904555e51843c95776df2d6b161226af045e655f464c101d8ad8708e99", size = 70188 }, + { url = "https://files.pythonhosted.org/packages/a7/83/51805084b6208529f82e5a52261468a56b758728153ee2400c421fa845f4/cbor2-5.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:4460164ffd0ceaf8cc3f5597e73dd99fd781541c7bba0ea64ac93043bf08bb6a", size = 66162 }, + { url = "https://files.pythonhosted.org/packages/41/cc/0ce73676d2a0c9e5a9330c301940c50eb325dacf5f6d9690fd43a8817fe9/cbor2-5.7.0-py3-none-any.whl", hash = "sha256:a871e7a6f7cba1ddb02503ea974f15f6524c95078fbfe0b860fd4193d7c8f27a", size = 23828 }, ] [[package]] @@ -164,119 +176,122 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818 }, - { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649 }, - { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045 }, - { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356 }, - { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471 }, - { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317 }, - { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368 }, - { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491 }, - { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695 }, - { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849 }, - { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091 }, - { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445 }, - { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782 }, - { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794 }, - { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846 }, - { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350 }, - { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657 }, - { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260 }, - { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164 }, - { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571 }, - { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952 }, - { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959 }, - { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030 }, - { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015 }, - { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106 }, - { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402 }, - { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936 }, - { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790 }, - { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924 }, - { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626 }, - { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567 }, - { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957 }, - { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408 }, - { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399 }, - { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815 }, - { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537 }, - { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565 }, - { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357 }, - { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776 }, - { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622 }, - { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435 }, - { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653 }, - { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231 }, - { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243 }, - { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442 }, - { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147 }, - { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057 }, - { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454 }, - { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174 }, - { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166 }, - { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064 }, - { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641 }, - { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 }, +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695 }, + { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153 }, + { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428 }, + { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627 }, + { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388 }, + { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077 }, + { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631 }, + { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210 }, + { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739 }, + { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825 }, + { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452 }, + { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483 }, + { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520 }, + { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876 }, + { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083 }, + { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295 }, + { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379 }, + { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018 }, + { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430 }, + { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600 }, + { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616 }, + { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108 }, + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655 }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223 }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366 }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104 }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830 }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854 }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670 }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501 }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173 }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822 }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543 }, + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326 }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008 }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196 }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819 }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350 }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644 }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468 }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187 }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699 }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580 }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366 }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342 }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995 }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640 }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636 }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939 }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580 }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870 }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797 }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224 }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086 }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400 }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175 }, ] [[package]] name = "ckzg" -version = "2.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/55/df/f6db8e83bd4594c1ea685cd37fb81d5399e55765aae16d1a8a9502598f4e/ckzg-2.1.1.tar.gz", hash = "sha256:d6b306b7ec93a24e4346aa53d07f7f75053bc0afc7398e35fa649e5f9d48fcc4", size = 1120500 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/4b/cd25e857cdf46a752e97c530fe2582fae77c4d16c29fff5a15b7a998e2fd/ckzg-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b9825a1458219e8b4b023012b8ef027ef1f47e903f9541cbca4615f80132730", size = 116377 }, - { url = "https://files.pythonhosted.org/packages/7e/bc/5dfef36589545f797245ecacb54ed2acfa75507f63cfe12182d1277a88f1/ckzg-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e2a40a3ba65cca4b52825d26829e6f7eb464aa27a9e9efb6b8b2ce183442c741", size = 100208 }, - { url = "https://files.pythonhosted.org/packages/b7/52/96f0e3affbed321dc52b9b4ca13e0fb594da572d1f8edc47378fe48d8e9a/ckzg-2.1.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1d753fbe85be7c21602eddc2d40e0915e25fce10329f4f801a0002a4f886cc7", size = 174800 }, - { url = "https://files.pythonhosted.org/packages/dc/21/b1bc07cc8e5ed32817e89b054e2399d38775d92ff2d55e24bf233f537c02/ckzg-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d76b50527f1d12430bf118aff6fa4051e9860eada43f29177258b8d399448ea", size = 160847 }, - { url = "https://files.pythonhosted.org/packages/c9/5a/97b173d4ff9bce798031beb12b340c4f1729eaaddd07f69f368f843db28e/ckzg-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44c8603e43c021d100f355f50189183135d1df3cbbddb8881552d57fbf421dde", size = 169712 }, - { url = "https://files.pythonhosted.org/packages/7b/52/48be78c07f362438e189e2fbea7df8543290c3ee99845442549c8dc5405b/ckzg-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:38707a638c9d715b3c30b29352b969f78d8fc10faed7db5faf517f04359895c0", size = 172942 }, - { url = "https://files.pythonhosted.org/packages/13/42/3cfcd6cbdfb9030b9071d5e413a458f93883e47ad4a7d8d4c1d57608e57d/ckzg-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:52c4d257bdcbe822d20c5cd24c8154ec5aac33c49a8f5a19e716d9107a1c8785", size = 187707 }, - { url = "https://files.pythonhosted.org/packages/eb/54/d43bc3a2de486fb8be29ffedc3ec80f5726765ee4fa78beabe2ab2440f93/ckzg-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1507f7bfb9bcf51d816db5d8d0f0ed53c8289605137820d437b69daea8333e16", size = 182505 }, - { url = "https://files.pythonhosted.org/packages/21/43/5bcd2b7630732b532006572fbb8d64a29f69530c630ae4811167a2a0dc3b/ckzg-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:d02eaaf4f841910133552b3a051dea53bcfe60cd98199fc4cf80b27609d8baa2", size = 98822 }, - { url = "https://files.pythonhosted.org/packages/95/2c/44120b2d9dcb0246d67a1f28b9eaa625c499014d4d42561467e28eedd285/ckzg-2.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:465e2b71cf9dc383f66f1979269420a0da9274a3a9e98b1a4455e84927dfe491", size = 116378 }, - { url = "https://files.pythonhosted.org/packages/23/88/c5b89ba9a730fee5e089be9e0c7048fb6707c1a0e4b6c30fcf725c3eef44/ckzg-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ee2f26f17a64ad0aab833d637b276f28486b82a29e34f32cf54b237b8f8ab72d", size = 100202 }, - { url = "https://files.pythonhosted.org/packages/ee/11/b0a473e80346db52ad9a629bc9fd8f773c718ed78932ea3a70392306ffc3/ckzg-2.1.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99cc2c4e9fb8c62e3e0862c7f4df9142f07ba640da17fded5f6e0fd09f75909f", size = 175595 }, - { url = "https://files.pythonhosted.org/packages/52/fa/17a7e125d07a96dd6dce4db7262231f7583856b2be5d5b7df59e04bfa188/ckzg-2.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773dd016693d74aca1f5d7982db2bad7dde2e147563aeb16a783f7e5f69c01fe", size = 161681 }, - { url = "https://files.pythonhosted.org/packages/57/bd/46d6b90bf53da732f9adab7593d132a0834ed4f2f7659b4c7414d8f78d39/ckzg-2.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af2b2144f87ba218d8db01382a961b3ecbdde5ede4fa0d9428d35f8c8a595ba", size = 170471 }, - { url = "https://files.pythonhosted.org/packages/9d/98/113c7704749d037d75f23240ffc5c46dfe8416de574b946438587835715f/ckzg-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8f55e63d3f7c934a2cb53728ed1d815479e177aca8c84efe991c2920977cff6", size = 173595 }, - { url = "https://files.pythonhosted.org/packages/2f/d5/05fca6dcb5a19327be491157794eafc3d7498daf615c2ff5a5b745852945/ckzg-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ecb42aaa0ffa427ff14a9dde9356ba69e5ae6014650b397af55b31bdae7a9b6e", size = 188417 }, - { url = "https://files.pythonhosted.org/packages/72/36/131ae2dfc82d0fdc98fae8e3bbfe71ff14265bb434b23bd07b585afc6d61/ckzg-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5a01514239f12fb1a7ad9009c20062a4496e13b09541c1a65f97e295da648c70", size = 183286 }, - { url = "https://files.pythonhosted.org/packages/c5/6a/d371b27024422b25228fc11fa57b1ba7756a94cc9fb0c75da292c235fdaa/ckzg-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:6516b9684aae262c85cf7fddd8b585b8139ad20e08ec03994e219663abbb0916", size = 98819 }, - { url = "https://files.pythonhosted.org/packages/93/a1/9c07513dd0ea01e5db727e67bd2660f3b300a4511281cdb8d5e04afa1cfd/ckzg-2.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c60e8903344ce98ce036f0fabacce952abb714cad4607198b2f0961c28b8aa72", size = 116421 }, - { url = "https://files.pythonhosted.org/packages/27/04/b69a0dfbb2722a14c98a52973f276679151ec56a14178cb48e6f2e1697bc/ckzg-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4299149dd72448e5a8d2d1cc6cc7472c92fc9d9f00b1377f5b017c089d9cd92", size = 100216 }, - { url = "https://files.pythonhosted.org/packages/2e/24/9cc850d0b8ead395ad5064de67c7c91adacaf31b6b35292ab53fbd93270b/ckzg-2.1.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:025dd31ffdcc799f3ff842570a2a6683b6c5b01567da0109c0c05d11768729c4", size = 175764 }, - { url = "https://files.pythonhosted.org/packages/c0/c1/eb13ba399082a98b932f10b230ec08e6456051c0ce3886b3f6d8548d11ab/ckzg-2.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b42ab8385c273f40a693657c09d2bba40cb4f4666141e263906ba2e519e80bd", size = 161885 }, - { url = "https://files.pythonhosted.org/packages/57/c7/58baa64199781950c5a8c6139a46e1acff0f057a36e56769817400eb87fb/ckzg-2.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1be3890fc1543f4fcfc0063e4baf5c036eb14bcf736dabdc6171ab017e0f1671", size = 170757 }, - { url = "https://files.pythonhosted.org/packages/65/bd/4b8e1c70972c98829371b7004dc750a45268c5d3442d602e1b62f13ca867/ckzg-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b754210ded172968b201e2d7252573af6bf52d6ad127ddd13d0b9a45a51dae7b", size = 173761 }, - { url = "https://files.pythonhosted.org/packages/1f/32/c3fd1002f97ba3e0c5b1d9ab2c8fb7a6f475fa9b80ed9c4fa55975501a54/ckzg-2.1.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2f8fda87865897a269c4e951e3826c2e814427a6cdfed6731cccfe548f12b36", size = 188666 }, - { url = "https://files.pythonhosted.org/packages/e2/d9/91cf5a8169ee60c9397c975163cbca34432571f94facec5f8c0086bb47d8/ckzg-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:98e70b5923d77c7359432490145e9d1ab0bf873eb5de56ec53f4a551d7eaec79", size = 183652 }, - { url = "https://files.pythonhosted.org/packages/25/d4/8c9f6b852f99926862344b29f0c59681916ccfec2ac60a85952a369e0bca/ckzg-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:42af7bde4ca45469cd93a96c3d15d69d51d40e7f0d30e3a20711ebd639465fcb", size = 98816 }, - { url = "https://files.pythonhosted.org/packages/b7/9a/fa698b12e97452d11dd314e0335aae759725284ef6e1c1665aed56b1cd3e/ckzg-2.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e4edfdaf87825ff43b9885fabfdea408737a714f4ce5467100d9d1d0a03b673", size = 116426 }, - { url = "https://files.pythonhosted.org/packages/a1/a6/8cccd308bd11b49b40eecad6900b5769da117951cac33e880dd25e851ef7/ckzg-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:815fd2a87d6d6c57d669fda30c150bc9bf387d47e67d84535aa42b909fdc28ea", size = 100219 }, - { url = "https://files.pythonhosted.org/packages/30/0e/63573d816c1292b9a4d70eb6a7366b3593d29a977794039e926805a76ca0/ckzg-2.1.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c32466e809b1ab3ff01d3b0bb0b9912f61dcf72957885615595f75e3f7cc10e5", size = 175725 }, - { url = "https://files.pythonhosted.org/packages/86/f6/a279609516695ad3fb8b201098c669ba3b2844cbf4fa0d83a0f02b9bb29b/ckzg-2.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f11b73ccf37b12993f39a7dbace159c6d580aacacde6ee17282848476550ddbc", size = 161835 }, - { url = "https://files.pythonhosted.org/packages/39/e4/8cf7aef7dc05a777cb221e94046f947c6fe5317159a8dae2cd7090d52ef2/ckzg-2.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3b9433a1f2604bd9ac1646d3c83ad84a850d454d3ac589fe8e70c94b38a6b0", size = 170759 }, - { url = "https://files.pythonhosted.org/packages/0b/17/b34e3c08eb36bc67e338b114f289b2595e581b8bdc09a8f12299a1db5d2f/ckzg-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b7d7e1b5ea06234558cd95c483666fd785a629b720a7f1622b3cbffebdc62033", size = 173787 }, - { url = "https://files.pythonhosted.org/packages/2e/f0/aff87c3ed80713453cb6c84fe6fbb7582d86a7a5e4460fda2a497d47f489/ckzg-2.1.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9f5556e6675866040cc4335907be6c537051e7f668da289fa660fdd8a30c9ddb", size = 188722 }, - { url = "https://files.pythonhosted.org/packages/44/d9/1f08bfb8fd1cbb8c7513e7ad3fb76bbb5c3fb446238c1eba582276e4d905/ckzg-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55b2ba30c5c9daac0c55f1aac851f1b7bf1f7aa0028c2db4440e963dd5b866d6", size = 183686 }, - { url = "https://files.pythonhosted.org/packages/a3/ff/434f6d2893cbdfad00c20d17e9a52d426ca042f5e980d5c3db96bc6b6e15/ckzg-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:10d201601fc8f28c0e8cec3406676797024dd374c367bbeec5a7a9eac9147237", size = 98817 }, - { url = "https://files.pythonhosted.org/packages/30/b3/a0c7d7ba6e669cf04605dc0329173db62fc1fe3c488761755cc01e5e1b4d/ckzg-2.1.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:375918e25eafb9bafe5215ab91698504cba3fe51b4fe92f5896af6c5663f50c6", size = 113191 }, - { url = "https://files.pythonhosted.org/packages/f2/b9/a6cf403b8528d18d7d9154e28381a397bf466c86aa8e0b3327cffdde5749/ckzg-2.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:38b3b7802c76d4ad015db2b7a79a49c193babae50ee5f77e9ac2865c9e9ddb09", size = 96207 }, - { url = "https://files.pythonhosted.org/packages/63/6b/5ddd713d97886becb8450e3e13db891199125f722366d30d087ad5438390/ckzg-2.1.1-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:438a5009fd254ace0bc1ad974d524547f1a41e6aa5e778c5cd41f4ee3106bcd6", size = 126160 }, - { url = "https://files.pythonhosted.org/packages/c7/dd/e05aecc01e62108a7579f8df5e5d38536841f50e12172f8a84677edac0fa/ckzg-2.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ce11cc163a2e0dab3af7455aca7053f9d5bb8d157f231acc7665fd230565d48", size = 102811 }, - { url = "https://files.pythonhosted.org/packages/c6/81/6cdadd8626ac11290af3f58ae5dcffe38bd2c8f8c798dacee7475e244aac/ckzg-2.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b53964c07f6a076e97eaa1ef35045e935d7040aff14f80bae7e9105717702d05", size = 111328 }, - { url = "https://files.pythonhosted.org/packages/36/b7/b129ff6955cd264c6ab3dbd52dd1b2759d1b121c09c03f9991e4c722c72f/ckzg-2.1.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cf085f15ae52ab2599c9b5a3d5842794bcf5613b7f58661fbfb0c5d9eac988b9", size = 98846 }, - { url = "https://files.pythonhosted.org/packages/7f/ba/7d9c1f9cec7e0e382653c72165896194a05743e589b1dae2aa80236aa87f/ckzg-2.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4b0c850bd6cad22ac79b2a2ab884e0e7cd2b54a67d643cd616c145ebdb535a11", size = 113188 }, - { url = "https://files.pythonhosted.org/packages/2f/92/9728f5ccc1c5e87c6c5ae7941250a447b61fd5a63aadbc15249e29c21bcf/ckzg-2.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:26951f36bb60c9150bbd38110f5e1625596f9779dad54d1d492d8ec38bc84e3a", size = 96208 }, - { url = "https://files.pythonhosted.org/packages/39/63/5e27d587bd224fee70cb66b022e7c4ef95d0e091e08ee76c25ec12094b0d/ckzg-2.1.1-pp311-pypy311_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbe12445e49c4bee67746b7b958e90a973b0de116d0390749b0df351d94e9a8c", size = 126158 }, - { url = "https://files.pythonhosted.org/packages/43/98/e0a45946575a7b823d8ee0b47afb104b6017e54e1208f07da2529bc01900/ckzg-2.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71c5d4f66f09de4a99271acac74d2acb3559a77de77a366b34a91e99e8822667", size = 102812 }, - { url = "https://files.pythonhosted.org/packages/cb/50/718ca7b03e4b89b18cdf99cc3038050105b0acbf9b612c23cd513093c6de/ckzg-2.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42673c1d007372a4e8b48f6ef8f0ce31a9688a463317a98539757d1e2fb1ecc7", size = 111327 }, - { url = "https://files.pythonhosted.org/packages/29/c5/80e5a0c6967d02d801150104320484a258e5a49bd191e198643e74039320/ckzg-2.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:57a7dc41ec6b69c1d9117eb61cf001295e6b4f67a736020442e71fb4367fb1a5", size = 98847 }, +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/77/9a44934373eff2013cab641f4ac70b98bb8372fe2938ea78c349501aa825/ckzg-2.1.2.tar.gz", hash = "sha256:7d445215261068d914c3607fd89889bb405260911804cd0eea789ce7422db0d8", size = 1124054 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/9d/f68b10ebbc6c890736122be520580e1e741e16a396138b60fb0ab9885b8a/ckzg-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:96d4a9764a9f616c9a8eb3bcf8c171f03a85bbbfdb7c16f3ead53988083eea25", size = 116269 }, + { url = "https://files.pythonhosted.org/packages/d1/d8/2854b4c4ed3fc0811aeb3c9fb5adcf597e97206665340b59637b1dd8d036/ckzg-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6798af534394d1e4c05ca0f0a5abd714a6938551c402a072ba3f4bc27aa8b7ec", size = 99936 }, + { url = "https://files.pythonhosted.org/packages/43/fd/bc65c5684d091a8ef6cd5bd9a0093785d2aabf8a4a0d3277eadf7d425bc0/ckzg-2.1.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4190e74a8bdd866d5d0c7e182a2db24bcffedd9d5ef807fdd83fcbcd241223f1", size = 175429 }, + { url = "https://files.pythonhosted.org/packages/4e/49/034c44f497ad817b4c83b0d17b8d57acb49f71418dfb47fdba20a823e4fb/ckzg-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f4007ce9646b53fdc048c844a4304979d901b4b8905263be022a3ff9d7c92dd", size = 160842 }, + { url = "https://files.pythonhosted.org/packages/ff/9e/7c63b5637ceb5d41b9168e3f40163b9d3a2ae5ee5f2c96b13e5b7f851de3/ckzg-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18a98fc0cce78998e13566fb8f9258b1833286258e5f557a319654d23c2fa91f", size = 170082 }, + { url = "https://files.pythonhosted.org/packages/03/b7/f18046f9d7d92fd6cb7c83c6d77ee1daf18e76aea6333ef815752eef8613/ckzg-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:24408a806f4a507f3d428b4d2e19ac3b48ebe315a0d19e72067241c2a774d342", size = 172619 }, + { url = "https://files.pythonhosted.org/packages/43/11/2f132fbe99c753cb9f788f240ad5d0b4e5da03ff5d5137de2704fe34ebd2/ckzg-2.1.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a8180c4e1e20f5599c3df77d687a4526edd32f5bc023e93a901837f42de6c9a6", size = 187757 }, + { url = "https://files.pythonhosted.org/packages/c6/d1/fc889243f05453131174664d3c9bef392711c731a0e4b501fb9de7d22631/ckzg-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4a4212e67e9f87ef61915c5d6a39b734048fa28f130d5d07d5dd23ec6a0c2faf", size = 182319 }, + { url = "https://files.pythonhosted.org/packages/88/33/d46537483f9dadbfda54deb682b742e66c3ca15b08a1a242f0940ae9be6f/ckzg-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:5e58e84a20097bb7e5eb15334a8c1f23377c1aeffd90ae7c67ff147434346fed", size = 100725 }, + { url = "https://files.pythonhosted.org/packages/b2/b0/34d5d0391c9a0e9baa780702f7c920209e418af1128299a4e6260b89faeb/ckzg-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b298e9eddc832ef5b7a37ddd5436edb562480fd55be437b6d0b4ad3c936339a9", size = 116270 }, + { url = "https://files.pythonhosted.org/packages/21/4f/204f684ccd36d87bec6a73961153778def92c24c3e2fd031cb1c6c83fe34/ckzg-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62607c11198184f9bc3d7e9893ef9418d71768c942e39fb3c5a674ff6655885f", size = 99940 }, + { url = "https://files.pythonhosted.org/packages/f2/87/7d228489df4fb39ba38eb76f5a9c5ef12acf632b4d2934f0ccd944d163fd/ckzg-2.1.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e062b0860b300172091133ab55ca11be9218b80eea976aa157e367d3d298ddce", size = 176201 }, + { url = "https://files.pythonhosted.org/packages/66/73/02674c0e73591f77d440a240d9e7f6d39c348f782640c31997120ec0ea86/ckzg-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b8c78f0b517503f0ba39821f226a285d9570747125ed45f90aa7375217a2fbf", size = 161701 }, + { url = "https://files.pythonhosted.org/packages/4d/e1/cdc000f62cfac012b0275375c27d2bc550cfba4e746fe7a8864206e27fee/ckzg-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b6b82dd1f40d76ab235b5a10fc06d2a0e727cdda8d2a1fe6515af0e29ced964", size = 170839 }, + { url = "https://files.pythonhosted.org/packages/ed/c4/60171b74e943447bffc62860b92b6adcb3e261813d5515a66b44da3a29f6/ckzg-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ffb1b2da867c9d91925ab4fc4dac79aa313ae0a63565d3ddb70eb37d5633e5a3", size = 173281 }, + { url = "https://files.pythonhosted.org/packages/17/13/619d1c27595cbe5f36f0815db341cdd0bcd4c80274741c95760e3bd4e5b7/ckzg-2.1.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5db4b0de2b7934a350a6f42b7cea531dea343dda4e46a19fca217e391548b349", size = 188437 }, + { url = "https://files.pythonhosted.org/packages/4a/66/f21da2641cfde8f1cab73ee2a6e71075253a5b9d08ee959d9fcfaa2ae4be/ckzg-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bb8b7a581f45649423fdba5e2636f76115df62f401a752b8156b2a3492781d0", size = 183072 }, + { url = "https://files.pythonhosted.org/packages/d1/51/bf700ed95e4552c729ef7693abcbe1852252b7973d4f316cb790609ee28b/ckzg-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:89e03d3b0db10ef5157e932efd8dd65b3de12ff37bfabaf569c8cb72557d5b57", size = 100727 }, + { url = "https://files.pythonhosted.org/packages/99/34/bc1261aeb3c173ce3eaf7f9050923823488d9e63ecfc4830e8b162168cb6/ckzg-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41181abbc3936c0f375c561cf01b9c210d6761b8d0d4bc8eadb52c38c3636e3e", size = 116304 }, + { url = "https://files.pythonhosted.org/packages/69/17/cdec0fdd550560467792705af56880453e26c3dc9e9054144c0d7dc7ea5a/ckzg-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:00d847cb39e6921dbead165a2f5a4434f3b4ed1455fac216acf8330941bce67a", size = 99951 }, + { url = "https://files.pythonhosted.org/packages/3b/d7/b03cdd67ef4d5c07deb363737533c8e25e7c6b5348b9606873e75fb10820/ckzg-2.1.2-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f33abd137d90960e95a1620a35ea3a99d0b2d33272922d4c1325f3464833410", size = 176359 }, + { url = "https://files.pythonhosted.org/packages/bb/00/67241dcedb40c8baa02e5cf831b77dbb908d54217a084a1f96749a93eba0/ckzg-2.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a8fcc29778f0e74ba9cf2a87b7ef1a354361602c0b323e2564a89b7f1a914ba", size = 161876 }, + { url = "https://files.pythonhosted.org/packages/f7/64/bcfb3898ea04206ee4a175567665f7ea2bcc6b0cc6afaec1b4c08ed24509/ckzg-2.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba909a23522c1dbd85f5fb011a20603a7a4fd828b1bac3b144b78ab0a553c60f", size = 171124 }, + { url = "https://files.pythonhosted.org/packages/17/61/d04b6715f28682678309fe532723f09eef0653bbb6bc3634bdfe08b9eeb2/ckzg-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f9ffe5a968acb976830cf24d266f3c25b8dee2730574cf6c4ddfa95dfe5ddbfc", size = 173489 }, + { url = "https://files.pythonhosted.org/packages/f5/ff/7b24a023db001c5f9f99b81d8b45e7173c209485f7f7152d778f7e7b2b85/ckzg-2.1.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d2bc2d809dfd7d7737f5021daeb67501eb63be05a458b50b8dd4453da5da16b", size = 188710 }, + { url = "https://files.pythonhosted.org/packages/03/66/22a36b3e36c4f844c319b82a5b44a0a5bf9cbbd48c6b39644ee862241fcc/ckzg-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9b3110974e4982f0a6b0b44f2f29d2c915d443c012ef4898e91c3f4c38d8c5c2", size = 183460 }, + { url = "https://files.pythonhosted.org/packages/02/d3/0ea9ddc370190e1345ecf5ee60071ec3084395ea83018002badaaf34d5d2/ckzg-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:f00f585128a2a2305b61988ce74b05d27eed5c2fcde4aea286790e7c7601ebae", size = 100694 }, + { url = "https://files.pythonhosted.org/packages/11/ed/007ddc03613be6e8b246cace85edc943116fd78413a228789ca490775971/ckzg-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:457635f924610414b7e7460b1e5097187ca4c40406ea80c73848866267213fed", size = 116305 }, + { url = "https://files.pythonhosted.org/packages/fc/9f/1a9df26c78b5f26c06a9a97948e12db434c2b4a784708b9214f72ad8cea7/ckzg-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:32b750784bef9fc7832dee07635eb46309eca5b55a6eb350ff40021b5fc483f2", size = 99956 }, + { url = "https://files.pythonhosted.org/packages/7b/d8/9fc6537a8fcc0a373f0bb0cf2747e28e7aa99918c9d96385ef1f3ec51c9c/ckzg-2.1.2-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4eeff254f60b08dba7991d3ab20018d5df7cbe3318e67efd070d2361104e6d4", size = 176341 }, + { url = "https://files.pythonhosted.org/packages/12/f1/06b20839ac10c4e839bad82e32ccf1078be810c972fdf703c08754fbd348/ckzg-2.1.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ad66afefac5836c340a853b543f932a9e98830359617414b1972233eaa5a069", size = 161827 }, + { url = "https://files.pythonhosted.org/packages/c4/fa/04df1f37a4075c7e0032c960f037d14fead960db699504781fd421c735a4/ckzg-2.1.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d3046c1541f9b3aed997860fdab106795ac4e8335cb1d3fe6a2a45958fb00ab", size = 171088 }, + { url = "https://files.pythonhosted.org/packages/6a/9d/50b82acbf1f89159fb70853ecd42a5b67ecba0e298eebb31760bb41b2aa0/ckzg-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b98029c67d34bcf6b0e030d06505a1accc1829a378736e2cb69e4af852add99", size = 173505 }, + { url = "https://files.pythonhosted.org/packages/61/6f/97085ef1002fcfd7620b774df13c918cd83a84247f1b5ece098073a3fc25/ckzg-2.1.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59541361c9402ec14790db88c16532e66ece8e56d985b75756f36387858549fa", size = 188738 }, + { url = "https://files.pythonhosted.org/packages/2e/7a/e8208411860bd2dca57eae2771e045b1a4dcde8dc08004d74401ad74f23a/ckzg-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:edf41132119d67673af1cf6cbf22f3852d092e94c9c890ff153e761d7be6e684", size = 183486 }, + { url = "https://files.pythonhosted.org/packages/41/28/8b381db79aa362e975e86c3bf2c85de6b9482923dc55f19bb21419d12994/ckzg-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:0074cbfe11702c1d413ed86a74d9fcfef48fcb206c31a37c0b3eeb830f6d0a05", size = 100693 }, + { url = "https://files.pythonhosted.org/packages/23/f0/e370077e14ebc91fc5659456cebf463aaef16193e276f3f6cee57ba79be0/ckzg-2.1.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b4acf34040785baa6169f378a164c8f1818806af9d09731ee3a437d936bd3a17", size = 113083 }, + { url = "https://files.pythonhosted.org/packages/da/49/4897682a890f7d5b3d0790eb7991c23d4c13b4696d5840a24567cd922539/ckzg-2.1.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1283828dee9712ce360b177c4fd7821baa9d6f5efaab6df5e759cfaaf58caaa1", size = 95939 }, + { url = "https://files.pythonhosted.org/packages/71/fa/2df94aad1dfea4a56c85f137486a7f9c82f75f1db0ff48e7c0076afde1e5/ckzg-2.1.2-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7baf5a2189f69f7dc6fd4bd423c909f648a1ff7feb8193cc72ecf61738e69668", size = 126519 }, + { url = "https://files.pythonhosted.org/packages/4d/19/2635b180b4329fcb67be043b22c4d5df9617faf2589a88e186cb22a14b46/ckzg-2.1.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa7e8c648d67fb3411baa1527cd32b81ec5d739efaf141b22be42491590b9560", size = 102784 }, + { url = "https://files.pythonhosted.org/packages/34/09/328b4d4fa742f24f60645946a5bdf50709ac969876af15f01cdbe0dede22/ckzg-2.1.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:883a87c73180b3afaa060c30804c06bd0cf8eeea8db1d5727bedd10f500e9d44", size = 111501 }, + { url = "https://files.pythonhosted.org/packages/cf/48/fdb44b7a81d08a9aec6a1f88a6faee04bfcf868a660d16f16172656d19b6/ckzg-2.1.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7fd022ce433673826da5efc66fb606b8ba86ecd31346a7b647054d940b040720", size = 100719 }, + { url = "https://files.pythonhosted.org/packages/16/99/295500f1b6f05ab5558a7083587f2bdc59409c70a3cb9403f7b3e228dd57/ckzg-2.1.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:12e2d7fcd9663241a5dc1ecb8a2d36ec9a3b03c6ed36094f16accc3ad7c1bfa5", size = 113084 }, + { url = "https://files.pythonhosted.org/packages/6b/99/b597aea9ae3dc0a4ad24322fdb84f4f31e90b669e7e0a90366303fee1445/ckzg-2.1.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f7df2067cbcd8385ecc2f6b8e15f4816c9cdb3589a1089e87eca93342d0291c", size = 95940 }, + { url = "https://files.pythonhosted.org/packages/41/35/756ef51ca4b2c1cd0b31d7b4e365469babf5080abdf54f35a350ca5bcf0b/ckzg-2.1.2-pp311-pypy311_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:251460feafbe3216c7edea3caa6e1d95dd36f1b079c4e854aab56cb5a4893def", size = 126519 }, + { url = "https://files.pythonhosted.org/packages/53/9a/fa2ce970bf6df8129d159c863eb04a966d277191b7d4c6a559f6ed3b2351/ckzg-2.1.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94a4619c5a1824ee7dd47274713339bbfec922786888d44aa70e0546ed8c7ddd", size = 102784 }, + { url = "https://files.pythonhosted.org/packages/9e/15/912640540d40542209ceae29af2a0b0dbd5c139aa7e7133ca5aebe436e19/ckzg-2.1.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed818d3bb5926f9a41e31bc481b1ce00630de3bcac7acd66c8b586b3ba946d87", size = 111500 }, + { url = "https://files.pythonhosted.org/packages/29/f5/1de274a840f9c68bf07e3ccde1d5fea97ab444da8da41d43be429ee6a896/ckzg-2.1.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c5fe5293f791568b09cdd0a9ea816dcf8537a42eb853644edebea646c875aaed", size = 100718 }, ] [[package]] @@ -302,87 +317,87 @@ wheels = [ [[package]] name = "coverage" -version = "7.10.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/76/17780846fc7aade1e66712e1e27dd28faa0a5d987a1f433610974959eaa8/coverage-7.10.2.tar.gz", hash = "sha256:5d6e6d84e6dd31a8ded64759626627247d676a23c1b892e1326f7c55c8d61055", size = 820754 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/5f/5ce748ab3f142593698aff5f8a0cf020775aa4e24b9d8748b5a56b64d3f8/coverage-7.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:79f0283ab5e6499fd5fe382ca3d62afa40fb50ff227676a3125d18af70eabf65", size = 215003 }, - { url = "https://files.pythonhosted.org/packages/f4/ed/507088561217b000109552139802fa99c33c16ad19999c687b601b3790d0/coverage-7.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4545e906f595ee8ab8e03e21be20d899bfc06647925bc5b224ad7e8c40e08b8", size = 215391 }, - { url = "https://files.pythonhosted.org/packages/79/1b/0f496259fe137c4c5e1e8eaff496fb95af88b71700f5e57725a4ddbe742b/coverage-7.10.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ae385e1d58fbc6a9b1c315e5510ac52281e271478b45f92ca9b5ad42cf39643f", size = 242367 }, - { url = "https://files.pythonhosted.org/packages/b9/8e/5a8835fb0122a2e2a108bf3527931693c4625fdc4d953950a480b9625852/coverage-7.10.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f0cbe5f7dd19f3a32bac2251b95d51c3b89621ac88a2648096ce40f9a5aa1e7", size = 243627 }, - { url = "https://files.pythonhosted.org/packages/c3/96/6a528429c2e0e8d85261764d0cd42e51a429510509bcc14676ee5d1bb212/coverage-7.10.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd17f427f041f6b116dc90b4049c6f3e1230524407d00daa2d8c7915037b5947", size = 245485 }, - { url = "https://files.pythonhosted.org/packages/bf/82/1fba935c4d02c33275aca319deabf1f22c0f95f2c0000bf7c5f276d6f7b4/coverage-7.10.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7f10ca4cde7b466405cce0a0e9971a13eb22e57a5ecc8b5f93a81090cc9c7eb9", size = 243429 }, - { url = "https://files.pythonhosted.org/packages/fc/a8/c8dc0a57a729fc93be33ab78f187a8f52d455fa8f79bfb379fe23b45868d/coverage-7.10.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3b990df23dd51dccce26d18fb09fd85a77ebe46368f387b0ffba7a74e470b31b", size = 242104 }, - { url = "https://files.pythonhosted.org/packages/b9/6f/0b7da1682e2557caeed299a00897b42afde99a241a01eba0197eb982b90f/coverage-7.10.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc3902584d25c7eef57fb38f440aa849a26a3a9f761a029a72b69acfca4e31f8", size = 242397 }, - { url = "https://files.pythonhosted.org/packages/2d/e4/54dc833dadccd519c04a28852f39a37e522bad35d70cfe038817cdb8f168/coverage-7.10.2-cp310-cp310-win32.whl", hash = "sha256:9dd37e9ac00d5eb72f38ed93e3cdf2280b1dbda3bb9b48c6941805f265ad8d87", size = 217502 }, - { url = "https://files.pythonhosted.org/packages/c3/e7/2f78159c4c127549172f427dff15b02176329327bf6a6a1fcf1f603b5456/coverage-7.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:99d16f15cb5baf0729354c5bd3080ae53847a4072b9ba1e10957522fb290417f", size = 218388 }, - { url = "https://files.pythonhosted.org/packages/6e/53/0125a6fc0af4f2687b4e08b0fb332cd0d5e60f3ca849e7456f995d022656/coverage-7.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c3b210d79925a476dfc8d74c7d53224888421edebf3a611f3adae923e212b27", size = 215119 }, - { url = "https://files.pythonhosted.org/packages/0e/2e/960d9871de9152dbc9ff950913c6a6e9cf2eb4cc80d5bc8f93029f9f2f9f/coverage-7.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf67d1787cd317c3f8b2e4c6ed1ae93497be7e30605a0d32237ac37a37a8a322", size = 215511 }, - { url = "https://files.pythonhosted.org/packages/3f/34/68509e44995b9cad806d81b76c22bc5181f3535bca7cd9c15791bfd8951e/coverage-7.10.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:069b779d03d458602bc0e27189876e7d8bdf6b24ac0f12900de22dd2154e6ad7", size = 245513 }, - { url = "https://files.pythonhosted.org/packages/ef/d4/9b12f357413248ce40804b0f58030b55a25b28a5c02db95fb0aa50c5d62c/coverage-7.10.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4c2de4cb80b9990e71c62c2d3e9f3ec71b804b1f9ca4784ec7e74127e0f42468", size = 247350 }, - { url = "https://files.pythonhosted.org/packages/b6/40/257945eda1f72098e4a3c350b1d68fdc5d7d032684a0aeb6c2391153ecf4/coverage-7.10.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75bf7ab2374a7eb107602f1e07310cda164016cd60968abf817b7a0b5703e288", size = 249516 }, - { url = "https://files.pythonhosted.org/packages/ff/55/8987f852ece378cecbf39a367f3f7ec53351e39a9151b130af3a3045b83f/coverage-7.10.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3f37516458ec1550815134937f73d6d15b434059cd10f64678a2068f65c62406", size = 247241 }, - { url = "https://files.pythonhosted.org/packages/df/ae/da397de7a42a18cea6062ed9c3b72c50b39e0b9e7b2893d7172d3333a9a1/coverage-7.10.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:de3c6271c482c250d3303fb5c6bdb8ca025fff20a67245e1425df04dc990ece9", size = 245274 }, - { url = "https://files.pythonhosted.org/packages/4e/64/7baa895eb55ec0e1ec35b988687ecd5d4475ababb0d7ae5ca3874dd90ee7/coverage-7.10.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:98a838101321ac3089c9bb1d4bfa967e8afed58021fda72d7880dc1997f20ae1", size = 245882 }, - { url = "https://files.pythonhosted.org/packages/24/6c/1fd76a0bd09ae75220ae9775a8290416d726f0e5ba26ea72346747161240/coverage-7.10.2-cp311-cp311-win32.whl", hash = "sha256:f2a79145a531a0e42df32d37be5af069b4a914845b6f686590739b786f2f7bce", size = 217541 }, - { url = "https://files.pythonhosted.org/packages/5f/2d/8c18fb7a6e74c79fd4661e82535bc8c68aee12f46c204eabf910b097ccc9/coverage-7.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:e4f5f1320f8ee0d7cfa421ceb257bef9d39fd614dd3ddcfcacd284d4824ed2c2", size = 218426 }, - { url = "https://files.pythonhosted.org/packages/da/40/425bb35e4ff7c7af177edf5dffd4154bc2a677b27696afe6526d75c77fec/coverage-7.10.2-cp311-cp311-win_arm64.whl", hash = "sha256:d8f2d83118f25328552c728b8e91babf93217db259ca5c2cd4dd4220b8926293", size = 217116 }, - { url = "https://files.pythonhosted.org/packages/4e/1e/2c752bdbbf6f1199c59b1a10557fbb6fb3dc96b3c0077b30bd41a5922c1f/coverage-7.10.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:890ad3a26da9ec7bf69255b9371800e2a8da9bc223ae5d86daeb940b42247c83", size = 215311 }, - { url = "https://files.pythonhosted.org/packages/68/6a/84277d73a2cafb96e24be81b7169372ba7ff28768ebbf98e55c85a491b0f/coverage-7.10.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38fd1ccfca7838c031d7a7874d4353e2f1b98eb5d2a80a2fe5732d542ae25e9c", size = 215550 }, - { url = "https://files.pythonhosted.org/packages/b5/e7/5358b73b46ac76f56cc2de921eeabd44fabd0b7ff82ea4f6b8c159c4d5dc/coverage-7.10.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:76c1ffaaf4f6f0f6e8e9ca06f24bb6454a7a5d4ced97a1bc466f0d6baf4bd518", size = 246564 }, - { url = "https://files.pythonhosted.org/packages/7c/0e/b0c901dd411cb7fc0cfcb28ef0dc6f3049030f616bfe9fc4143aecd95901/coverage-7.10.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:86da8a3a84b79ead5c7d0e960c34f580bc3b231bb546627773a3f53c532c2f21", size = 248993 }, - { url = "https://files.pythonhosted.org/packages/0e/4e/a876db272072a9e0df93f311e187ccdd5f39a190c6d1c1f0b6e255a0d08e/coverage-7.10.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99cef9731c8a39801830a604cc53c93c9e57ea8b44953d26589499eded9576e0", size = 250454 }, - { url = "https://files.pythonhosted.org/packages/64/d6/1222dc69f8dd1be208d55708a9f4a450ad582bf4fa05320617fea1eaa6d8/coverage-7.10.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ea58b112f2966a8b91eb13f5d3b1f8bb43c180d624cd3283fb33b1cedcc2dd75", size = 248365 }, - { url = "https://files.pythonhosted.org/packages/62/e3/40fd71151064fc315c922dd9a35e15b30616f00146db1d6a0b590553a75a/coverage-7.10.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:20f405188d28da9522b7232e51154e1b884fc18d0b3a10f382d54784715bbe01", size = 246562 }, - { url = "https://files.pythonhosted.org/packages/fc/14/8aa93ddcd6623ddaef5d8966268ac9545b145bce4fe7b1738fd1c3f0d957/coverage-7.10.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:64586ce42bbe0da4d9f76f97235c545d1abb9b25985a8791857690f96e23dc3b", size = 247772 }, - { url = "https://files.pythonhosted.org/packages/07/4e/dcb1c01490623c61e2f2ea85cb185fa6a524265bb70eeb897d3c193efeb9/coverage-7.10.2-cp312-cp312-win32.whl", hash = "sha256:bc2e69b795d97ee6d126e7e22e78a509438b46be6ff44f4dccbb5230f550d340", size = 217710 }, - { url = "https://files.pythonhosted.org/packages/79/16/e8aab4162b5f80ad2e5e1f54b1826e2053aa2f4db508b864af647f00c239/coverage-7.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:adda2268b8cf0d11f160fad3743b4dfe9813cd6ecf02c1d6397eceaa5b45b388", size = 218499 }, - { url = "https://files.pythonhosted.org/packages/06/7f/c112ec766e8f1131ce8ce26254be028772757b2d1e63e4f6a4b0ad9a526c/coverage-7.10.2-cp312-cp312-win_arm64.whl", hash = "sha256:164429decd0d6b39a0582eaa30c67bf482612c0330572343042d0ed9e7f15c20", size = 217154 }, - { url = "https://files.pythonhosted.org/packages/8d/04/9b7a741557f93c0ed791b854d27aa8d9fe0b0ce7bb7c52ca1b0f2619cb74/coverage-7.10.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aca7b5645afa688de6d4f8e89d30c577f62956fefb1bad021490d63173874186", size = 215337 }, - { url = "https://files.pythonhosted.org/packages/02/a4/8d1088cd644750c94bc305d3cf56082b4cdf7fb854a25abb23359e74892f/coverage-7.10.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:96e5921342574a14303dfdb73de0019e1ac041c863743c8fe1aa6c2b4a257226", size = 215596 }, - { url = "https://files.pythonhosted.org/packages/01/2f/643a8d73343f70e162d8177a3972b76e306b96239026bc0c12cfde4f7c7a/coverage-7.10.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11333094c1bff621aa811b67ed794865cbcaa99984dedea4bd9cf780ad64ecba", size = 246145 }, - { url = "https://files.pythonhosted.org/packages/1f/4a/722098d1848db4072cda71b69ede1e55730d9063bf868375264d0d302bc9/coverage-7.10.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6eb586fa7d2aee8d65d5ae1dd71414020b2f447435c57ee8de8abea0a77d5074", size = 248492 }, - { url = "https://files.pythonhosted.org/packages/3f/b0/8a6d7f326f6e3e6ed398cde27f9055e860a1e858317001835c521673fb60/coverage-7.10.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d358f259d8019d4ef25d8c5b78aca4c7af25e28bd4231312911c22a0e824a57", size = 249927 }, - { url = "https://files.pythonhosted.org/packages/bb/21/1aaadd3197b54d1e61794475379ecd0f68d8fc5c2ebd352964dc6f698a3d/coverage-7.10.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5250bda76e30382e0a2dcd68d961afcab92c3a7613606e6269855c6979a1b0bb", size = 248138 }, - { url = "https://files.pythonhosted.org/packages/48/65/be75bafb2bdd22fd8bf9bf63cd5873b91bb26ec0d68f02d4b8b09c02decb/coverage-7.10.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a91e027d66eff214d88d9afbe528e21c9ef1ecdf4956c46e366c50f3094696d0", size = 246111 }, - { url = "https://files.pythonhosted.org/packages/5e/30/a4f0c5e249c3cc60e6c6f30d8368e372f2d380eda40e0434c192ac27ccf5/coverage-7.10.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:228946da741558904e2c03ce870ba5efd9cd6e48cbc004d9a27abee08100a15a", size = 247493 }, - { url = "https://files.pythonhosted.org/packages/85/99/f09b9493e44a75cf99ca834394c12f8cb70da6c1711ee296534f97b52729/coverage-7.10.2-cp313-cp313-win32.whl", hash = "sha256:95e23987b52d02e7c413bf2d6dc6288bd5721beb518052109a13bfdc62c8033b", size = 217756 }, - { url = "https://files.pythonhosted.org/packages/2d/bb/cbcb09103be330c7d26ff0ab05c4a8861dd2e254656fdbd3eb7600af4336/coverage-7.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:f35481d42c6d146d48ec92d4e239c23f97b53a3f1fbd2302e7c64336f28641fe", size = 218526 }, - { url = "https://files.pythonhosted.org/packages/37/8f/8bfb4e0bca52c00ab680767c0dd8cfd928a2a72d69897d9b2d5d8b5f63f5/coverage-7.10.2-cp313-cp313-win_arm64.whl", hash = "sha256:65b451949cb789c346f9f9002441fc934d8ccedcc9ec09daabc2139ad13853f7", size = 217176 }, - { url = "https://files.pythonhosted.org/packages/1e/25/d458ba0bf16a8204a88d74dbb7ec5520f29937ffcbbc12371f931c11efd2/coverage-7.10.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e8415918856a3e7d57a4e0ad94651b761317de459eb74d34cc1bb51aad80f07e", size = 216058 }, - { url = "https://files.pythonhosted.org/packages/0b/1c/af4dfd2d7244dc7610fed6d59d57a23ea165681cd764445dc58d71ed01a6/coverage-7.10.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f287a25a8ca53901c613498e4a40885b19361a2fe8fbfdbb7f8ef2cad2a23f03", size = 216273 }, - { url = "https://files.pythonhosted.org/packages/8e/67/ec5095d4035c6e16368226fa9cb15f77f891194c7e3725aeefd08e7a3e5a/coverage-7.10.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:75cc1a3f8c88c69bf16a871dab1fe5a7303fdb1e9f285f204b60f1ee539b8fc0", size = 257513 }, - { url = "https://files.pythonhosted.org/packages/1c/47/be5550b57a3a8ba797de4236b0fd31031f88397b2afc84ab3c2d4cf265f6/coverage-7.10.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca07fa78cc9d26bc8c4740de1abd3489cf9c47cc06d9a8ab3d552ff5101af4c0", size = 259377 }, - { url = "https://files.pythonhosted.org/packages/37/50/b12a4da1382e672305c2d17cd3029dc16b8a0470de2191dbf26b91431378/coverage-7.10.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2e117e64c26300032755d4520cd769f2623cde1a1d1c3515b05a3b8add0ade1", size = 261516 }, - { url = "https://files.pythonhosted.org/packages/db/41/4d3296dbd33dd8da178171540ca3391af7c0184c0870fd4d4574ac290290/coverage-7.10.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:daaf98009977f577b71f8800208f4d40d4dcf5c2db53d4d822787cdc198d76e1", size = 259110 }, - { url = "https://files.pythonhosted.org/packages/ea/f1/b409959ecbc0cec0e61e65683b22bacaa4a3b11512f834e16dd8ffbc37db/coverage-7.10.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ea8d8fe546c528535c761ba424410bbeb36ba8a0f24be653e94b70c93fd8a8ca", size = 257248 }, - { url = "https://files.pythonhosted.org/packages/48/ab/7076dc1c240412e9267d36ec93e9e299d7659f6a5c1e958f87e998b0fb6d/coverage-7.10.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fe024d40ac31eb8d5aae70215b41dafa264676caa4404ae155f77d2fa95c37bb", size = 258063 }, - { url = "https://files.pythonhosted.org/packages/1e/77/f6b51a0288f8f5f7dcc7c89abdd22cf514f3bc5151284f5cd628917f8e10/coverage-7.10.2-cp313-cp313t-win32.whl", hash = "sha256:8f34b09f68bdadec122ffad312154eda965ade433559cc1eadd96cca3de5c824", size = 218433 }, - { url = "https://files.pythonhosted.org/packages/7b/6d/547a86493e25270ce8481543e77f3a0aa3aa872c1374246b7b76273d66eb/coverage-7.10.2-cp313-cp313t-win_amd64.whl", hash = "sha256:71d40b3ac0f26fa9ffa6ee16219a714fed5c6ec197cdcd2018904ab5e75bcfa3", size = 219523 }, - { url = "https://files.pythonhosted.org/packages/ff/d5/3c711e38eaf9ab587edc9bed232c0298aed84e751a9f54aaa556ceaf7da6/coverage-7.10.2-cp313-cp313t-win_arm64.whl", hash = "sha256:abb57fdd38bf6f7dcc66b38dafb7af7c5fdc31ac6029ce373a6f7f5331d6f60f", size = 217739 }, - { url = "https://files.pythonhosted.org/packages/71/53/83bafa669bb9d06d4c8c6a055d8d05677216f9480c4698fb183ba7ec5e47/coverage-7.10.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a3e853cc04987c85ec410905667eed4bf08b1d84d80dfab2684bb250ac8da4f6", size = 215328 }, - { url = "https://files.pythonhosted.org/packages/1d/6c/30827a9c5a48a813e865fbaf91e2db25cce990bd223a022650ef2293fe11/coverage-7.10.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0100b19f230df72c90fdb36db59d3f39232391e8d89616a7de30f677da4f532b", size = 215608 }, - { url = "https://files.pythonhosted.org/packages/bb/a0/c92d85948056ddc397b72a3d79d36d9579c53cb25393ed3c40db7d33b193/coverage-7.10.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9c1cd71483ea78331bdfadb8dcec4f4edfb73c7002c1206d8e0af6797853f5be", size = 246111 }, - { url = "https://files.pythonhosted.org/packages/c2/cf/d695cf86b2559aadd072c91720a7844be4fb82cb4a3b642a2c6ce075692d/coverage-7.10.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9f75dbf4899e29a37d74f48342f29279391668ef625fdac6d2f67363518056a1", size = 248419 }, - { url = "https://files.pythonhosted.org/packages/ce/0a/03206aec4a05986e039418c038470d874045f6e00426b0c3879adc1f9251/coverage-7.10.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7df481e7508de1c38b9b8043da48d94931aefa3e32b47dd20277e4978ed5b95", size = 250038 }, - { url = "https://files.pythonhosted.org/packages/ab/9b/b3bd6bd52118c12bc4cf319f5baba65009c9beea84e665b6b9f03fa3f180/coverage-7.10.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:835f39e618099325e7612b3406f57af30ab0a0af350490eff6421e2e5f608e46", size = 248066 }, - { url = "https://files.pythonhosted.org/packages/80/cc/bfa92e261d3e055c851a073e87ba6a3bff12a1f7134233e48a8f7d855875/coverage-7.10.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:12e52b5aa00aa720097d6947d2eb9e404e7c1101ad775f9661ba165ed0a28303", size = 245909 }, - { url = "https://files.pythonhosted.org/packages/12/80/c8df15db4847710c72084164f615ae900af1ec380dce7f74a5678ccdf5e1/coverage-7.10.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:718044729bf1fe3e9eb9f31b52e44ddae07e434ec050c8c628bf5adc56fe4bdd", size = 247329 }, - { url = "https://files.pythonhosted.org/packages/04/6f/cb66e1f7124d5dd9ced69f889f02931419cb448125e44a89a13f4e036124/coverage-7.10.2-cp314-cp314-win32.whl", hash = "sha256:f256173b48cc68486299d510a3e729a96e62c889703807482dbf56946befb5c8", size = 218007 }, - { url = "https://files.pythonhosted.org/packages/8c/e1/3d4be307278ce32c1b9d95cc02ee60d54ddab784036101d053ec9e4fe7f5/coverage-7.10.2-cp314-cp314-win_amd64.whl", hash = "sha256:2e980e4179f33d9b65ac4acb86c9c0dde904098853f27f289766657ed16e07b3", size = 218802 }, - { url = "https://files.pythonhosted.org/packages/ec/66/1e43bbeb66c55a5a5efec70f1c153cf90cfc7f1662ab4ebe2d844de9122c/coverage-7.10.2-cp314-cp314-win_arm64.whl", hash = "sha256:14fb5b6641ab5b3c4161572579f0f2ea8834f9d3af2f7dd8fbaecd58ef9175cc", size = 217397 }, - { url = "https://files.pythonhosted.org/packages/81/01/ae29c129217f6110dc694a217475b8aecbb1b075d8073401f868c825fa99/coverage-7.10.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e96649ac34a3d0e6491e82a2af71098e43be2874b619547c3282fc11d3840a4b", size = 216068 }, - { url = "https://files.pythonhosted.org/packages/a2/50/6e9221d4139f357258f36dfa1d8cac4ec56d9d5acf5fdcc909bb016954d7/coverage-7.10.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1a2e934e9da26341d342d30bfe91422bbfdb3f1f069ec87f19b2909d10d8dcc4", size = 216285 }, - { url = "https://files.pythonhosted.org/packages/eb/ec/89d1d0c0ece0d296b4588e0ef4df185200456d42a47f1141335f482c2fc5/coverage-7.10.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:651015dcd5fd9b5a51ca79ece60d353cacc5beaf304db750407b29c89f72fe2b", size = 257603 }, - { url = "https://files.pythonhosted.org/packages/82/06/c830af66734671c778fc49d35b58339e8f0687fbd2ae285c3f96c94da092/coverage-7.10.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81bf6a32212f9f66da03d63ecb9cd9bd48e662050a937db7199dbf47d19831de", size = 259568 }, - { url = "https://files.pythonhosted.org/packages/60/57/f280dd6f1c556ecc744fbf39e835c33d3ae987d040d64d61c6f821e87829/coverage-7.10.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d800705f6951f75a905ea6feb03fff8f3ea3468b81e7563373ddc29aa3e5d1ca", size = 261691 }, - { url = "https://files.pythonhosted.org/packages/54/2b/c63a0acbd19d99ec32326164c23df3a4e18984fb86e902afdd66ff7b3d83/coverage-7.10.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:248b5394718e10d067354448dc406d651709c6765669679311170da18e0e9af8", size = 259166 }, - { url = "https://files.pythonhosted.org/packages/fd/c5/cd2997dcfcbf0683634da9df52d3967bc1f1741c1475dd0e4722012ba9ef/coverage-7.10.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5c61675a922b569137cf943770d7ad3edd0202d992ce53ac328c5ff68213ccf4", size = 257241 }, - { url = "https://files.pythonhosted.org/packages/16/26/c9e30f82fdad8d47aee90af4978b18c88fa74369ae0f0ba0dbf08cee3a80/coverage-7.10.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:52d708b5fd65589461381fa442d9905f5903d76c086c6a4108e8e9efdca7a7ed", size = 258139 }, - { url = "https://files.pythonhosted.org/packages/c9/99/bdb7bd00bebcd3dedfb895fa9af8e46b91422993e4a37ac634a5f1113790/coverage-7.10.2-cp314-cp314t-win32.whl", hash = "sha256:916369b3b914186b2c5e5ad2f7264b02cff5df96cdd7cdad65dccd39aa5fd9f0", size = 218809 }, - { url = "https://files.pythonhosted.org/packages/eb/5e/56a7852e38a04d1520dda4dfbfbf74a3d6dec932c20526968f7444763567/coverage-7.10.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5b9d538e8e04916a5df63052d698b30c74eb0174f2ca9cd942c981f274a18eaf", size = 219926 }, - { url = "https://files.pythonhosted.org/packages/e0/12/7fbe6b9c52bb9d627e9556f9f2edfdbe88b315e084cdecc9afead0c3b36a/coverage-7.10.2-cp314-cp314t-win_arm64.whl", hash = "sha256:04c74f9ef1f925456a9fd23a7eef1103126186d0500ef9a0acb0bd2514bdc7cc", size = 217925 }, - { url = "https://files.pythonhosted.org/packages/18/d8/9b768ac73a8ac2d10c080af23937212434a958c8d2a1c84e89b450237942/coverage-7.10.2-py3-none-any.whl", hash = "sha256:95db3750dd2e6e93d99fa2498f3a1580581e49c494bddccc6f85c5c21604921f", size = 206973 }, +version = "7.10.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/70/025b179c993f019105b79575ac6edb5e084fb0f0e63f15cdebef4e454fb5/coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90", size = 823736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/1d/2e64b43d978b5bd184e0756a41415597dfef30fcbd90b747474bd749d45f/coverage-7.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70e7bfbd57126b5554aa482691145f798d7df77489a177a6bef80de78860a356", size = 217025 }, + { url = "https://files.pythonhosted.org/packages/23/62/b1e0f513417c02cc10ef735c3ee5186df55f190f70498b3702d516aad06f/coverage-7.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e41be6f0f19da64af13403e52f2dec38bbc2937af54df8ecef10850ff8d35301", size = 217419 }, + { url = "https://files.pythonhosted.org/packages/e7/16/b800640b7a43e7c538429e4d7223e0a94fd72453a1a048f70bf766f12e96/coverage-7.10.6-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c61fc91ab80b23f5fddbee342d19662f3d3328173229caded831aa0bd7595460", size = 244180 }, + { url = "https://files.pythonhosted.org/packages/fb/6f/5e03631c3305cad187eaf76af0b559fff88af9a0b0c180d006fb02413d7a/coverage-7.10.6-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10356fdd33a7cc06e8051413140bbdc6f972137508a3572e3f59f805cd2832fd", size = 245992 }, + { url = "https://files.pythonhosted.org/packages/eb/a1/f30ea0fb400b080730125b490771ec62b3375789f90af0bb68bfb8a921d7/coverage-7.10.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80b1695cf7c5ebe7b44bf2521221b9bb8cdf69b1f24231149a7e3eb1ae5fa2fb", size = 247851 }, + { url = "https://files.pythonhosted.org/packages/02/8e/cfa8fee8e8ef9a6bb76c7bef039f3302f44e615d2194161a21d3d83ac2e9/coverage-7.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2e4c33e6378b9d52d3454bd08847a8651f4ed23ddbb4a0520227bd346382bbc6", size = 245891 }, + { url = "https://files.pythonhosted.org/packages/93/a9/51be09b75c55c4f6c16d8d73a6a1d46ad764acca0eab48fa2ffaef5958fe/coverage-7.10.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c8a3ec16e34ef980a46f60dc6ad86ec60f763c3f2fa0db6d261e6e754f72e945", size = 243909 }, + { url = "https://files.pythonhosted.org/packages/e9/a6/ba188b376529ce36483b2d585ca7bdac64aacbe5aa10da5978029a9c94db/coverage-7.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7d79dabc0a56f5af990cc6da9ad1e40766e82773c075f09cc571e2076fef882e", size = 244786 }, + { url = "https://files.pythonhosted.org/packages/d0/4c/37ed872374a21813e0d3215256180c9a382c3f5ced6f2e5da0102fc2fd3e/coverage-7.10.6-cp310-cp310-win32.whl", hash = "sha256:86b9b59f2b16e981906e9d6383eb6446d5b46c278460ae2c36487667717eccf1", size = 219521 }, + { url = "https://files.pythonhosted.org/packages/8e/36/9311352fdc551dec5b973b61f4e453227ce482985a9368305880af4f85dd/coverage-7.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:e132b9152749bd33534e5bd8565c7576f135f157b4029b975e15ee184325f528", size = 220417 }, + { url = "https://files.pythonhosted.org/packages/d4/16/2bea27e212c4980753d6d563a0803c150edeaaddb0771a50d2afc410a261/coverage-7.10.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c706db3cabb7ceef779de68270150665e710b46d56372455cd741184f3868d8f", size = 217129 }, + { url = "https://files.pythonhosted.org/packages/2a/51/e7159e068831ab37e31aac0969d47b8c5ee25b7d307b51e310ec34869315/coverage-7.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e0c38dc289e0508ef68ec95834cb5d2e96fdbe792eaccaa1bccac3966bbadcc", size = 217532 }, + { url = "https://files.pythonhosted.org/packages/e7/c0/246ccbea53d6099325d25cd208df94ea435cd55f0db38099dd721efc7a1f/coverage-7.10.6-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:752a3005a1ded28f2f3a6e8787e24f28d6abe176ca64677bcd8d53d6fe2ec08a", size = 247931 }, + { url = "https://files.pythonhosted.org/packages/7d/fb/7435ef8ab9b2594a6e3f58505cc30e98ae8b33265d844007737946c59389/coverage-7.10.6-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:689920ecfd60f992cafca4f5477d55720466ad2c7fa29bb56ac8d44a1ac2b47a", size = 249864 }, + { url = "https://files.pythonhosted.org/packages/51/f8/d9d64e8da7bcddb094d511154824038833c81e3a039020a9d6539bf303e9/coverage-7.10.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec98435796d2624d6905820a42f82149ee9fc4f2d45c2c5bc5a44481cc50db62", size = 251969 }, + { url = "https://files.pythonhosted.org/packages/43/28/c43ba0ef19f446d6463c751315140d8f2a521e04c3e79e5c5fe211bfa430/coverage-7.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b37201ce4a458c7a758ecc4efa92fa8ed783c66e0fa3c42ae19fc454a0792153", size = 249659 }, + { url = "https://files.pythonhosted.org/packages/79/3e/53635bd0b72beaacf265784508a0b386defc9ab7fad99ff95f79ce9db555/coverage-7.10.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2904271c80898663c810a6b067920a61dd8d38341244a3605bd31ab55250dad5", size = 247714 }, + { url = "https://files.pythonhosted.org/packages/4c/55/0964aa87126624e8c159e32b0bc4e84edef78c89a1a4b924d28dd8265625/coverage-7.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5aea98383463d6e1fa4e95416d8de66f2d0cb588774ee20ae1b28df826bcb619", size = 248351 }, + { url = "https://files.pythonhosted.org/packages/eb/ab/6cfa9dc518c6c8e14a691c54e53a9433ba67336c760607e299bfcf520cb1/coverage-7.10.6-cp311-cp311-win32.whl", hash = "sha256:e3fb1fa01d3598002777dd259c0c2e6d9d5e10e7222976fc8e03992f972a2cba", size = 219562 }, + { url = "https://files.pythonhosted.org/packages/5b/18/99b25346690cbc55922e7cfef06d755d4abee803ef335baff0014268eff4/coverage-7.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:f35ed9d945bece26553d5b4c8630453169672bea0050a564456eb88bdffd927e", size = 220453 }, + { url = "https://files.pythonhosted.org/packages/d8/ed/81d86648a07ccb124a5cf1f1a7788712b8d7216b593562683cd5c9b0d2c1/coverage-7.10.6-cp311-cp311-win_arm64.whl", hash = "sha256:99e1a305c7765631d74b98bf7dbf54eeea931f975e80f115437d23848ee8c27c", size = 219127 }, + { url = "https://files.pythonhosted.org/packages/26/06/263f3305c97ad78aab066d116b52250dd316e74fcc20c197b61e07eb391a/coverage-7.10.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b2dd6059938063a2c9fee1af729d4f2af28fd1a545e9b7652861f0d752ebcea", size = 217324 }, + { url = "https://files.pythonhosted.org/packages/e9/60/1e1ded9a4fe80d843d7d53b3e395c1db3ff32d6c301e501f393b2e6c1c1f/coverage-7.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:388d80e56191bf846c485c14ae2bc8898aa3124d9d35903fef7d907780477634", size = 217560 }, + { url = "https://files.pythonhosted.org/packages/b8/25/52136173c14e26dfed8b106ed725811bb53c30b896d04d28d74cb64318b3/coverage-7.10.6-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:90cb5b1a4670662719591aa92d0095bb41714970c0b065b02a2610172dbf0af6", size = 249053 }, + { url = "https://files.pythonhosted.org/packages/cb/1d/ae25a7dc58fcce8b172d42ffe5313fc267afe61c97fa872b80ee72d9515a/coverage-7.10.6-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:961834e2f2b863a0e14260a9a273aff07ff7818ab6e66d2addf5628590c628f9", size = 251802 }, + { url = "https://files.pythonhosted.org/packages/f5/7a/1f561d47743710fe996957ed7c124b421320f150f1d38523d8d9102d3e2a/coverage-7.10.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf9a19f5012dab774628491659646335b1928cfc931bf8d97b0d5918dd58033c", size = 252935 }, + { url = "https://files.pythonhosted.org/packages/6c/ad/8b97cd5d28aecdfde792dcbf646bac141167a5cacae2cd775998b45fabb5/coverage-7.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99c4283e2a0e147b9c9cc6bc9c96124de9419d6044837e9799763a0e29a7321a", size = 250855 }, + { url = "https://files.pythonhosted.org/packages/33/6a/95c32b558d9a61858ff9d79580d3877df3eb5bc9eed0941b1f187c89e143/coverage-7.10.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:282b1b20f45df57cc508c1e033403f02283adfb67d4c9c35a90281d81e5c52c5", size = 248974 }, + { url = "https://files.pythonhosted.org/packages/0d/9c/8ce95dee640a38e760d5b747c10913e7a06554704d60b41e73fdea6a1ffd/coverage-7.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cdbe264f11afd69841bd8c0d83ca10b5b32853263ee62e6ac6a0ab63895f972", size = 250409 }, + { url = "https://files.pythonhosted.org/packages/04/12/7a55b0bdde78a98e2eb2356771fd2dcddb96579e8342bb52aa5bc52e96f0/coverage-7.10.6-cp312-cp312-win32.whl", hash = "sha256:a517feaf3a0a3eca1ee985d8373135cfdedfbba3882a5eab4362bda7c7cf518d", size = 219724 }, + { url = "https://files.pythonhosted.org/packages/36/4a/32b185b8b8e327802c9efce3d3108d2fe2d9d31f153a0f7ecfd59c773705/coverage-7.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:856986eadf41f52b214176d894a7de05331117f6035a28ac0016c0f63d887629", size = 220536 }, + { url = "https://files.pythonhosted.org/packages/08/3a/d5d8dc703e4998038c3099eaf77adddb00536a3cec08c8dcd556a36a3eb4/coverage-7.10.6-cp312-cp312-win_arm64.whl", hash = "sha256:acf36b8268785aad739443fa2780c16260ee3fa09d12b3a70f772ef100939d80", size = 219171 }, + { url = "https://files.pythonhosted.org/packages/bd/e7/917e5953ea29a28c1057729c1d5af9084ab6d9c66217523fd0e10f14d8f6/coverage-7.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffea0575345e9ee0144dfe5701aa17f3ba546f8c3bb48db62ae101afb740e7d6", size = 217351 }, + { url = "https://files.pythonhosted.org/packages/eb/86/2e161b93a4f11d0ea93f9bebb6a53f113d5d6e416d7561ca41bb0a29996b/coverage-7.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d91d7317cde40a1c249d6b7382750b7e6d86fad9d8eaf4fa3f8f44cf171e80", size = 217600 }, + { url = "https://files.pythonhosted.org/packages/0e/66/d03348fdd8df262b3a7fb4ee5727e6e4936e39e2f3a842e803196946f200/coverage-7.10.6-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e23dd5408fe71a356b41baa82892772a4cefcf758f2ca3383d2aa39e1b7a003", size = 248600 }, + { url = "https://files.pythonhosted.org/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27", size = 251206 }, + { url = "https://files.pythonhosted.org/packages/e9/1f/9020135734184f439da85c70ea78194c2730e56c2d18aee6e8ff1719d50d/coverage-7.10.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db4a1d897bbbe7339946ffa2fe60c10cc81c43fab8b062d3fcb84188688174a4", size = 252478 }, + { url = "https://files.pythonhosted.org/packages/a4/a4/3d228f3942bb5a2051fde28c136eea23a761177dc4ff4ef54533164ce255/coverage-7.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fd7879082953c156d5b13c74aa6cca37f6a6f4747b39538504c3f9c63d043d", size = 250637 }, + { url = "https://files.pythonhosted.org/packages/36/e3/293dce8cdb9a83de971637afc59b7190faad60603b40e32635cbd15fbf61/coverage-7.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:28395ca3f71cd103b8c116333fa9db867f3a3e1ad6a084aa3725ae002b6583bc", size = 248529 }, + { url = "https://files.pythonhosted.org/packages/90/26/64eecfa214e80dd1d101e420cab2901827de0e49631d666543d0e53cf597/coverage-7.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:61c950fc33d29c91b9e18540e1aed7d9f6787cc870a3e4032493bbbe641d12fc", size = 250143 }, + { url = "https://files.pythonhosted.org/packages/3e/70/bd80588338f65ea5b0d97e424b820fb4068b9cfb9597fbd91963086e004b/coverage-7.10.6-cp313-cp313-win32.whl", hash = "sha256:160c00a5e6b6bdf4e5984b0ef21fc860bc94416c41b7df4d63f536d17c38902e", size = 219770 }, + { url = "https://files.pythonhosted.org/packages/a7/14/0b831122305abcc1060c008f6c97bbdc0a913ab47d65070a01dc50293c2b/coverage-7.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:628055297f3e2aa181464c3808402887643405573eb3d9de060d81531fa79d32", size = 220566 }, + { url = "https://files.pythonhosted.org/packages/83/c6/81a83778c1f83f1a4a168ed6673eeedc205afb562d8500175292ca64b94e/coverage-7.10.6-cp313-cp313-win_arm64.whl", hash = "sha256:df4ec1f8540b0bcbe26ca7dd0f541847cc8a108b35596f9f91f59f0c060bfdd2", size = 219195 }, + { url = "https://files.pythonhosted.org/packages/d7/1c/ccccf4bf116f9517275fa85047495515add43e41dfe8e0bef6e333c6b344/coverage-7.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c9a8b7a34a4de3ed987f636f71881cd3b8339f61118b1aa311fbda12741bff0b", size = 218059 }, + { url = "https://files.pythonhosted.org/packages/92/97/8a3ceff833d27c7492af4f39d5da6761e9ff624831db9e9f25b3886ddbca/coverage-7.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd5af36092430c2b075cee966719898f2ae87b636cefb85a653f1d0ba5d5393", size = 218287 }, + { url = "https://files.pythonhosted.org/packages/92/d8/50b4a32580cf41ff0423777a2791aaf3269ab60c840b62009aec12d3970d/coverage-7.10.6-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0353b0f0850d49ada66fdd7d0c7cdb0f86b900bb9e367024fd14a60cecc1e27", size = 259625 }, + { url = "https://files.pythonhosted.org/packages/7e/7e/6a7df5a6fb440a0179d94a348eb6616ed4745e7df26bf2a02bc4db72c421/coverage-7.10.6-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6b9ae13d5d3e8aeca9ca94198aa7b3ebbc5acfada557d724f2a1f03d2c0b0df", size = 261801 }, + { url = "https://files.pythonhosted.org/packages/3a/4c/a270a414f4ed5d196b9d3d67922968e768cd971d1b251e1b4f75e9362f75/coverage-7.10.6-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:675824a363cc05781b1527b39dc2587b8984965834a748177ee3c37b64ffeafb", size = 264027 }, + { url = "https://files.pythonhosted.org/packages/9c/8b/3210d663d594926c12f373c5370bf1e7c5c3a427519a8afa65b561b9a55c/coverage-7.10.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:692d70ea725f471a547c305f0d0fc6a73480c62fb0da726370c088ab21aed282", size = 261576 }, + { url = "https://files.pythonhosted.org/packages/72/d0/e1961eff67e9e1dba3fc5eb7a4caf726b35a5b03776892da8d79ec895775/coverage-7.10.6-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:851430a9a361c7a8484a36126d1d0ff8d529d97385eacc8dfdc9bfc8c2d2cbe4", size = 259341 }, + { url = "https://files.pythonhosted.org/packages/3a/06/d6478d152cd189b33eac691cba27a40704990ba95de49771285f34a5861e/coverage-7.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d9369a23186d189b2fc95cc08b8160ba242057e887d766864f7adf3c46b2df21", size = 260468 }, + { url = "https://files.pythonhosted.org/packages/ed/73/737440247c914a332f0b47f7598535b29965bf305e19bbc22d4c39615d2b/coverage-7.10.6-cp313-cp313t-win32.whl", hash = "sha256:92be86fcb125e9bda0da7806afd29a3fd33fdf58fba5d60318399adf40bf37d0", size = 220429 }, + { url = "https://files.pythonhosted.org/packages/bd/76/b92d3214740f2357ef4a27c75a526eb6c28f79c402e9f20a922c295c05e2/coverage-7.10.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6b3039e2ca459a70c79523d39347d83b73f2f06af5624905eba7ec34d64d80b5", size = 221493 }, + { url = "https://files.pythonhosted.org/packages/fc/8e/6dcb29c599c8a1f654ec6cb68d76644fe635513af16e932d2d4ad1e5ac6e/coverage-7.10.6-cp313-cp313t-win_arm64.whl", hash = "sha256:3fb99d0786fe17b228eab663d16bee2288e8724d26a199c29325aac4b0319b9b", size = 219757 }, + { url = "https://files.pythonhosted.org/packages/d3/aa/76cf0b5ec00619ef208da4689281d48b57f2c7fde883d14bf9441b74d59f/coverage-7.10.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6008a021907be8c4c02f37cdc3ffb258493bdebfeaf9a839f9e71dfdc47b018e", size = 217331 }, + { url = "https://files.pythonhosted.org/packages/65/91/8e41b8c7c505d398d7730206f3cbb4a875a35ca1041efc518051bfce0f6b/coverage-7.10.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5e75e37f23eb144e78940b40395b42f2321951206a4f50e23cfd6e8a198d3ceb", size = 217607 }, + { url = "https://files.pythonhosted.org/packages/87/7f/f718e732a423d442e6616580a951b8d1ec3575ea48bcd0e2228386805e79/coverage-7.10.6-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0f7cb359a448e043c576f0da00aa8bfd796a01b06aa610ca453d4dde09cc1034", size = 248663 }, + { url = "https://files.pythonhosted.org/packages/e6/52/c1106120e6d801ac03e12b5285e971e758e925b6f82ee9b86db3aa10045d/coverage-7.10.6-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c68018e4fc4e14b5668f1353b41ccf4bc83ba355f0e1b3836861c6f042d89ac1", size = 251197 }, + { url = "https://files.pythonhosted.org/packages/3d/ec/3a8645b1bb40e36acde9c0609f08942852a4af91a937fe2c129a38f2d3f5/coverage-7.10.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd4b2b0707fc55afa160cd5fc33b27ccbf75ca11d81f4ec9863d5793fc6df56a", size = 252551 }, + { url = "https://files.pythonhosted.org/packages/a1/70/09ecb68eeb1155b28a1d16525fd3a9b65fbe75337311a99830df935d62b6/coverage-7.10.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cec13817a651f8804a86e4f79d815b3b28472c910e099e4d5a0e8a3b6a1d4cb", size = 250553 }, + { url = "https://files.pythonhosted.org/packages/c6/80/47df374b893fa812e953b5bc93dcb1427a7b3d7a1a7d2db33043d17f74b9/coverage-7.10.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f2a6a8e06bbda06f78739f40bfb56c45d14eb8249d0f0ea6d4b3d48e1f7c695d", size = 248486 }, + { url = "https://files.pythonhosted.org/packages/4a/65/9f98640979ecee1b0d1a7164b589de720ddf8100d1747d9bbdb84be0c0fb/coverage-7.10.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:081b98395ced0d9bcf60ada7661a0b75f36b78b9d7e39ea0790bb4ed8da14747", size = 249981 }, + { url = "https://files.pythonhosted.org/packages/1f/55/eeb6603371e6629037f47bd25bef300387257ed53a3c5fdb159b7ac8c651/coverage-7.10.6-cp314-cp314-win32.whl", hash = "sha256:6937347c5d7d069ee776b2bf4e1212f912a9f1f141a429c475e6089462fcecc5", size = 220054 }, + { url = "https://files.pythonhosted.org/packages/15/d1/a0912b7611bc35412e919a2cd59ae98e7ea3b475e562668040a43fb27897/coverage-7.10.6-cp314-cp314-win_amd64.whl", hash = "sha256:adec1d980fa07e60b6ef865f9e5410ba760e4e1d26f60f7e5772c73b9a5b0713", size = 220851 }, + { url = "https://files.pythonhosted.org/packages/ef/2d/11880bb8ef80a45338e0b3e0725e4c2d73ffbb4822c29d987078224fd6a5/coverage-7.10.6-cp314-cp314-win_arm64.whl", hash = "sha256:a80f7aef9535442bdcf562e5a0d5a5538ce8abe6bb209cfbf170c462ac2c2a32", size = 219429 }, + { url = "https://files.pythonhosted.org/packages/83/c0/1f00caad775c03a700146f55536ecd097a881ff08d310a58b353a1421be0/coverage-7.10.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0de434f4fbbe5af4fa7989521c655c8c779afb61c53ab561b64dcee6149e4c65", size = 218080 }, + { url = "https://files.pythonhosted.org/packages/a9/c4/b1c5d2bd7cc412cbeb035e257fd06ed4e3e139ac871d16a07434e145d18d/coverage-7.10.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6e31b8155150c57e5ac43ccd289d079eb3f825187d7c66e755a055d2c85794c6", size = 218293 }, + { url = "https://files.pythonhosted.org/packages/3f/07/4468d37c94724bf6ec354e4ec2f205fda194343e3e85fd2e59cec57e6a54/coverage-7.10.6-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:98cede73eb83c31e2118ae8d379c12e3e42736903a8afcca92a7218e1f2903b0", size = 259800 }, + { url = "https://files.pythonhosted.org/packages/82/d8/f8fb351be5fee31690cd8da768fd62f1cfab33c31d9f7baba6cd8960f6b8/coverage-7.10.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f863c08f4ff6b64fa8045b1e3da480f5374779ef187f07b82e0538c68cb4ff8e", size = 261965 }, + { url = "https://files.pythonhosted.org/packages/e8/70/65d4d7cfc75c5c6eb2fed3ee5cdf420fd8ae09c4808723a89a81d5b1b9c3/coverage-7.10.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b38261034fda87be356f2c3f42221fdb4171c3ce7658066ae449241485390d5", size = 264220 }, + { url = "https://files.pythonhosted.org/packages/98/3c/069df106d19024324cde10e4ec379fe2fb978017d25e97ebee23002fbadf/coverage-7.10.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e93b1476b79eae849dc3872faeb0bf7948fd9ea34869590bc16a2a00b9c82a7", size = 261660 }, + { url = "https://files.pythonhosted.org/packages/fc/8a/2974d53904080c5dc91af798b3a54a4ccb99a45595cc0dcec6eb9616a57d/coverage-7.10.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ff8a991f70f4c0cf53088abf1e3886edcc87d53004c7bb94e78650b4d3dac3b5", size = 259417 }, + { url = "https://files.pythonhosted.org/packages/30/38/9616a6b49c686394b318974d7f6e08f38b8af2270ce7488e879888d1e5db/coverage-7.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ac765b026c9f33044419cbba1da913cfb82cca1b60598ac1c7a5ed6aac4621a0", size = 260567 }, + { url = "https://files.pythonhosted.org/packages/76/16/3ed2d6312b371a8cf804abf4e14895b70e4c3491c6e53536d63fd0958a8d/coverage-7.10.6-cp314-cp314t-win32.whl", hash = "sha256:441c357d55f4936875636ef2cfb3bee36e466dcf50df9afbd398ce79dba1ebb7", size = 220831 }, + { url = "https://files.pythonhosted.org/packages/d5/e5/d38d0cb830abede2adb8b147770d2a3d0e7fecc7228245b9b1ae6c24930a/coverage-7.10.6-cp314-cp314t-win_amd64.whl", hash = "sha256:073711de3181b2e204e4870ac83a7c4853115b42e9cd4d145f2231e12d670930", size = 221950 }, + { url = "https://files.pythonhosted.org/packages/f4/51/e48e550f6279349895b0ffcd6d2a690e3131ba3a7f4eafccc141966d4dea/coverage-7.10.6-cp314-cp314t-win_arm64.whl", hash = "sha256:137921f2bac5559334ba66122b753db6dc5d1cf01eb7b64eb412bb0d064ef35b", size = 219969 }, + { url = "https://files.pythonhosted.org/packages/44/0c/50db5379b615854b5cf89146f8f5bd1d5a9693d7f3a987e269693521c404/coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3", size = 208986 }, ] [package.optional-dependencies] @@ -628,7 +643,7 @@ wheels = [ [[package]] name = "eth-utils" -version = "5.3.0" +version = "5.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cytoolz", marker = "implementation_name == 'cpython'" }, @@ -637,9 +652,9 @@ dependencies = [ { name = "pydantic" }, { name = "toolz", marker = "implementation_name == 'pypy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0d/49/bee95f16d2ef068097afeeffbd6c67738107001ee57ad7bcdd4fc4d3c6a7/eth_utils-5.3.0.tar.gz", hash = "sha256:1f096867ac6be895f456fa3acb26e9573ae66e753abad9208f316d24d6178156", size = 123753 } +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/c4/c6/0417a92e6a3fc9b85f5a8380d9f9d43b69ba836a90e45f79f9ae74d41e53/eth_utils-5.3.0-py3-none-any.whl", hash = "sha256:ac184883ab299d923428bbe25dae5e356979a3993e0ef695a864db0a20bc262d", size = 102531 }, + { url = "https://files.pythonhosted.org/packages/bf/4d/257cdc01ada430b8e84b9f2385c2553f33218f5b47da9adf0a616308d4b7/eth_utils-5.3.1-py3-none-any.whl", hash = "sha256:1f5476d8f29588d25b8ae4987e1ffdfae6d4c09026e476c4aad13b32dda3ead0", size = 102529 }, ] [[package]] @@ -695,16 +710,16 @@ wheels = [ [[package]] name = "hypothesis" -version = "6.137.1" +version = "6.138.14" 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/65/92/70f29b04e4d7acf7f9a0c3dd52619853715ad9ae092a8e5d89bc7bdc39ec/hypothesis-6.137.1.tar.gz", hash = "sha256:b086e644456da79ad460fdaf8fbf90a41a661e8a4076232dd4ea64cfbc0d0529", size = 460593 } +sdist = { url = "https://files.pythonhosted.org/packages/67/ed/2f65a358dd6b5bf3cee99dd2d3473eb9c5d5e50b50f83bbf8ef89ea96e39/hypothesis-6.138.14.tar.gz", hash = "sha256:5c1aa1ce3f1094b5c04ea03476017695bda408a174330e5275e40ddd06d3307a", size = 466152 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/13/92753f97f70f3584a70ccbd2a678878ea43d5880c4e009664c3fe9fe7e50/hypothesis-6.137.1-py3-none-any.whl", hash = "sha256:7cbda6a98ed4d32aad31a5fc5bff5e119b9275fe2579a7b08863cba313a4b9be", size = 527566 }, + { url = "https://files.pythonhosted.org/packages/55/2e/fcf7371887f45083472165b21b68a7f552049334e01dc3febe484bbb7bc4/hypothesis-6.138.14-py3-none-any.whl", hash = "sha256:1a702ecfff7034b3252d7a83328093388641cdba863197169559839e841c2154", size = 533626 }, ] [[package]] @@ -849,14 +864,14 @@ wheels = [ [[package]] name = "markdown-it-py" -version = "3.0.0" +version = "4.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +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/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, + { 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]] @@ -1045,11 +1060,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.3.8" +version = "4.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362 } +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 }, + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654 }, ] [[package]] @@ -1267,7 +1282,7 @@ wheels = [ [[package]] name = "pytest" -version = "8.4.1" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1278,9 +1293,9 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714 } +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/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474 }, + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750 }, ] [[package]] @@ -1407,86 +1422,86 @@ wheels = [ [[package]] name = "regex" -version = "2025.7.34" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/de/e13fa6dc61d78b30ba47481f99933a3b49a57779d625c392d8036770a60d/regex-2025.7.34.tar.gz", hash = "sha256:9ead9765217afd04a86822dfcd4ed2747dfe426e887da413b15ff0ac2457e21a", size = 400714 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/50/d2/0a44a9d92370e5e105f16669acf801b215107efea9dea4317fe96e9aad67/regex-2025.7.34-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d856164d25e2b3b07b779bfed813eb4b6b6ce73c2fd818d46f47c1eb5cd79bd6", size = 484591 }, - { url = "https://files.pythonhosted.org/packages/2e/b1/00c4f83aa902f1048495de9f2f33638ce970ce1cf9447b477d272a0e22bb/regex-2025.7.34-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d15a9da5fad793e35fb7be74eec450d968e05d2e294f3e0e77ab03fa7234a83", size = 289293 }, - { url = "https://files.pythonhosted.org/packages/f3/b0/5bc5c8ddc418e8be5530b43ae1f7c9303f43aeff5f40185c4287cf6732f2/regex-2025.7.34-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:95b4639c77d414efa93c8de14ce3f7965a94d007e068a94f9d4997bb9bd9c81f", size = 285932 }, - { url = "https://files.pythonhosted.org/packages/46/c7/a1a28d050b23665a5e1eeb4d7f13b83ea86f0bc018da7b8f89f86ff7f094/regex-2025.7.34-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7de1ceed5a5f84f342ba4a9f4ae589524adf9744b2ee61b5da884b5b659834", size = 780361 }, - { url = "https://files.pythonhosted.org/packages/cb/0d/82e7afe7b2c9fe3d488a6ab6145d1d97e55f822dfb9b4569aba2497e3d09/regex-2025.7.34-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02e5860a250cd350c4933cf376c3bc9cb28948e2c96a8bc042aee7b985cfa26f", size = 849176 }, - { url = "https://files.pythonhosted.org/packages/bf/16/3036e16903d8194f1490af457a7e33b06d9e9edd9576b1fe6c7ac660e9ed/regex-2025.7.34-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a5966220b9a1a88691282b7e4350e9599cf65780ca60d914a798cb791aa1177", size = 897222 }, - { url = "https://files.pythonhosted.org/packages/5a/c2/010e089ae00d31418e7d2c6601760eea1957cde12be719730c7133b8c165/regex-2025.7.34-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48fb045bbd4aab2418dc1ba2088a5e32de4bfe64e1457b948bb328a8dc2f1c2e", size = 789831 }, - { url = "https://files.pythonhosted.org/packages/dd/86/b312b7bf5c46d21dbd9a3fdc4a80fde56ea93c9c0b89cf401879635e094d/regex-2025.7.34-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:20ff8433fa45e131f7316594efe24d4679c5449c0ca69d91c2f9d21846fdf064", size = 780665 }, - { url = "https://files.pythonhosted.org/packages/40/e5/674b82bfff112c820b09e3c86a423d4a568143ede7f8440fdcbce259e895/regex-2025.7.34-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c436fd1e95c04c19039668cfb548450a37c13f051e8659f40aed426e36b3765f", size = 773511 }, - { url = "https://files.pythonhosted.org/packages/2d/18/39e7c578eb6cf1454db2b64e4733d7e4f179714867a75d84492ec44fa9b2/regex-2025.7.34-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0b85241d3cfb9f8a13cefdfbd58a2843f208f2ed2c88181bf84e22e0c7fc066d", size = 843990 }, - { url = "https://files.pythonhosted.org/packages/b6/d9/522a6715aefe2f463dc60c68924abeeb8ab6893f01adf5720359d94ede8c/regex-2025.7.34-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:075641c94126b064c65ab86e7e71fc3d63e7ff1bea1fb794f0773c97cdad3a03", size = 834676 }, - { url = "https://files.pythonhosted.org/packages/59/53/c4d5284cb40543566542e24f1badc9f72af68d01db21e89e36e02292eee0/regex-2025.7.34-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:70645cad3407d103d1dbcb4841839d2946f7d36cf38acbd40120fee1682151e5", size = 778420 }, - { url = "https://files.pythonhosted.org/packages/ea/4a/b779a7707d4a44a7e6ee9d0d98e40b2a4de74d622966080e9c95e25e2d24/regex-2025.7.34-cp310-cp310-win32.whl", hash = "sha256:3b836eb4a95526b263c2a3359308600bd95ce7848ebd3c29af0c37c4f9627cd3", size = 263999 }, - { url = "https://files.pythonhosted.org/packages/ef/6e/33c7583f5427aa039c28bff7f4103c2de5b6aa5b9edc330c61ec576b1960/regex-2025.7.34-cp310-cp310-win_amd64.whl", hash = "sha256:cbfaa401d77334613cf434f723c7e8ba585df162be76474bccc53ae4e5520b3a", size = 276023 }, - { url = "https://files.pythonhosted.org/packages/9f/fc/00b32e0ac14213d76d806d952826402b49fd06d42bfabacdf5d5d016bc47/regex-2025.7.34-cp310-cp310-win_arm64.whl", hash = "sha256:bca11d3c38a47c621769433c47f364b44e8043e0de8e482c5968b20ab90a3986", size = 268357 }, - { url = "https://files.pythonhosted.org/packages/0d/85/f497b91577169472f7c1dc262a5ecc65e39e146fc3a52c571e5daaae4b7d/regex-2025.7.34-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da304313761b8500b8e175eb2040c4394a875837d5635f6256d6fa0377ad32c8", size = 484594 }, - { url = "https://files.pythonhosted.org/packages/1c/c5/ad2a5c11ce9e6257fcbfd6cd965d07502f6054aaa19d50a3d7fd991ec5d1/regex-2025.7.34-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:35e43ebf5b18cd751ea81455b19acfdec402e82fe0dc6143edfae4c5c4b3909a", size = 289294 }, - { url = "https://files.pythonhosted.org/packages/8e/01/83ffd9641fcf5e018f9b51aa922c3e538ac9439424fda3df540b643ecf4f/regex-2025.7.34-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96bbae4c616726f4661fe7bcad5952e10d25d3c51ddc388189d8864fbc1b3c68", size = 285933 }, - { url = "https://files.pythonhosted.org/packages/77/20/5edab2e5766f0259bc1da7381b07ce6eb4401b17b2254d02f492cd8a81a8/regex-2025.7.34-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9feab78a1ffa4f2b1e27b1bcdaad36f48c2fed4870264ce32f52a393db093c78", size = 792335 }, - { url = "https://files.pythonhosted.org/packages/30/bd/744d3ed8777dce8487b2606b94925e207e7c5931d5870f47f5b643a4580a/regex-2025.7.34-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f14b36e6d4d07f1a5060f28ef3b3561c5d95eb0651741474ce4c0a4c56ba8719", size = 858605 }, - { url = "https://files.pythonhosted.org/packages/99/3d/93754176289718d7578c31d151047e7b8acc7a8c20e7706716f23c49e45e/regex-2025.7.34-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85c3a958ef8b3d5079c763477e1f09e89d13ad22198a37e9d7b26b4b17438b33", size = 905780 }, - { url = "https://files.pythonhosted.org/packages/ee/2e/c689f274a92deffa03999a430505ff2aeace408fd681a90eafa92fdd6930/regex-2025.7.34-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37555e4ae0b93358fa7c2d240a4291d4a4227cc7c607d8f85596cdb08ec0a083", size = 798868 }, - { url = "https://files.pythonhosted.org/packages/0d/9e/39673688805d139b33b4a24851a71b9978d61915c4d72b5ffda324d0668a/regex-2025.7.34-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee38926f31f1aa61b0232a3a11b83461f7807661c062df9eb88769d86e6195c3", size = 781784 }, - { url = "https://files.pythonhosted.org/packages/18/bd/4c1cab12cfabe14beaa076523056b8ab0c882a8feaf0a6f48b0a75dab9ed/regex-2025.7.34-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a664291c31cae9c4a30589bd8bc2ebb56ef880c9c6264cb7643633831e606a4d", size = 852837 }, - { url = "https://files.pythonhosted.org/packages/cb/21/663d983cbb3bba537fc213a579abbd0f263fb28271c514123f3c547ab917/regex-2025.7.34-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f3e5c1e0925e77ec46ddc736b756a6da50d4df4ee3f69536ffb2373460e2dafd", size = 844240 }, - { url = "https://files.pythonhosted.org/packages/8e/2d/9beeeb913bc5d32faa913cf8c47e968da936af61ec20af5d269d0f84a100/regex-2025.7.34-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d428fc7731dcbb4e2ffe43aeb8f90775ad155e7db4347a639768bc6cd2df881a", size = 787139 }, - { url = "https://files.pythonhosted.org/packages/eb/f5/9b9384415fdc533551be2ba805dd8c4621873e5df69c958f403bfd3b2b6e/regex-2025.7.34-cp311-cp311-win32.whl", hash = "sha256:e154a7ee7fa18333ad90b20e16ef84daaeac61877c8ef942ec8dfa50dc38b7a1", size = 264019 }, - { url = "https://files.pythonhosted.org/packages/18/9d/e069ed94debcf4cc9626d652a48040b079ce34c7e4fb174f16874958d485/regex-2025.7.34-cp311-cp311-win_amd64.whl", hash = "sha256:24257953d5c1d6d3c129ab03414c07fc1a47833c9165d49b954190b2b7f21a1a", size = 276047 }, - { url = "https://files.pythonhosted.org/packages/fd/cf/3bafbe9d1fd1db77355e7fbbbf0d0cfb34501a8b8e334deca14f94c7b315/regex-2025.7.34-cp311-cp311-win_arm64.whl", hash = "sha256:3157aa512b9e606586900888cd469a444f9b898ecb7f8931996cb715f77477f0", size = 268362 }, - { url = "https://files.pythonhosted.org/packages/ff/f0/31d62596c75a33f979317658e8d261574785c6cd8672c06741ce2e2e2070/regex-2025.7.34-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7f7211a746aced993bef487de69307a38c5ddd79257d7be83f7b202cb59ddb50", size = 485492 }, - { url = "https://files.pythonhosted.org/packages/d8/16/b818d223f1c9758c3434be89aa1a01aae798e0e0df36c1f143d1963dd1ee/regex-2025.7.34-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fb31080f2bd0681484b275461b202b5ad182f52c9ec606052020fe13eb13a72f", size = 290000 }, - { url = "https://files.pythonhosted.org/packages/cd/70/69506d53397b4bd6954061bae75677ad34deb7f6ca3ba199660d6f728ff5/regex-2025.7.34-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0200a5150c4cf61e407038f4b4d5cdad13e86345dac29ff9dab3d75d905cf130", size = 286072 }, - { url = "https://files.pythonhosted.org/packages/b0/73/536a216d5f66084fb577bb0543b5cb7de3272eb70a157f0c3a542f1c2551/regex-2025.7.34-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:739a74970e736df0773788377969c9fea3876c2fc13d0563f98e5503e5185f46", size = 797341 }, - { url = "https://files.pythonhosted.org/packages/26/af/733f8168449e56e8f404bb807ea7189f59507cbea1b67a7bbcd92f8bf844/regex-2025.7.34-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4fef81b2f7ea6a2029161ed6dea9ae13834c28eb5a95b8771828194a026621e4", size = 862556 }, - { url = "https://files.pythonhosted.org/packages/19/dd/59c464d58c06c4f7d87de4ab1f590e430821345a40c5d345d449a636d15f/regex-2025.7.34-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea74cf81fe61a7e9d77989050d0089a927ab758c29dac4e8e1b6c06fccf3ebf0", size = 910762 }, - { url = "https://files.pythonhosted.org/packages/37/a8/b05ccf33ceca0815a1e253693b2c86544932ebcc0049c16b0fbdf18b688b/regex-2025.7.34-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4636a7f3b65a5f340ed9ddf53585c42e3ff37101d383ed321bfe5660481744b", size = 801892 }, - { url = "https://files.pythonhosted.org/packages/5f/9a/b993cb2e634cc22810afd1652dba0cae156c40d4864285ff486c73cd1996/regex-2025.7.34-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cef962d7834437fe8d3da6f9bfc6f93f20f218266dcefec0560ed7765f5fe01", size = 786551 }, - { url = "https://files.pythonhosted.org/packages/2d/79/7849d67910a0de4e26834b5bb816e028e35473f3d7ae563552ea04f58ca2/regex-2025.7.34-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:cbe1698e5b80298dbce8df4d8d1182279fbdaf1044e864cbc9d53c20e4a2be77", size = 856457 }, - { url = "https://files.pythonhosted.org/packages/91/c6/de516bc082524b27e45cb4f54e28bd800c01efb26d15646a65b87b13a91e/regex-2025.7.34-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:32b9f9bcf0f605eb094b08e8da72e44badabb63dde6b83bd530580b488d1c6da", size = 848902 }, - { url = "https://files.pythonhosted.org/packages/7d/22/519ff8ba15f732db099b126f039586bd372da6cd4efb810d5d66a5daeda1/regex-2025.7.34-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:524c868ba527eab4e8744a9287809579f54ae8c62fbf07d62aacd89f6026b282", size = 788038 }, - { url = "https://files.pythonhosted.org/packages/3f/7d/aabb467d8f57d8149895d133c88eb809a1a6a0fe262c1d508eb9dfabb6f9/regex-2025.7.34-cp312-cp312-win32.whl", hash = "sha256:d600e58ee6d036081c89696d2bdd55d507498a7180df2e19945c6642fac59588", size = 264417 }, - { url = "https://files.pythonhosted.org/packages/3b/39/bd922b55a4fc5ad5c13753274e5b536f5b06ec8eb9747675668491c7ab7a/regex-2025.7.34-cp312-cp312-win_amd64.whl", hash = "sha256:9a9ab52a466a9b4b91564437b36417b76033e8778e5af8f36be835d8cb370d62", size = 275387 }, - { url = "https://files.pythonhosted.org/packages/f7/3c/c61d2fdcecb754a40475a3d1ef9a000911d3e3fc75c096acf44b0dfb786a/regex-2025.7.34-cp312-cp312-win_arm64.whl", hash = "sha256:c83aec91af9c6fbf7c743274fd952272403ad9a9db05fe9bfc9df8d12b45f176", size = 268482 }, - { url = "https://files.pythonhosted.org/packages/15/16/b709b2119975035169a25aa8e4940ca177b1a2e25e14f8d996d09130368e/regex-2025.7.34-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3c9740a77aeef3f5e3aaab92403946a8d34437db930a0280e7e81ddcada61f5", size = 485334 }, - { url = "https://files.pythonhosted.org/packages/94/a6/c09136046be0595f0331bc58a0e5f89c2d324cf734e0b0ec53cf4b12a636/regex-2025.7.34-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:69ed3bc611540f2ea70a4080f853741ec698be556b1df404599f8724690edbcd", size = 289942 }, - { url = "https://files.pythonhosted.org/packages/36/91/08fc0fd0f40bdfb0e0df4134ee37cfb16e66a1044ac56d36911fd01c69d2/regex-2025.7.34-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d03c6f9dcd562c56527c42b8530aad93193e0b3254a588be1f2ed378cdfdea1b", size = 285991 }, - { url = "https://files.pythonhosted.org/packages/be/2f/99dc8f6f756606f0c214d14c7b6c17270b6bbe26d5c1f05cde9dbb1c551f/regex-2025.7.34-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6164b1d99dee1dfad33f301f174d8139d4368a9fb50bf0a3603b2eaf579963ad", size = 797415 }, - { url = "https://files.pythonhosted.org/packages/62/cf/2fcdca1110495458ba4e95c52ce73b361cf1cafd8a53b5c31542cde9a15b/regex-2025.7.34-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1e4f4f62599b8142362f164ce776f19d79bdd21273e86920a7b604a4275b4f59", size = 862487 }, - { url = "https://files.pythonhosted.org/packages/90/38/899105dd27fed394e3fae45607c1983e138273ec167e47882fc401f112b9/regex-2025.7.34-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:72a26dcc6a59c057b292f39d41465d8233a10fd69121fa24f8f43ec6294e5415", size = 910717 }, - { url = "https://files.pythonhosted.org/packages/ee/f6/4716198dbd0bcc9c45625ac4c81a435d1c4d8ad662e8576dac06bab35b17/regex-2025.7.34-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5273fddf7a3e602695c92716c420c377599ed3c853ea669c1fe26218867002f", size = 801943 }, - { url = "https://files.pythonhosted.org/packages/40/5d/cff8896d27e4e3dd11dd72ac78797c7987eb50fe4debc2c0f2f1682eb06d/regex-2025.7.34-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c1844be23cd40135b3a5a4dd298e1e0c0cb36757364dd6cdc6025770363e06c1", size = 786664 }, - { url = "https://files.pythonhosted.org/packages/10/29/758bf83cf7b4c34f07ac3423ea03cee3eb3176941641e4ccc05620f6c0b8/regex-2025.7.34-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dde35e2afbbe2272f8abee3b9fe6772d9b5a07d82607b5788e8508974059925c", size = 856457 }, - { url = "https://files.pythonhosted.org/packages/d7/30/c19d212b619963c5b460bfed0ea69a092c6a43cba52a973d46c27b3e2975/regex-2025.7.34-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f6e8e7af516a7549412ce57613e859c3be27d55341a894aacaa11703a4c31a", size = 849008 }, - { url = "https://files.pythonhosted.org/packages/9e/b8/3c35da3b12c87e3cc00010ef6c3a4ae787cff0bc381aa3d251def219969a/regex-2025.7.34-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:469142fb94a869beb25b5f18ea87646d21def10fbacb0bcb749224f3509476f0", size = 788101 }, - { url = "https://files.pythonhosted.org/packages/47/80/2f46677c0b3c2b723b2c358d19f9346e714113865da0f5f736ca1a883bde/regex-2025.7.34-cp313-cp313-win32.whl", hash = "sha256:da7507d083ee33ccea1310447410c27ca11fb9ef18c95899ca57ff60a7e4d8f1", size = 264401 }, - { url = "https://files.pythonhosted.org/packages/be/fa/917d64dd074682606a003cba33585c28138c77d848ef72fc77cbb1183849/regex-2025.7.34-cp313-cp313-win_amd64.whl", hash = "sha256:9d644de5520441e5f7e2db63aec2748948cc39ed4d7a87fd5db578ea4043d997", size = 275368 }, - { url = "https://files.pythonhosted.org/packages/65/cd/f94383666704170a2154a5df7b16be28f0c27a266bffcd843e58bc84120f/regex-2025.7.34-cp313-cp313-win_arm64.whl", hash = "sha256:7bf1c5503a9f2cbd2f52d7e260acb3131b07b6273c470abb78568174fe6bde3f", size = 268482 }, - { url = "https://files.pythonhosted.org/packages/ac/23/6376f3a23cf2f3c00514b1cdd8c990afb4dfbac3cb4a68b633c6b7e2e307/regex-2025.7.34-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:8283afe7042d8270cecf27cca558873168e771183d4d593e3c5fe5f12402212a", size = 485385 }, - { url = "https://files.pythonhosted.org/packages/73/5b/6d4d3a0b4d312adbfd6d5694c8dddcf1396708976dd87e4d00af439d962b/regex-2025.7.34-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6c053f9647e3421dd2f5dff8172eb7b4eec129df9d1d2f7133a4386319b47435", size = 289788 }, - { url = "https://files.pythonhosted.org/packages/92/71/5862ac9913746e5054d01cb9fb8125b3d0802c0706ef547cae1e7f4428fa/regex-2025.7.34-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a16dd56bbcb7d10e62861c3cd000290ddff28ea142ffb5eb3470f183628011ac", size = 286136 }, - { url = "https://files.pythonhosted.org/packages/27/df/5b505dc447eb71278eba10d5ec940769ca89c1af70f0468bfbcb98035dc2/regex-2025.7.34-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69c593ff5a24c0d5c1112b0df9b09eae42b33c014bdca7022d6523b210b69f72", size = 797753 }, - { url = "https://files.pythonhosted.org/packages/86/38/3e3dc953d13998fa047e9a2414b556201dbd7147034fbac129392363253b/regex-2025.7.34-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98d0ce170fcde1a03b5df19c5650db22ab58af375aaa6ff07978a85c9f250f0e", size = 863263 }, - { url = "https://files.pythonhosted.org/packages/68/e5/3ff66b29dde12f5b874dda2d9dec7245c2051f2528d8c2a797901497f140/regex-2025.7.34-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d72765a4bff8c43711d5b0f5b452991a9947853dfa471972169b3cc0ba1d0751", size = 910103 }, - { url = "https://files.pythonhosted.org/packages/9e/fe/14176f2182125977fba3711adea73f472a11f3f9288c1317c59cd16ad5e6/regex-2025.7.34-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4494f8fd95a77eb434039ad8460e64d57baa0434f1395b7da44015bef650d0e4", size = 801709 }, - { url = "https://files.pythonhosted.org/packages/5a/0d/80d4e66ed24f1ba876a9e8e31b709f9fd22d5c266bf5f3ab3c1afe683d7d/regex-2025.7.34-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4f42b522259c66e918a0121a12429b2abcf696c6f967fa37bdc7b72e61469f98", size = 786726 }, - { url = "https://files.pythonhosted.org/packages/12/75/c3ebb30e04a56c046f5c85179dc173818551037daae2c0c940c7b19152cb/regex-2025.7.34-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:aaef1f056d96a0a5d53ad47d019d5b4c66fe4be2da87016e0d43b7242599ffc7", size = 857306 }, - { url = "https://files.pythonhosted.org/packages/b1/b2/a4dc5d8b14f90924f27f0ac4c4c4f5e195b723be98adecc884f6716614b6/regex-2025.7.34-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:656433e5b7dccc9bc0da6312da8eb897b81f5e560321ec413500e5367fcd5d47", size = 848494 }, - { url = "https://files.pythonhosted.org/packages/0d/21/9ac6e07a4c5e8646a90b56b61f7e9dac11ae0747c857f91d3d2bc7c241d9/regex-2025.7.34-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e91eb2c62c39705e17b4d42d4b86c4e86c884c0d15d9c5a47d0835f8387add8e", size = 787850 }, - { url = "https://files.pythonhosted.org/packages/be/6c/d51204e28e7bc54f9a03bb799b04730d7e54ff2718862b8d4e09e7110a6a/regex-2025.7.34-cp314-cp314-win32.whl", hash = "sha256:f978ddfb6216028c8f1d6b0f7ef779949498b64117fc35a939022f67f810bdcb", size = 269730 }, - { url = "https://files.pythonhosted.org/packages/74/52/a7e92d02fa1fdef59d113098cb9f02c5d03289a0e9f9e5d4d6acccd10677/regex-2025.7.34-cp314-cp314-win_amd64.whl", hash = "sha256:4b7dc33b9b48fb37ead12ffc7bdb846ac72f99a80373c4da48f64b373a7abeae", size = 278640 }, - { url = "https://files.pythonhosted.org/packages/d1/78/a815529b559b1771080faa90c3ab401730661f99d495ab0071649f139ebd/regex-2025.7.34-cp314-cp314-win_arm64.whl", hash = "sha256:4b8c4d39f451e64809912c82392933d80fe2e4a87eeef8859fcc5380d0173c64", size = 271757 }, +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/5a/4c63457fbcaf19d138d72b2e9b39405954f98c0349b31c601bfcb151582c/regex-2025.9.1.tar.gz", hash = "sha256:88ac07b38d20b54d79e704e38aa3bd2c0f8027432164226bdee201a1c0c9c9ff", size = 400852 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/c1/ed9ef923156105a78aa004f9390e5dd87eadc29f5ca8840f172cadb638de/regex-2025.9.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c5aa2a6a73bf218515484b36a0d20c6ad9dc63f6339ff6224147b0e2c095ee55", size = 484813 }, + { url = "https://files.pythonhosted.org/packages/05/de/97957618a774c67f892609eee2fafe3e30703fbbba66de5e6b79d7196dbc/regex-2025.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c2ff5c01d5e47ad5fc9d31bcd61e78c2fa0068ed00cab86b7320214446da766", size = 288981 }, + { url = "https://files.pythonhosted.org/packages/3c/b0/441afadd0a6ffccbd58a9663e5bdd182daa237893e5f8ceec6ff9df4418a/regex-2025.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d49dc84e796b666181de8a9973284cad6616335f01b52bf099643253094920fc", size = 286608 }, + { url = "https://files.pythonhosted.org/packages/6e/cf/d89aecaf17e999ab11a3ef73fc9ab8b64f4e156f121250ef84340b35338d/regex-2025.9.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9914fe1040874f83c15fcea86d94ea54091b0666eab330aaab69e30d106aabe", size = 780459 }, + { url = "https://files.pythonhosted.org/packages/f6/05/05884594a9975a29597917bbdd6837f7b97e8ac23faf22d628aa781e58f7/regex-2025.9.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e71bceb3947362ec5eabd2ca0870bb78eae4edfc60c6c21495133c01b6cd2df4", size = 849276 }, + { url = "https://files.pythonhosted.org/packages/8c/8d/2b3067506838d02096bf107beb129b2ce328cdf776d6474b7f542c0a7bfd/regex-2025.9.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67a74456f410fe5e869239ee7a5423510fe5121549af133809d9591a8075893f", size = 897320 }, + { url = "https://files.pythonhosted.org/packages/9e/b3/0f9f7766e980b900df0ba9901b52871a2e4203698fb35cdebd219240d5f7/regex-2025.9.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c3b96ed0223b32dbdc53a83149b6de7ca3acd5acd9c8e64b42a166228abe29c", size = 789931 }, + { url = "https://files.pythonhosted.org/packages/47/9f/7b2f29c8f8b698eb44be5fc68e8b9c8d32e99635eac5defc98de114e9f35/regex-2025.9.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:113d5aa950f428faf46fd77d452df62ebb4cc6531cb619f6cc30a369d326bfbd", size = 780764 }, + { url = "https://files.pythonhosted.org/packages/ac/ac/56176caa86155c14462531eb0a4ddc450d17ba8875001122b3b7c0cb01bf/regex-2025.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fcdeb38de4f7f3d69d798f4f371189061446792a84e7c92b50054c87aae9c07c", size = 773610 }, + { url = "https://files.pythonhosted.org/packages/39/e8/9d6b9bd43998268a9de2f35602077519cacc9cb149f7381758cf8f502ba7/regex-2025.9.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4bcdff370509164b67a6c8ec23c9fb40797b72a014766fdc159bb809bd74f7d8", size = 844090 }, + { url = "https://files.pythonhosted.org/packages/fd/92/d89743b089005cae4cb81cc2fe177e180b7452e60f29de53af34349640f8/regex-2025.9.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:7383efdf6e8e8c61d85e00cfb2e2e18da1a621b8bfb4b0f1c2747db57b942b8f", size = 834775 }, + { url = "https://files.pythonhosted.org/packages/01/8f/86a3e0aaa89295d2a3445bb238e56369963ef6b02a5b4aa3362f4e687413/regex-2025.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1ec2bd3bdf0f73f7e9f48dca550ba7d973692d5e5e9a90ac42cc5f16c4432d8b", size = 778521 }, + { url = "https://files.pythonhosted.org/packages/3e/df/72072acb370ee8577c255717f8a58264f1d0de40aa3c9e6ebd5271cac633/regex-2025.9.1-cp310-cp310-win32.whl", hash = "sha256:9627e887116c4e9c0986d5c3b4f52bcfe3df09850b704f62ec3cbf177a0ae374", size = 264105 }, + { url = "https://files.pythonhosted.org/packages/97/73/fb82faaf0375aeaa1bb675008246c79b6779fa5688585a35327610ea0e2e/regex-2025.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:94533e32dc0065eca43912ee6649c90ea0681d59f56d43c45b5bcda9a740b3dd", size = 276131 }, + { url = "https://files.pythonhosted.org/packages/d3/3a/77d7718a2493e54725494f44da1a1e55704743dc4b8fabe5b0596f7b8014/regex-2025.9.1-cp310-cp310-win_arm64.whl", hash = "sha256:a874a61bb580d48642ffd338570ee24ab13fa023779190513fcacad104a6e251", size = 268462 }, + { url = "https://files.pythonhosted.org/packages/06/4d/f741543c0c59f96c6625bc6c11fea1da2e378b7d293ffff6f318edc0ce14/regex-2025.9.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e5bcf112b09bfd3646e4db6bf2e598534a17d502b0c01ea6550ba4eca780c5e6", size = 484811 }, + { url = "https://files.pythonhosted.org/packages/c2/bd/27e73e92635b6fbd51afc26a414a3133243c662949cd1cda677fe7bb09bd/regex-2025.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:67a0295a3c31d675a9ee0238d20238ff10a9a2fdb7a1323c798fc7029578b15c", size = 288977 }, + { url = "https://files.pythonhosted.org/packages/eb/7d/7dc0c6efc8bc93cd6e9b947581f5fde8a5dbaa0af7c4ec818c5729fdc807/regex-2025.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea8267fbadc7d4bd7c1301a50e85c2ff0de293ff9452a1a9f8d82c6cafe38179", size = 286606 }, + { url = "https://files.pythonhosted.org/packages/d1/01/9b5c6dd394f97c8f2c12f6e8f96879c9ac27292a718903faf2e27a0c09f6/regex-2025.9.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aeff21de7214d15e928fb5ce757f9495214367ba62875100d4c18d293750cc1", size = 792436 }, + { url = "https://files.pythonhosted.org/packages/fc/24/b7430cfc6ee34bbb3db6ff933beb5e7692e5cc81e8f6f4da63d353566fb0/regex-2025.9.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d89f1bbbbbc0885e1c230f7770d5e98f4f00b0ee85688c871d10df8b184a6323", size = 858705 }, + { url = "https://files.pythonhosted.org/packages/d6/98/155f914b4ea6ae012663188545c4f5216c11926d09b817127639d618b003/regex-2025.9.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca3affe8ddea498ba9d294ab05f5f2d3b5ad5d515bc0d4a9016dd592a03afe52", size = 905881 }, + { url = "https://files.pythonhosted.org/packages/8a/a7/a470e7bc8259c40429afb6d6a517b40c03f2f3e455c44a01abc483a1c512/regex-2025.9.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91892a7a9f0a980e4c2c85dd19bc14de2b219a3a8867c4b5664b9f972dcc0c78", size = 798968 }, + { url = "https://files.pythonhosted.org/packages/1d/fa/33f6fec4d41449fea5f62fdf5e46d668a1c046730a7f4ed9f478331a8e3a/regex-2025.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e1cb40406f4ae862710615f9f636c1e030fd6e6abe0e0f65f6a695a2721440c6", size = 781884 }, + { url = "https://files.pythonhosted.org/packages/42/de/2b45f36ab20da14eedddf5009d370625bc5942d9953fa7e5037a32d66843/regex-2025.9.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:94f6cff6f7e2149c7e6499a6ecd4695379eeda8ccbccb9726e8149f2fe382e92", size = 852935 }, + { url = "https://files.pythonhosted.org/packages/1e/f9/878f4fc92c87e125e27aed0f8ee0d1eced9b541f404b048f66f79914475a/regex-2025.9.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6c0226fb322b82709e78c49cc33484206647f8a39954d7e9de1567f5399becd0", size = 844340 }, + { url = "https://files.pythonhosted.org/packages/90/c2/5b6f2bce6ece5f8427c718c085eca0de4bbb4db59f54db77aa6557aef3e9/regex-2025.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a12f59c7c380b4fcf7516e9cbb126f95b7a9518902bcf4a852423ff1dcd03e6a", size = 787238 }, + { url = "https://files.pythonhosted.org/packages/47/66/1ef1081c831c5b611f6f55f6302166cfa1bc9574017410ba5595353f846a/regex-2025.9.1-cp311-cp311-win32.whl", hash = "sha256:49865e78d147a7a4f143064488da5d549be6bfc3f2579e5044cac61f5c92edd4", size = 264118 }, + { url = "https://files.pythonhosted.org/packages/ad/e0/8adc550d7169df1d6b9be8ff6019cda5291054a0107760c2f30788b6195f/regex-2025.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:d34b901f6f2f02ef60f4ad3855d3a02378c65b094efc4b80388a3aeb700a5de7", size = 276151 }, + { url = "https://files.pythonhosted.org/packages/cb/bd/46fef29341396d955066e55384fb93b0be7d64693842bf4a9a398db6e555/regex-2025.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:47d7c2dab7e0b95b95fd580087b6ae196039d62306a592fa4e162e49004b6299", size = 268460 }, + { url = "https://files.pythonhosted.org/packages/39/ef/a0372febc5a1d44c1be75f35d7e5aff40c659ecde864d7fa10e138f75e74/regex-2025.9.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:84a25164bd8dcfa9f11c53f561ae9766e506e580b70279d05a7946510bdd6f6a", size = 486317 }, + { url = "https://files.pythonhosted.org/packages/b5/25/d64543fb7eb41a1024786d518cc57faf1ce64aa6e9ddba097675a0c2f1d2/regex-2025.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:645e88a73861c64c1af558dd12294fb4e67b5c1eae0096a60d7d8a2143a611c7", size = 289698 }, + { url = "https://files.pythonhosted.org/packages/d8/dc/fbf31fc60be317bd9f6f87daa40a8a9669b3b392aa8fe4313df0a39d0722/regex-2025.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10a450cba5cd5409526ee1d4449f42aad38dd83ac6948cbd6d7f71ca7018f7db", size = 287242 }, + { url = "https://files.pythonhosted.org/packages/0f/74/f933a607a538f785da5021acf5323961b4620972e2c2f1f39b6af4b71db7/regex-2025.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9dc5991592933a4192c166eeb67b29d9234f9c86344481173d1bc52f73a7104", size = 797441 }, + { url = "https://files.pythonhosted.org/packages/89/d0/71fc49b4f20e31e97f199348b8c4d6e613e7b6a54a90eb1b090c2b8496d7/regex-2025.9.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a32291add816961aab472f4fad344c92871a2ee33c6c219b6598e98c1f0108f2", size = 862654 }, + { url = "https://files.pythonhosted.org/packages/59/05/984edce1411a5685ba9abbe10d42cdd9450aab4a022271f9585539788150/regex-2025.9.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:588c161a68a383478e27442a678e3b197b13c5ba51dbba40c1ccb8c4c7bee9e9", size = 910862 }, + { url = "https://files.pythonhosted.org/packages/b2/02/5c891bb5fe0691cc1bad336e3a94b9097fbcf9707ec8ddc1dce9f0397289/regex-2025.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47829ffaf652f30d579534da9085fe30c171fa2a6744a93d52ef7195dc38218b", size = 801991 }, + { url = "https://files.pythonhosted.org/packages/f1/ae/fd10d6ad179910f7a1b3e0a7fde1ef8bb65e738e8ac4fd6ecff3f52252e4/regex-2025.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e978e5a35b293ea43f140c92a3269b6ab13fe0a2bf8a881f7ac740f5a6ade85", size = 786651 }, + { url = "https://files.pythonhosted.org/packages/30/cf/9d686b07bbc5bf94c879cc168db92542d6bc9fb67088d03479fef09ba9d3/regex-2025.9.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4cf09903e72411f4bf3ac1eddd624ecfd423f14b2e4bf1c8b547b72f248b7bf7", size = 856556 }, + { url = "https://files.pythonhosted.org/packages/91/9d/302f8a29bb8a49528abbab2d357a793e2a59b645c54deae0050f8474785b/regex-2025.9.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d016b0f77be63e49613c9e26aaf4a242f196cd3d7a4f15898f5f0ab55c9b24d2", size = 849001 }, + { url = "https://files.pythonhosted.org/packages/93/fa/b4c6dbdedc85ef4caec54c817cd5f4418dbfa2453214119f2538082bf666/regex-2025.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:656563e620de6908cd1c9d4f7b9e0777e3341ca7db9d4383bcaa44709c90281e", size = 788138 }, + { url = "https://files.pythonhosted.org/packages/4a/1b/91ee17a3cbf87f81e8c110399279d0e57f33405468f6e70809100f2ff7d8/regex-2025.9.1-cp312-cp312-win32.whl", hash = "sha256:df33f4ef07b68f7ab637b1dbd70accbf42ef0021c201660656601e8a9835de45", size = 264524 }, + { url = "https://files.pythonhosted.org/packages/92/28/6ba31cce05b0f1ec6b787921903f83bd0acf8efde55219435572af83c350/regex-2025.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:5aba22dfbc60cda7c0853516104724dc904caa2db55f2c3e6e984eb858d3edf3", size = 275489 }, + { url = "https://files.pythonhosted.org/packages/bd/ed/ea49f324db00196e9ef7fe00dd13c6164d5173dd0f1bbe495e61bb1fb09d/regex-2025.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:ec1efb4c25e1849c2685fa95da44bfde1b28c62d356f9c8d861d4dad89ed56e9", size = 268589 }, + { url = "https://files.pythonhosted.org/packages/98/25/b2959ce90c6138c5142fe5264ee1f9b71a0c502ca4c7959302a749407c79/regex-2025.9.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bc6834727d1b98d710a63e6c823edf6ffbf5792eba35d3fa119531349d4142ef", size = 485932 }, + { url = "https://files.pythonhosted.org/packages/49/2e/6507a2a85f3f2be6643438b7bd976e67ad73223692d6988eb1ff444106d3/regex-2025.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c3dc05b6d579875719bccc5f3037b4dc80433d64e94681a0061845bd8863c025", size = 289568 }, + { url = "https://files.pythonhosted.org/packages/c7/d8/de4a4b57215d99868f1640e062a7907e185ec7476b4b689e2345487c1ff4/regex-2025.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22213527df4c985ec4a729b055a8306272d41d2f45908d7bacb79be0fa7a75ad", size = 286984 }, + { url = "https://files.pythonhosted.org/packages/03/15/e8cb403403a57ed316e80661db0e54d7aa2efcd85cb6156f33cc18746922/regex-2025.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e3f6e3c5a5a1adc3f7ea1b5aec89abfc2f4fbfba55dafb4343cd1d084f715b2", size = 797514 }, + { url = "https://files.pythonhosted.org/packages/e4/26/2446f2b9585fed61faaa7e2bbce3aca7dd8df6554c32addee4c4caecf24a/regex-2025.9.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcb89c02a0d6c2bec9b0bb2d8c78782699afe8434493bfa6b4021cc51503f249", size = 862586 }, + { url = "https://files.pythonhosted.org/packages/fd/b8/82ffbe9c0992c31bbe6ae1c4b4e21269a5df2559102b90543c9b56724c3c/regex-2025.9.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0e2f95413eb0c651cd1516a670036315b91b71767af83bc8525350d4375ccba", size = 910815 }, + { url = "https://files.pythonhosted.org/packages/2f/d8/7303ea38911759c1ee30cc5bc623ee85d3196b733c51fd6703c34290a8d9/regex-2025.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a41dc039e1c97d3c2ed3e26523f748e58c4de3ea7a31f95e1cf9ff973fff5a", size = 802042 }, + { url = "https://files.pythonhosted.org/packages/fc/0e/6ad51a55ed4b5af512bb3299a05d33309bda1c1d1e1808fa869a0bed31bc/regex-2025.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f0b4258b161094f66857a26ee938d3fe7b8a5063861e44571215c44fbf0e5df", size = 786764 }, + { url = "https://files.pythonhosted.org/packages/8d/d5/394e3ffae6baa5a9217bbd14d96e0e5da47bb069d0dbb8278e2681a2b938/regex-2025.9.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bf70e18ac390e6977ea7e56f921768002cb0fa359c4199606c7219854ae332e0", size = 856557 }, + { url = "https://files.pythonhosted.org/packages/cd/80/b288d3910c41194ad081b9fb4b371b76b0bbfdce93e7709fc98df27b37dc/regex-2025.9.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b84036511e1d2bb0a4ff1aec26951caa2dea8772b223c9e8a19ed8885b32dbac", size = 849108 }, + { url = "https://files.pythonhosted.org/packages/d1/cd/5ec76bf626d0d5abdc277b7a1734696f5f3d14fbb4a3e2540665bc305d85/regex-2025.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c2e05dcdfe224047f2a59e70408274c325d019aad96227ab959403ba7d58d2d7", size = 788201 }, + { url = "https://files.pythonhosted.org/packages/b5/36/674672f3fdead107565a2499f3007788b878188acec6d42bc141c5366c2c/regex-2025.9.1-cp313-cp313-win32.whl", hash = "sha256:3b9a62107a7441b81ca98261808fed30ae36ba06c8b7ee435308806bd53c1ed8", size = 264508 }, + { url = "https://files.pythonhosted.org/packages/83/ad/931134539515eb64ce36c24457a98b83c1b2e2d45adf3254b94df3735a76/regex-2025.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:b38afecc10c177eb34cfae68d669d5161880849ba70c05cbfbe409f08cc939d7", size = 275469 }, + { url = "https://files.pythonhosted.org/packages/24/8c/96d34e61c0e4e9248836bf86d69cb224fd222f270fa9045b24e218b65604/regex-2025.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:ec329890ad5e7ed9fc292858554d28d58d56bf62cf964faf0aa57964b21155a0", size = 268586 }, + { url = "https://files.pythonhosted.org/packages/21/b1/453cbea5323b049181ec6344a803777914074b9726c9c5dc76749966d12d/regex-2025.9.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:72fb7a016467d364546f22b5ae86c45680a4e0de6b2a6f67441d22172ff641f1", size = 486111 }, + { url = "https://files.pythonhosted.org/packages/f6/0e/92577f197bd2f7652c5e2857f399936c1876978474ecc5b068c6d8a79c86/regex-2025.9.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c9527fa74eba53f98ad86be2ba003b3ebe97e94b6eb2b916b31b5f055622ef03", size = 289520 }, + { url = "https://files.pythonhosted.org/packages/af/c6/b472398116cca7ea5a6c4d5ccd0fc543f7fd2492cb0c48d2852a11972f73/regex-2025.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c905d925d194c83a63f92422af7544ec188301451b292c8b487f0543726107ca", size = 287215 }, + { url = "https://files.pythonhosted.org/packages/cf/11/f12ecb0cf9ca792a32bb92f758589a84149017467a544f2f6bfb45c0356d/regex-2025.9.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74df7c74a63adcad314426b1f4ea6054a5ab25d05b0244f0c07ff9ce640fa597", size = 797855 }, + { url = "https://files.pythonhosted.org/packages/46/88/bbb848f719a540fb5997e71310f16f0b33a92c5d4b4d72d4311487fff2a3/regex-2025.9.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4f6e935e98ea48c7a2e8be44494de337b57a204470e7f9c9c42f912c414cd6f5", size = 863363 }, + { url = "https://files.pythonhosted.org/packages/54/a9/2321eb3e2838f575a78d48e03c1e83ea61bd08b74b7ebbdeca8abc50fc25/regex-2025.9.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4a62d033cd9ebefc7c5e466731a508dfabee827d80b13f455de68a50d3c2543d", size = 910202 }, + { url = "https://files.pythonhosted.org/packages/33/07/d1d70835d7d11b7e126181f316f7213c4572ecf5c5c97bdbb969fb1f38a2/regex-2025.9.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef971ebf2b93bdc88d8337238be4dfb851cc97ed6808eb04870ef67589415171", size = 801808 }, + { url = "https://files.pythonhosted.org/packages/13/d1/29e4d1bed514ef2bf3a4ead3cb8bb88ca8af94130239a4e68aa765c35b1c/regex-2025.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d936a1db208bdca0eca1f2bb2c1ba1d8370b226785c1e6db76e32a228ffd0ad5", size = 786824 }, + { url = "https://files.pythonhosted.org/packages/33/27/20d8ccb1bee460faaa851e6e7cc4cfe852a42b70caa1dca22721ba19f02f/regex-2025.9.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:7e786d9e4469698fc63815b8de08a89165a0aa851720eb99f5e0ea9d51dd2b6a", size = 857406 }, + { url = "https://files.pythonhosted.org/packages/74/fe/60c6132262dc36430d51e0c46c49927d113d3a38c1aba6a26c7744c84cf3/regex-2025.9.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6b81d7dbc5466ad2c57ce3a0ddb717858fe1a29535c8866f8514d785fdb9fc5b", size = 848593 }, + { url = "https://files.pythonhosted.org/packages/cc/ae/2d4ff915622fabbef1af28387bf71e7f2f4944a348b8460d061e85e29bf0/regex-2025.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cd4890e184a6feb0ef195338a6ce68906a8903a0f2eb7e0ab727dbc0a3156273", size = 787951 }, + { url = "https://files.pythonhosted.org/packages/85/37/dc127703a9e715a284cc2f7dbdd8a9776fd813c85c126eddbcbdd1ca5fec/regex-2025.9.1-cp314-cp314-win32.whl", hash = "sha256:34679a86230e46164c9e0396b56cab13c0505972343880b9e705083cc5b8ec86", size = 269833 }, + { url = "https://files.pythonhosted.org/packages/83/bf/4bed4d3d0570e16771defd5f8f15f7ea2311edcbe91077436d6908956c4a/regex-2025.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:a1196e530a6bfa5f4bde029ac5b0295a6ecfaaffbfffede4bbaf4061d9455b70", size = 278742 }, + { url = "https://files.pythonhosted.org/packages/cf/3e/7d7ac6fd085023312421e0d69dfabdfb28e116e513fadbe9afe710c01893/regex-2025.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:f46d525934871ea772930e997d577d48c6983e50f206ff7b66d4ac5f8941e993", size = 271860 }, ] [[package]] name = "requests" -version = "2.32.4" +version = "2.32.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -1494,9 +1509,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258 } +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/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847 }, + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738 }, ] [[package]] @@ -1554,7 +1569,7 @@ wheels = [ [[package]] name = "titanoboa" version = "0.2.7" -source = { git = "https://github.com/AlbertoCentonze/titanoboa?rev=vvm-eval#5f9a6515fe1279d29e29ec21910d49754a314a7a" } +source = { git = "https://github.com/AlbertoCentonze/titanoboa?rev=vvm-eval#6290da1ad3e5161490425065eed0e2e636826828" } dependencies = [ { name = "eth-abi" }, { name = "eth-account" }, @@ -1638,11 +1653,11 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.14.1" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673 } +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/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906 }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, ] [[package]] From 8707c8d2c5d322661d3664bc79f0c4e52ef7c472 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 4 Sep 2025 19:09:26 +0200 Subject: [PATCH 201/413] test: fix test missing return type --- tests/stableborrow/stabilize/unitary/test_pk_regulator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/stableborrow/stabilize/unitary/test_pk_regulator.py b/tests/stableborrow/stabilize/unitary/test_pk_regulator.py index 1cf549fd..4455260c 100644 --- a/tests/stableborrow/stabilize/unitary/test_pk_regulator.py +++ b/tests/stableborrow/stabilize/unitary/test_pk_regulator.py @@ -215,7 +215,7 @@ 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")) ] From 10266b8e2206e7f473bd7bbaefa02fc3cef12aa5 Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 5 Sep 2025 13:30:23 +0200 Subject: [PATCH 202/413] test: fix incorrect constructor --- tests/stableborrow/stabilize/unitary/test_pk_offboarding.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/stableborrow/stabilize/unitary/test_pk_offboarding.py b/tests/stableborrow/stabilize/unitary/test_pk_offboarding.py index 0a9beacd..7e82a1e2 100644 --- a/tests/stableborrow/stabilize/unitary/test_pk_offboarding.py +++ b/tests/stableborrow/stabilize/unitary/test_pk_offboarding.py @@ -13,10 +13,11 @@ @pytest.fixture(scope="module") -def offboarding(stablecoin, receiver, admin, peg_keepers): +def offboarding(receiver, admin, peg_keepers): + # TODO should come from deployers hr = boa.load( 'contracts/stabilizer/PegKeeperOffboarding.vy', - stablecoin, ZERO_ADDRESS, receiver, admin, admin + receiver, admin, admin ) with boa.env.prank(admin): for peg_keeper in peg_keepers: @@ -109,4 +110,3 @@ def test_admin(reg, admin, alice, agg, receiver): reg.set_admin(alice) assert reg.admin() == alice - From b638de37fa51a1d4daee1176563bb4f5d45c94e5 Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 5 Sep 2025 14:24:53 +0200 Subject: [PATCH 203/413] test: fix broken tests because of vvm diff --- .../stabilize/unitary/test_agg_monetary_policy_3.py | 8 ++++---- .../stabilize/unitary/test_pk_admin_functions.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) 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 59c71a15..06a5924c 100644 --- a/tests/stableborrow/stabilize/unitary/test_agg_monetary_policy_3.py +++ b/tests/stableborrow/stabilize/unitary/test_agg_monetary_policy_3.py @@ -83,9 +83,9 @@ 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 @@ -110,7 +110,7 @@ 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): @@ -120,6 +120,6 @@ def test_add_controllers(mp, mock_factory, admin): 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 5d187b6e..a4e873d5 100644 --- a/tests/stableborrow/stabilize/unitary/test_pk_admin_functions.py +++ b/tests/stableborrow/stabilize/unitary/test_pk_admin_functions.py @@ -72,7 +72,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.evm.patch.timestamp + ADMIN_ACTIONS_DEADLINE == pk.new_admin_deadline() # apply_new_admin boa.env.time_travel(ADMIN_ACTIONS_DEADLINE - 60) From a8e9647249e9ccacd42eda9a3789e7364adeadcb Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 5 Sep 2025 14:52:16 +0200 Subject: [PATCH 204/413] test: replace dev reason with string in old contract --- contracts/Stableswap.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/Stableswap.vy b/contracts/Stableswap.vy index 85ad292f..8b2382f8 100644 --- a/contracts/Stableswap.vy +++ b/contracts/Stableswap.vy @@ -913,7 +913,7 @@ def remove_liquidity_imbalance( total_supply: uint256 = self.totalSupply burn_amount: uint256 = ((D0 - D2) * total_supply // D0) + 1 - assert burn_amount > 1 # dev: zero tokens burned + assert burn_amount > 1, "zero tokens burned" assert burn_amount <= _max_burn_amount, "Slippage screwed you" total_supply -= burn_amount From b23ef65e69e0aa6e66bc10b34b0fdaf427eb8f21 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 8 Sep 2025 14:39:55 +0200 Subject: [PATCH 205/413] test: fix legacy timestamp usage --- tests/lm_callback/test_as_gauge.py | 8 ++++---- tests/lm_callback/test_lm_callback.py | 4 ++-- tests/lm_callback/test_st_as_gauge.py | 4 ++-- tests/lm_callback/test_st_lm_callback.py | 4 ++-- .../stabilize/unitary/test_pk_admin_functions.py | 2 +- tests/stableborrow/stabilize/unitary/test_pk_delay.py | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/lm_callback/test_as_gauge.py b/tests/lm_callback/test_as_gauge.py index b75bf40f..be17fe96 100644 --- a/tests/lm_callback/test_as_gauge.py +++ b/tests/lm_callback/test_as_gauge.py @@ -13,7 +13,7 @@ def test_gauge_integral_one_user(accounts, admin, collateral_token, crv, lm_call 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 @@ -23,7 +23,7 @@ def test_gauge_integral_one_user(accounts, admin, collateral_token, crv, lm_call def update_integral(): nonlocal checkpoint, checkpoint_rate, integral, checkpoint_balance, checkpoint_supply - t1 = boa.env.evm.patch.timestamp + t1 = boa.env.timestamp rate1 = crv.rate() t_epoch = crv.start_epoch_time_write(sender=admin) if checkpoint >= t_epoch: @@ -94,7 +94,7 @@ def test_gauge_integral(accounts, admin, collateral_token, crv, lm_callback, mar 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 @@ -108,7 +108,7 @@ def test_gauge_integral(accounts, admin, collateral_token, crv, lm_callback, mar def update_integral(): nonlocal checkpoint, checkpoint_rate, integral, checkpoint_balance, checkpoint_supply - t1 = boa.env.evm.patch.timestamp + t1 = boa.env.timestamp rate1 = crv.rate() t_epoch = crv.start_epoch_time() if checkpoint >= t_epoch: diff --git a/tests/lm_callback/test_lm_callback.py b/tests/lm_callback/test_lm_callback.py index ba58b4fc..fb04e85c 100644 --- a/tests/lm_callback/test_lm_callback.py +++ b/tests/lm_callback/test_lm_callback.py @@ -79,7 +79,7 @@ def test_gauge_integral_with_exchanges( 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 @@ -94,7 +94,7 @@ def test_gauge_integral_with_exchanges( def update_integral(): nonlocal checkpoint, checkpoint_rate, integral, checkpoint_balance, checkpoint_supply - t1 = boa.env.evm.patch.timestamp + t1 = boa.env.timestamp t_epoch = crv.start_epoch_time_write(sender=admin) rate1 = crv.rate() if checkpoint >= t_epoch: diff --git a/tests/lm_callback/test_st_as_gauge.py b/tests/lm_callback/test_st_as_gauge.py index eb7cb344..09fd3288 100644 --- a/tests/lm_callback/test_st_as_gauge.py +++ b/tests/lm_callback/test_st_as_gauge.py @@ -17,14 +17,14 @@ def __init__(self): self.checkpoint_total_collateral = 0 self.checkpoint_rate = self.crv.rate() self.integrals = {addr: { - "checkpoint": boa.env.evm.patch.timestamp, + "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]: diff --git a/tests/lm_callback/test_st_lm_callback.py b/tests/lm_callback/test_st_lm_callback.py index 40342ed2..772e65d5 100644 --- a/tests/lm_callback/test_st_lm_callback.py +++ b/tests/lm_callback/test_st_lm_callback.py @@ -21,14 +21,14 @@ def __init__(self): self.checkpoint_total_collateral = 0 self.checkpoint_rate = self.crv.rate() self.integrals = {addr: { - "checkpoint": boa.env.evm.patch.timestamp, + "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]: diff --git a/tests/stableborrow/stabilize/unitary/test_pk_admin_functions.py b/tests/stableborrow/stabilize/unitary/test_pk_admin_functions.py index a4e873d5..e7cd0f85 100644 --- a/tests/stableborrow/stabilize/unitary/test_pk_admin_functions.py +++ b/tests/stableborrow/stabilize/unitary/test_pk_admin_functions.py @@ -72,7 +72,7 @@ def test_new_admin(peg_keepers, admin, alice, bob): assert pk.admin() == admin assert pk.future_admin() == alice - assert boa.env.evm.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..831e4486 100644 --- a/tests/stableborrow/stabilize/unitary/test_pk_delay.py +++ b/tests/stableborrow/stabilize/unitary/test_pk_delay.py @@ -36,7 +36,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 From dd47602bd103b41e37a689b952c765989bce77f1 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 8 Sep 2025 15:31:46 +0200 Subject: [PATCH 206/413] test: fix broken abi usage --- tests/amm/test_price_oracles.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/amm/test_price_oracles.py b/tests/amm/test_price_oracles.py index 74dd0190..6b62c50c 100644 --- a/tests/amm/test_price_oracles.py +++ b/tests/amm/test_price_oracles.py @@ -1,6 +1,5 @@ import boa import pytest -import eth_utils from ..conftest import PRICE from tests.utils.deployers import EMA_PRICE_ORACLE_DEPLOYER @@ -8,13 +7,9 @@ @pytest.fixture(scope="module") def ema_price_oracle(price_oracle, admin): with boa.env.prank(admin): - fn_abi = price_oracle.price._abi - fn_name = fn_abi["name"] - arg_types = ",".join(i["type"] for i in fn_abi["inputs"]) - signature = f"{fn_name}({arg_types})" - signature = eth_utils.keccak(text=signature)[:4] - signature = b'\x00' * (32 - len(signature)) + signature - return EMA_PRICE_ORACLE_DEPLOYER.deploy(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): From 418bdaa6adf418c867b6c175805068ded8da881e Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 8 Sep 2025 16:00:23 +0200 Subject: [PATCH 207/413] refactor: use kwargs in liquidate call --- contracts/interfaces/IMintController.vyi | 2 +- contracts/zaps/PartialRepayZap.vy | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/interfaces/IMintController.vyi b/contracts/interfaces/IMintController.vyi index 843d2745..c1bd3522 100644 --- a/contracts/interfaces/IMintController.vyi +++ b/contracts/interfaces/IMintController.vyi @@ -120,7 +120,7 @@ def max_borrowable(collateral: uint256, N: uint256, current_debt: uint256, user: @external -def liquidate(user: address, min_x: uint256, _frac: uint256, callbacker: address, calldata: Bytes[10000]): +def liquidate(user: address, min_x: uint256, _frac: uint256, callbacker: address = empty(address), calldata: Bytes[10000] = b''): ... diff --git a/contracts/zaps/PartialRepayZap.vy b/contracts/zaps/PartialRepayZap.vy index 3765c053..1f783a8f 100644 --- a/contracts/zaps/PartialRepayZap.vy +++ b/contracts/zaps/PartialRepayZap.vy @@ -106,7 +106,7 @@ def liquidate_partial(_controller: address, _user: address, _min_x: uint256): tkn.transferFrom(BORROWED, msg.sender, self, borrowed_from_sender) - extcall CONTROLLER.liquidate(_user, _min_x, FRAC, empty(address), b"") + extcall CONTROLLER.liquidate(_user, _min_x, FRAC) collateral_received: uint256 = staticcall COLLATERAL.balanceOf(self) tkn.transfer(COLLATERAL, msg.sender, collateral_received) From 286daf3f7b92e317e84cddef602a2c14934c8688 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 8 Sep 2025 16:02:11 +0200 Subject: [PATCH 208/413] test: disable not effective tests --- tests/lending/test_oracle_attack.py | 45 +++++++++++++++-------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/tests/lending/test_oracle_attack.py b/tests/lending/test_oracle_attack.py index bca1fdb7..7f75cb15 100644 --- a/tests/lending/test_oracle_attack.py +++ b/tests/lending/test_oracle_attack.py @@ -192,25 +192,26 @@ def template_vuln_test(vault, controller, amm, admin, borrowed_token, price_orac 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 +# Commenting out as it doesn't test much in its current state. +# @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 From b96300b24ce5cbed23b695c4c032a3eed83b03d0 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 8 Sep 2025 16:05:33 +0200 Subject: [PATCH 209/413] test: fix collateral reference --- tests/lending/test_health_in_trades.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/lending/test_health_in_trades.py b/tests/lending/test_health_in_trades.py index 93bc1c85..eb2ddee8 100644 --- a/tests/lending/test_health_in_trades.py +++ b/tests/lending/test_health_in_trades.py @@ -39,7 +39,7 @@ def initializer(self, collateral_amount, n): n)) user = self.accounts[0] with boa.env.prank(user): - boa.deal(collateral, 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 +59,7 @@ def trade_to_price(self, p): return self.amm.exchange(0, 1, amount, 0) else: - boa.deal(collateral, user, amount) + boa.deal(self.collateral, user, amount) self.amm.exchange(1, 0, amount, 0) @rule(oracle_step=oracle_step) @@ -79,7 +79,7 @@ def random_trade(self, amount_fraction, is_pump): self.amm.exchange(0, 1, amount, 0) else: amount = int(self.collateral_amount * amount_fraction) - boa.deal(collateral, user, amount) + boa.deal(self.collateral, user, amount) self.amm.exchange(1, 0, amount, 0) @rule(t=t) From 5f28d730e0357a2786aa26b4e9044ecdc8dd475b Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 8 Sep 2025 17:25:23 +0200 Subject: [PATCH 210/413] test: fix fixtures names --- tests/lending/test_st_interest_conservation.py | 9 +++++---- tests/lending/test_vault.py | 14 +++++++------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/tests/lending/test_st_interest_conservation.py b/tests/lending/test_st_interest_conservation.py index 7cdbc6d4..cbe5fdd4 100644 --- a/tests/lending/test_st_interest_conservation.py +++ b/tests/lending/test_st_interest_conservation.py @@ -21,8 +21,9 @@ class StatefulLendBorrow(RuleBasedStateMachine): def __init__(self): super().__init__() self.collateral = self.collateral_token - self.amm = self.market_amm - self.controller = self.market_controller + # 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: @@ -225,14 +226,14 @@ def debt_payable(self): 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, monetary_policy, collateral_token, borrowed_token, accounts, admin): +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, monetary_policy, 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_vault.py b/tests/lending/test_vault.py index e2a74a0d..033ff63e 100644 --- a/tests/lending/test_vault.py +++ b/tests/lending/test_vault.py @@ -9,16 +9,16 @@ DEAD_SHARES = 1000 -def test_vault_creation(vault, market_controller, market_amm, monetary_policy, factory, price_oracle, +def test_vault_creation(vault, controller, amm, monetary_policy, 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() == monetary_policy.address + 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 @@ -391,7 +391,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) From 2ac015393afa289a740b23cd337686b23b72ca1b Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 8 Sep 2025 17:25:30 +0200 Subject: [PATCH 211/413] chore: add TODO --- contracts/lending/LendingFactory.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/lending/LendingFactory.vy b/contracts/lending/LendingFactory.vy index f71b1e78..4b187735 100644 --- a/contracts/lending/LendingFactory.vy +++ b/contracts/lending/LendingFactory.vy @@ -327,7 +327,7 @@ def set_implementations( vault: address, pool_price_oracle: address, monetary_policy: address, - view: address, + view: address, # TODO improve naming ): """ @notice Set new implementations (blueprints) for controller, amm, vault, pool price oracle and monetary policy. From d719173d053907c712f36d6b4620bd4e843bbb36 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 8 Sep 2025 17:25:50 +0200 Subject: [PATCH 212/413] revert: test is actually important --- tests/lending/test_oracle_attack.py | 44 ++++++++++++++--------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/lending/test_oracle_attack.py b/tests/lending/test_oracle_attack.py index 7f75cb15..f845eaaa 100644 --- a/tests/lending/test_oracle_attack.py +++ b/tests/lending/test_oracle_attack.py @@ -193,25 +193,25 @@ def template_vuln_test(vault, controller, amm, admin, borrowed_token, price_orac # Commenting out as it doesn't test much in its current state. -# @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 +@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 From 788da995156ed62d3761b24fef5839713caecc2b Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 9 Sep 2025 15:58:55 +0200 Subject: [PATCH 213/413] chore: add TODOs --- contracts/lending/LendingFactory.vy | 1 + contracts/price_oracles/CryptoFromPoolsRate.vy | 1 + contracts/price_oracles/CryptoFromPoolsRateWAgg.vy | 1 + contracts/price_oracles/L2/CryptoFromPoolsRateArbitrum.vy | 1 + contracts/price_oracles/L2/CryptoFromPoolsRateArbitrumWAgg.vy | 1 + contracts/price_oracles/L2/CryptoFromPoolsWAgg.vy | 1 + contracts/price_oracles/lp-oracles/LPOracleStable.vy | 1 + 7 files changed, 7 insertions(+) diff --git a/contracts/lending/LendingFactory.vy b/contracts/lending/LendingFactory.vy index 4b187735..977aa877 100644 --- a/contracts/lending/LendingFactory.vy +++ b/contracts/lending/LendingFactory.vy @@ -245,6 +245,7 @@ def create_from_pool( # 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 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/lp-oracles/LPOracleStable.vy b/contracts/price_oracles/lp-oracles/LPOracleStable.vy index 62b893ca..50b291d7 100644 --- a/contracts/price_oracles/lp-oracles/LPOracleStable.vy +++ b/contracts/price_oracles/lp-oracles/LPOracleStable.vy @@ -32,6 +32,7 @@ def __init__(_pool: IStablePool, _coin0_oracle: IPriceOracle): 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, From 869f620ae58fcb35fc18bb3edf4cf1679fc337f5 Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 9 Sep 2025 16:01:51 +0200 Subject: [PATCH 214/413] test: fix attack sim test --- tests/lending/test_oracle_attack.py | 177 ++++------------------------ 1 file changed, 25 insertions(+), 152 deletions(-) diff --git a/tests/lending/test_oracle_attack.py b/tests/lending/test_oracle_attack.py index f845eaaa..39eeb6c5 100644 --- a/tests/lending/test_oracle_attack.py +++ b/tests/lending/test_oracle_attack.py @@ -12,119 +12,30 @@ from hypothesis import given, settings from hypothesis import strategies as st -from tests.utils.deployers import LENDING_FACTORY_DEPLOYER, OLD_AMM_DEPLOYER, AMM_DEPLOYER, LL_CONTROLLER_DEPLOYER, VAULT_DEPLOYER, ERC20_MOCK_DEPLOYER - - -MAX = 2**256 - 1 - - -@pytest.fixture(scope='module') -def collateral_token(admin): - decimals = 18 - with boa.env.prank(admin): - return ERC20_MOCK_DEPLOYER.deploy(decimals) - - -@pytest.fixture(scope='module') -def borrowed_token(stablecoin): - return stablecoin - - -@pytest.fixture(scope='module') -def victim(accounts): - return accounts[1] - - -@pytest.fixture(scope='module') -def hacker(accounts): - return accounts[2] - - -@pytest.fixture(scope="module") -def factory_new(amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, admin): - with boa.env.prank(admin): - return LENDING_FACTORY_DEPLOYER.deploy(amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, admin, admin) +from tests.utils.constants import MAX_UINT256 @pytest.fixture(scope="module") -def amm_old_interface(): - return OLD_AMM_DEPLOYER +def market_type(): + # Ensure we test against the lending market setup from tests/conftest.py + return "lending" @pytest.fixture(scope="module") -def factory_old(controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, amm_old_interface, admin): - # TODO is this really the old factory? I don't think so - with boa.env.prank(admin): - amm_impl = amm_old_interface.deploy_as_blueprint() - return LENDING_FACTORY_DEPLOYER.deploy(amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, admin, admin) - - -@pytest.fixture(scope='module') -def vault_new(factory_new, borrowed_token, collateral_token, price_oracle, admin): - with boa.env.prank(admin): - price_oracle.set_price(int(1e18)) - - vault = VAULT_DEPLOYER.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" - )[0] - ) - - boa.env.time_travel(120) - - return vault - - -@pytest.fixture(scope='module') -def vault_old(factory_old, borrowed_token, collateral_token, price_oracle, admin): - with boa.env.prank(admin): - price_oracle.set_price(int(1e18)) - - vault = VAULT_DEPLOYER.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" - )[0] - ) - - boa.env.time_travel(120) - - return vault - - -@pytest.fixture(scope='module') -def controller_new(vault_new, admin): - controller = LL_CONTROLLER_DEPLOYER.at(vault_new.controller()) - with boa.env.prank(admin): - controller.set_borrow_cap(2 ** 256 - 1) - - return controller - +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 amm_new(vault_new): - return AMM_DEPLOYER.at(vault_new.amm()) - -@pytest.fixture(scope='module') -def controller_old(vault_old, admin): - controller = LL_CONTROLLER_DEPLOYER.at(vault_old.controller()) - with boa.env.prank(admin): - controller.set_borrow_cap(2 ** 256 - 1) - - return controller - - -@pytest.fixture(scope='module') -def amm_old(vault_old): - return OLD_AMM_DEPLOYER.at(vault_old.amm()) - - -def template_vuln_test(vault, controller, amm, admin, borrowed_token, price_oracle, collateral_token, victim, hacker, - victim_gap, victim_bins, fee): +@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(vault, controller, amm, admin, borrowed_token, price_oracle, collateral_token, accounts, + victim_gap, victim_bins): + victim = accounts[1] + hacker = accounts[2] # victim loan victim_collateral_lent = int(10_000_000e18) @@ -133,24 +44,25 @@ def template_vuln_test(vault, controller, amm, admin, borrowed_token, price_orac 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) # 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) boa.deal(borrowed_token, admin, b_amount) - borrowed_token.approve(vault.address, MAX) + borrowed_token.approve(vault.address, MAX_UINT256) vault.deposit(b_amount) # victim creates a loan @@ -159,59 +71,20 @@ def template_vuln_test(vault, controller, amm, admin, borrowed_token, price_orac 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): boa.deal(borrowed_token, hacker, hacker_crvusd_reserves) - spent, received = amm.exchange(0, 1, hacker_crvusd_reserves, 0) - # print(f"Bought {received/1e18:.3f} for {spent/1e18:.2f}") + 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) - - # 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}") - - -# Commenting out as it doesn't test much in its current state. -@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 From f5fc7449c81aaa0d815f1877e0b73f4f0497ffac Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 9 Sep 2025 22:00:12 +0400 Subject: [PATCH 215/413] test: fix test_oracle_attack --- tests/lending/test_oracle_attack.py | 85 ++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 3 deletions(-) diff --git a/tests/lending/test_oracle_attack.py b/tests/lending/test_oracle_attack.py index 39eeb6c5..5a6bc422 100644 --- a/tests/lending/test_oracle_attack.py +++ b/tests/lending/test_oracle_attack.py @@ -32,8 +32,78 @@ def borrow_cap(): 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_gap, victim_bins): + victim = accounts[1] + hacker = 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() + + # Configure dynamic fee for this scenario + with boa.env.prank(admin): + controller.set_amm_fee(int(0.00001 * 1e18)) + + # 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) + + # add crvUSD to the vault + with boa.env.prank(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) + + # 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 + + # 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) + + # update oracle price + with boa.env.prank(admin): + new_p = int(p * (1 + price_manipulation)) + price_oracle.set_price(new_p) + + 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) + + # 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}") + + +@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] @@ -49,7 +119,7 @@ def test_vuln(vault, controller, amm, admin, borrowed_token, price_oracle, colla 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): @@ -60,7 +130,7 @@ def test_vuln(vault, controller, amm, admin, borrowed_token, price_oracle, colla # add crvUSD to the vault with boa.env.prank(admin): - b_amount = int(1_000_000_000e18) + 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) @@ -88,3 +158,12 @@ def test_vuln(vault, controller, amm, admin, borrowed_token, price_oracle, colla victim_health = controller.health(victim, True) / 1e18 with boa.reverts(): controller.liquidate(victim, 0) + + # 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}") From 0ad04da9309f31a6baf735cd8b8e353da133a353 Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 10 Sep 2025 12:08:39 +0400 Subject: [PATCH 216/413] test: fix test_flip --- tests/amm/conftest.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/amm/conftest.py b/tests/amm/conftest.py index 4ec23de7..c3258fbf 100644 --- a/tests/amm/conftest.py +++ b/tests/amm/conftest.py @@ -6,6 +6,11 @@ PRICE = 3000 +@pytest.fixture(scope="module") +def borrowed_token(): + return ERC20_MOCK_DEPLOYER.deploy(6) + + @pytest.fixture(scope="module") def get_amm(price_oracle, admin, accounts): def f(collateral_token, borrowed_token): From 6bdafa8ba4ec143df0df4553ab78a6d23167ceed Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 10 Sep 2025 14:21:16 +0200 Subject: [PATCH 217/413] test: stateful test setup --- .../stabilize/stateful/test_agg_monetary_policy.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/stableborrow/stabilize/stateful/test_agg_monetary_policy.py b/tests/stableborrow/stabilize/stateful/test_agg_monetary_policy.py index 260cc7ec..12823ab4 100644 --- a/tests/stableborrow/stabilize/stateful/test_agg_monetary_policy.py +++ b/tests/stableborrow/stabilize/stateful/test_agg_monetary_policy.py @@ -46,6 +46,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, @@ -118,6 +128,7 @@ def deposit(self, amount, split, _n): x = [int(amount * self.one_usd[n]), int(split * amount * 1e18)] with boa.env.prank(self.admin): 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) From 3f283f140f3a44f7d20285153bb01ef87daede75 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 10 Sep 2025 14:34:45 +0200 Subject: [PATCH 218/413] test: fix incorrect check --- tests/stableborrow/test_liquidate.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/stableborrow/test_liquidate.py b/tests/stableborrow/test_liquidate.py index cff936be..6afaa34b 100644 --- a/tests/stableborrow/test_liquidate.py +++ b/tests/stableborrow/test_liquidate.py @@ -172,18 +172,19 @@ def test_tokens_to_liquidate(accounts, admin, controller_for_liquidation, market 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(user, 0, frac) balance = stablecoin.balanceOf(fee_receiver) + spent = initial_balance - balance if frac < 10**18: - assert balance == pytest.approx(initial_balance - tokens_to_liquidate, rel=1e5, abs=1e5) + assert spent == pytest.approx(tokens_to_liquidate, rel=1e5, abs=1e5) else: assert balance != initial_balance - tokens_to_liquidate From db9a77daa34d219ab759861e4a6a4d1f302268d5 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 10 Sep 2025 14:55:16 +0200 Subject: [PATCH 219/413] test: don't hardcode borrow rates in tests --- tests/lending/test_monetary_policy.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/lending/test_monetary_policy.py b/tests/lending/test_monetary_policy.py index bc08bce6..93b6b4b3 100644 --- a/tests/lending/test_monetary_policy.py +++ b/tests/lending/test_monetary_policy.py @@ -3,9 +3,6 @@ from hypothesis import given from hypothesis import strategies as st -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(controller, collateral_token, borrowed_token, monetary_policy, admin, fill): @@ -23,8 +20,11 @@ def test_monetary_policy(controller, collateral_token, borrowed_token, monetary_ return else: 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_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 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) From 4b14bcb25fbb55f1ac0ec0697d32338acabf5951 Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 10 Sep 2025 21:33:30 +0400 Subject: [PATCH 220/413] test: hypothesis print_blob=True --- tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e00d4c96..d257e92d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,8 +19,8 @@ TESTING_DECIMALS = [2, 6, 8, 9, 18] -settings.register_profile("no-shrink", settings(phases=list(Phase)[:4]), deadline=timedelta(seconds=1000)) -settings.register_profile("default", deadline=timedelta(seconds=1000)) +settings.register_profile("no-shrink", settings(phases=list(Phase)[:4]), deadline=timedelta(seconds=1000), print_blob=True) +settings.register_profile("default", deadline=timedelta(seconds=1000), print_blob=True) settings.load_profile(os.getenv(u"HYPOTHESIS_PROFILE", "default")) From e4bf643503c3ea49fd4ac01a3406933a34cdd6c0 Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 10 Sep 2025 21:34:38 +0400 Subject: [PATCH 221/413] test: fix amount for test_exchange_down_up --- tests/amm/test_exchange.py | 18 +++++++++++------- tests/amm/test_exchange_dy.py | 20 ++++++++++---------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/tests/amm/test_exchange.py b/tests/amm/test_exchange.py index 5409e707..57e5437a 100644 --- a/tests/amm/test_exchange.py +++ b/tests/amm/test_exchange.py @@ -44,24 +44,28 @@ def test_dxdy_limits(amm, amounts, accounts, ns, dns, collateral_token, admin): @given( - amounts=st.lists(st.integers(min_value=10**16, max_value=10**6 * 10**18), 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), - amount=st.integers(min_value=0, max_value=10**9 * 10**6) + 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): + 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) - mint_for_testing(collateral_token, amm.address, amount) + amm.deposit_range(user, amt, n1, n2) + mint_for_testing(collateral_token, amm.address, amt) p_before = amm.get_p() diff --git a/tests/amm/test_exchange_dy.py b/tests/amm/test_exchange_dy.py index 643baccb..0050281a 100644 --- a/tests/amm/test_exchange_dy.py +++ b/tests/amm/test_exchange_dy.py @@ -7,9 +7,9 @@ @pytest.fixture(scope="module") -def decimals(): - """Override global decimals to fix borrowed_token at 18 for this module.""" - return 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") @@ -127,24 +127,24 @@ def test_dydx_compare_to_dxdy(amm, amounts, accounts, ns, dns, collateral_token, 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) + 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): 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) - mint_for_testing(collateral_token, amm.address, amount) + amm.deposit_range(user, amt, n1, n2) + mint_for_testing(collateral_token, amm.address, amt) p_before = amm.get_p() From 9776476690e6c24d8737dd4541109b826f358cda Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 11 Sep 2025 14:36:39 +0400 Subject: [PATCH 222/413] test: fix test_flip_dy --- tests/amm/test_flip_dy.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/amm/test_flip_dy.py b/tests/amm/test_flip_dy.py index ee2a786e..48175dd6 100644 --- a/tests/amm/test_flip_dy.py +++ b/tests/amm/test_flip_dy.py @@ -63,8 +63,9 @@ 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 @@ -103,8 +104,9 @@ 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: From a74ed337c332b11de9f6325a788862b5aed2500b Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 11 Sep 2025 17:40:47 +0400 Subject: [PATCH 223/413] test: fix test_no_untradable_funds --- tests/amm/test_oracle_change_noloss.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/amm/test_oracle_change_noloss.py b/tests/amm/test_oracle_change_noloss.py index c307bd82..b2bb956e 100644 --- a/tests/amm/test_oracle_change_noloss.py +++ b/tests/amm/test_oracle_change_noloss.py @@ -145,7 +145,10 @@ def test_no_untradable_funds(amm, collateral_token, borrowed_token, price_oracle 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 @@ -191,6 +194,9 @@ def test_no_untradable_funds_in(amm, collateral_token, borrowed_token, price_ora 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 From b3ad2b145c9750108598002b1fafda93e54a36aa Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 11 Sep 2025 17:43:13 +0400 Subject: [PATCH 224/413] test: make test_flip_dy more strict --- tests/amm/test_flip_dy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/amm/test_flip_dy.py b/tests/amm/test_flip_dy.py index 48175dd6..c1384eb6 100644 --- a/tests/amm/test_flip_dy.py +++ b/tests/amm/test_flip_dy.py @@ -63,7 +63,7 @@ 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)) <= 10**(18 - collateral_decimals) + 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 @@ -104,7 +104,7 @@ 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)) <= 10**(18 - borrowed_decimals) + 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 From 1245f9c02a6bd03e41a9e1d4dc4ded881798b19a Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 15 Sep 2025 15:24:53 +0400 Subject: [PATCH 225/413] test: refactor test_amount_for_price --- tests/amm/test_amount_for_price.py | 61 +++++++++++++----------------- 1 file changed, 27 insertions(+), 34 deletions(-) diff --git a/tests/amm/test_amount_for_price.py b/tests/amm/test_amount_for_price.py index 64a04919..887cd262 100644 --- a/tests/amm/test_amount_for_price.py +++ b/tests/amm/test_amount_for_price.py @@ -28,15 +28,13 @@ def test_amount_for_price(price_oracle, amm, accounts, collateral_token, borrowe amm.deposit_range(user, deposit_amount, n1, n2) mint_for_testing(collateral_token, amm.address, deposit_amount) - prices = [oracle_price] - prices.append(amm.get_p()) - 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: 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() @@ -58,44 +56,39 @@ def test_amount_for_price(price_oracle, amm, accounts, collateral_token, borrowe 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 p_max == pytest.approx(amm.p_current_up(n2), rel=1e-8) - assert p_min == pytest.approx(amm.p_current_down(n1), rel=1e-8) + if eamount > 0: + assert abs(n_final - n0) < 50 + + if p_final > p_max: + p_final = p_max + if p_final < p_min: + p_final = p_min - if abs(n_final - n0) < 50 - 1: + 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 p == pytest.approx(p_final, rel=prec) - - elif p_final >= p_max * (1 - prec): - if not p == pytest.approx(p_max, rel=prec): - assert n_final > n2 - - elif p_final <= p_min * (1 + prec): - if not p == pytest.approx(p_min, rel=prec): - assert n_final < n1 + 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): From 05e812182aa1267beca1b619d5882f4eb917e423 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 15 Sep 2025 15:14:28 +0200 Subject: [PATCH 226/413] style: bulk formatting --- .env-example | 2 +- .github/workflows/test.yaml | 2 +- .gitignore | 2 +- brownie-deploy-emas/scripts/deploy_emas.py | 38 +- contracts/Controller.vy | 4 +- contracts/constants.vy | 2 +- contracts/interfaces/IAMM.vyi | 1 - contracts/interfaces/IControllerView.vyi | 1 - contracts/interfaces/ICryptoPool.vyi | 4 +- contracts/interfaces/IFactory.vyi | 2 +- contracts/interfaces/ILMGauge.vyi | 4 +- contracts/interfaces/ILendingFactory.vyi | 2 +- contracts/interfaces/ILlamalendController.vyi | 3 +- contracts/interfaces/IMintController.vyi | 8 +- contracts/interfaces/IMonetaryPolicy.vyi | 2 +- contracts/interfaces/IPartialRepayZap.vyi | 1 - contracts/interfaces/IPool.vyi | 2 +- contracts/interfaces/IPriceOracle.vyi | 2 +- contracts/interfaces/IStablePool.vyi | 6 +- contracts/interfaces/IVault.vyi | 1 - contracts/lib/liquidation_lib.vy | 1 - contracts/zaps/LeverageZap.vy | 4 +- deployment-logs/fraxtal-lending.log | 2 +- deployment-logs/mainnet-attempt-4.log | 1 - deployment-logs/mainnet-deleverage-zap.log | 4 +- .../mainnet-leverage-zap-router.log | 2 +- deployment-logs/mainnet-wbtc-eth-mpolicy.log | 1 - .../oneway-lending-amm-controller.log | 1 - deployment-logs/oneway-lending-redeploy-3.log | 1 - deployment-logs/oneway-lending-redeploy.log | 1 - deployment-logs/optimism-lending.log | 5 +- deployment-logs/sepolia-test.log | 2 +- deployment-logs/sonic-deployment-2.log | 1 - deployment-logs/sonic-deployment.log | 1 - doc/whitepaper/curve-stablecoin.lyx | 330 ++-- docs/source/conf.py | 22 +- docs/source/stablecoin.rst | 2 +- model/avg.py | 4 +- model/rate-secondary-susde.py | 8 +- model/rate-secondary.py | 4 +- model/rate-usde.py | 10 +- pyproject.toml | 8 + scripts/ape-deploy-amm-controller-Aug17.py | 27 +- scripts/ape-deploy-amm-controller.py | 32 +- scripts/ape-deploy-controller-17jun.py | 28 +- scripts/ape-deploy-deleverage-zaps.py | 1023 ++++++---- scripts/ape-deploy-leverage-zaps.py | 745 +++---- scripts/ape-deploy-owner-proxy.py | 22 +- scripts/ape-deploy-sepolia.py | 156 +- scripts/ape-deploy.py | 197 +- scripts/ape-eth-oracle.py | 41 +- scripts/ape-frxeth-oracle.py | 43 +- scripts/ape-monetary-policy-2.py | 42 +- scripts/ape-monetary-policy-weth-wbtc.py | 53 +- scripts/ape-oracle-deploy.py | 29 +- scripts/ape-steth-oracle.py | 99 +- scripts/ape-steth-test-oracle.py | 31 +- scripts/ape-tbtc-oracle.py | 28 +- scripts/ape-test-deploy.py | 18 +- scripts/ape-wbtc-oracle.py | 48 +- scripts/arbi-agg-deployer.py | 18 +- scripts/boa-arbi-console.py | 27 +- scripts/boa-deploy-1inch-leverage-zap.py | 29 +- scripts/boa-deploy-flashloan.py | 14 +- scripts/boa-deploy-fxn-oracle.py | 28 +- scripts/boa-deploy-lending-example.py | 100 +- scripts/boa-deploy-lending.py | 163 +- scripts/boa-deploy-odos-leverage-zap.py | 29 +- scripts/boa-deploy-susde-oracle.py | 27 +- scripts/boa-market-creation-console.py | 17 +- scripts/boa-monetary-policy-3.py | 27 +- scripts/boa-new-amm-controller.py | 28 +- scripts/boa-salvation.py | 65 +- scripts/console_debug.py | 46 +- scripts/create-sfrax.py | 35 +- scripts/create-usde.py | 35 +- scripts/deploy-lending-arb-crv.py | 101 +- scripts/deploy-lending-arbitrum.py | 162 +- scripts/deploy-lending-fraxtal.py | 244 ++- scripts/deploy-lending-fxn.py | 83 +- scripts/deploy-lending-optimism.py | 269 +-- scripts/deploy-lending-sonic.py | 290 +-- scripts/deploy-secondary-mp-susde.py | 26 +- scripts/deploy-secondary-mps.py | 42 +- scripts/deploy.py | 41 +- scripts/opti-agg-deployer.py | 16 +- scripts/oracle-creation-console.py | 39 +- scripts/recreate-arbi-markets.py | 61 +- scripts/recreate-eth-and-wsteth.py | 47 +- scripts/setup-metaregistry.py | 100 +- scripts/vote_new_arbi_impl.py | 42 +- scripts/vote_susde_mp.py | 26 +- scripts/vote_susde_params.py | 38 +- scripts/vote_weth_mp.py | 18 +- scripts/vote_wsteth_mp.py | 18 +- tests/amm/conftest.py | 22 +- tests/amm/test_amount_for_price.py | 40 +- tests/amm/test_deposit_withdraw.py | 41 +- tests/amm/test_exchange.py | 37 +- tests/amm/test_exchange_dy.py | 75 +- tests/amm/test_flip.py | 14 +- tests/amm/test_flip_dy.py | 56 +- tests/amm/test_oracle_change_noloss.py | 87 +- tests/amm/test_share_pump_math.py | 10 +- tests/amm/test_st_exchange.py | 54 +- tests/amm/test_st_exchange_dy.py | 76 +- tests/amm/test_xdown_yup_invariants.py | 92 +- tests/amm/test_xdown_yup_invariants_dy.py | 64 +- tests/conftest.py | 24 +- tests/controller/conftest.py | 3 +- tests/controller/test_set_price_oracle.py | 48 +- tests/flashloan/conftest.py | 7 +- tests/flashloan/test_debt_ceiling.py | 24 +- tests/flashloan/test_flashloan.py | 2 +- tests/lending/conftest.py | 4 +- tests/lending/test_bigfuzz.py | 177 +- tests/lending/test_fuzz_max_borrowable.py | 57 +- .../lending/test_health_calculator_create.py | 6 +- .../test_health_calculator_stateful.py | 113 +- tests/lending/test_health_in_trades.py | 55 +- tests/lending/test_monetary_policy.py | 12 +- tests/lending/test_oracle_attack.py | 45 +- tests/lending/test_pool_price_oracle.py | 2 +- tests/lending/test_secondary_mpolicy.py | 43 +- tests/lending/test_shifted_trades.py | 11 +- .../lending/test_st_interest_conservation.py | 111 +- tests/lending/test_vault.py | 82 +- tests/lm_callback/conftest.py | 71 +- tests/lm_callback/test_add_new_lm_callback.py | 28 +- tests/lm_callback/test_as_gauge.py | 124 +- tests/lm_callback/test_lm_callback.py | 235 ++- tests/lm_callback/test_lm_callback_reverts.py | 23 +- tests/lm_callback/test_rewards_kill_unkill.py | 71 +- tests/lm_callback/test_st_as_gauge.py | 89 +- tests/lm_callback/test_st_lm_callback.py | 137 +- tests/price_oracles/lp-oracles/conftest.py | 19 +- .../lp-oracles/test_lp_oracle.py | 27 +- .../lp-oracles/test_lp_oracle_factory.py | 53 +- tests/price_oracles/proxy/conftest.py | 2 +- tests/price_oracles/proxy/test_proxy.py | 8 +- tests/stableborrow/conftest.py | 58 +- tests/stableborrow/stabilize/conftest.py | 224 ++- tests/stableborrow/stabilize/stateful/base.py | 10 +- .../stateful/test_agg_monetary_policy.py | 51 +- .../stabilize/stateful/test_diff.py | 24 +- .../stabilize/stateful/test_profit.py | 21 +- .../stateful/test_stable_peg_caller_profit.py | 42 +- .../stateful/test_withdraw_profit.py | 52 +- .../unitary/test_agg_monetary_policy_3.py | 25 +- .../unitary/test_pk_admin_functions.py | 12 +- .../stabilize/unitary/test_pk_delay.py | 12 +- .../stabilize/unitary/test_pk_offboarding.py | 21 +- .../stabilize/unitary/test_pk_profit.py | 30 +- .../stabilize/unitary/test_pk_provide.py | 27 +- .../stabilize/unitary/test_pk_regulator.py | 101 +- .../stabilize/unitary/test_pk_withdraw.py | 10 +- tests/stableborrow/test_approval.py | 49 +- tests/stableborrow/test_bigfuzz.py | 436 ++++- tests/stableborrow/test_calculate_n1.py | 10 +- tests/stableborrow/test_create_repay.py | 52 +- .../test_create_repay_stateful.py | 124 +- tests/stableborrow/test_extra_health.py | 49 +- tests/stableborrow/test_factory.py | 36 +- tests/stableborrow/test_leverage.py | 135 +- tests/stableborrow/test_liquidate.py | 61 +- tests/stableborrow/test_lm_callback.py | 10 +- .../test_st_interest_conservation.py | 192 +- tests/test_math.py | 10 +- tests/test_packing.py | 4 +- tests/utils/__init__.py | 4 +- tests/utils/constants.py | 2 +- tests/utils/deploy.py | 96 +- tests/utils/deployers.py | 293 ++- tests/zaps/conftest.py | 2 +- .../calculations/calculations.py | 17 +- tests_forked/price_oracles/conftest.py | 33 +- .../test_lp_oracle_compare_to_spot.py | 692 +++++-- tests_forked_ape/conftest.py | 90 +- tests_forked_ape/test_lendborrow.py | 145 +- tests_forked_ape/test_registry_integration.py | 15 +- tests_forked_ape/utils.py | 15 +- tests_leverage/test_v1/conftest.py | 19 +- tests_leverage/test_v1/test_deleverage.py | 162 +- .../test_v1/test_deleverage_light.py | 153 +- tests_leverage/test_v1/test_leverage.py | 46 +- tests_leverage/test_v1/test_leverage_light.py | 44 +- tests_leverage/test_v1/utils.py | 1722 ++++++++++------- tests_leverage/test_v2/conftest.py | 14 +- tests_leverage/test_v2/tests/test_leverage.py | 28 +- tests_leverage/test_v2/utils.py | 4 +- 190 files changed, 8826 insertions(+), 4556 deletions(-) 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/.github/workflows/test.yaml b/.github/workflows/test.yaml index 8d4cfc02..86c301ab 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -51,7 +51,7 @@ jobs: run: uv python install 3.12.6 - name: Install Requirements - run: uv sync + run: uv sync - name: Install Nightly Vyper if Venom is enabled if: ${{ matrix.venom.value }} diff --git a/.gitignore b/.gitignore index 4205667e..31fd7e45 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,4 @@ venv prof .prof .pytest_cache -AGENTS.md \ No newline at end of file +AGENTS.md 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/Controller.vy b/contracts/Controller.vy index 05e7208c..a7c7c8b8 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -571,7 +571,7 @@ def calculate_debt_n1( @internal def transferFrom(token: IERC20, _from: address, _to: address, amount: uint256): - # TODO: use contracts.lib.token_lib.transferFrom + # TODO: use contracts.lib.token_lib.transferFrom if amount > 0: assert extcall token.transferFrom( _from, _to, amount, default_return_value=True @@ -1352,7 +1352,7 @@ def user_state(user: address) -> uint256[4]: def set_amm_fee(fee: uint256): """ @notice Set the AMM fee (factory admin only) - @dev Reentrant because AMM is nonreentrant + @dev Reentrant because AMM is nonreentrant @param fee The fee which should be no higher than MAX_AMM_FEE """ self._check_admin() diff --git a/contracts/constants.vy b/contracts/constants.vy index 312945e0..7db1dc69 100644 --- a/contracts/constants.vy +++ b/contracts/constants.vy @@ -6,4 +6,4 @@ MAX_TICKS: constant(int256) = 50 DEAD_SHARES: constant(uint256) = 1000 # TODO make sure this is used everywhere WAD: constant(uint256) = 10**18 -SWAD: constant(int256) = 10**18 \ No newline at end of file +SWAD: constant(int256) = 10**18 diff --git a/contracts/interfaces/IAMM.vyi b/contracts/interfaces/IAMM.vyi index 59cf3678..70e0fccb 100644 --- a/contracts/interfaces/IAMM.vyi +++ b/contracts/interfaces/IAMM.vyi @@ -308,4 +308,3 @@ def user_shares(arg0: address) -> UserTicks: @external def liquidity_mining_callback() -> ILMGauge: ... - diff --git a/contracts/interfaces/IControllerView.vyi b/contracts/interfaces/IControllerView.vyi index 618b9b79..4ec0c83c 100644 --- a/contracts/interfaces/IControllerView.vyi +++ b/contracts/interfaces/IControllerView.vyi @@ -36,4 +36,3 @@ def max_borrowable(collateral: uint256, N: uint256, current_debt: uint256, user: @external def min_collateral(debt: uint256, N: uint256, user: address) -> uint256: ... - diff --git a/contracts/interfaces/ICryptoPool.vyi b/contracts/interfaces/ICryptoPool.vyi index 93b3d75e..cc924a52 100644 --- a/contracts/interfaces/ICryptoPool.vyi +++ b/contracts/interfaces/ICryptoPool.vyi @@ -1,3 +1,3 @@ @view -def lp_price() -> uint256: - ... \ No newline at end of file +def lp_price() -> uint256: + ... diff --git a/contracts/interfaces/IFactory.vyi b/contracts/interfaces/IFactory.vyi index a71bd497..9815141f 100644 --- a/contracts/interfaces/IFactory.vyi +++ b/contracts/interfaces/IFactory.vyi @@ -12,4 +12,4 @@ def admin() -> address: def fee_receiver() -> address: ... -# TODO double check view decorators on non generated interfaces \ No newline at end of file +# TODO double check view decorators on non generated interfaces diff --git a/contracts/interfaces/ILMGauge.vyi b/contracts/interfaces/ILMGauge.vyi index 11d76d07..7c7b8042 100644 --- a/contracts/interfaces/ILMGauge.vyi +++ b/contracts/interfaces/ILMGauge.vyi @@ -4,5 +4,5 @@ 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): - ... \ No newline at end of file +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 index 4d7b4fd4..18c19ba0 100644 --- a/contracts/interfaces/ILendingFactory.vyi +++ b/contracts/interfaces/ILendingFactory.vyi @@ -26,4 +26,4 @@ event NewVault: price_oracle: address monetary_policy: address -# TODO populate this \ No newline at end of file +# TODO populate this diff --git a/contracts/interfaces/ILlamalendController.vyi b/contracts/interfaces/ILlamalendController.vyi index eac1487a..95050cb5 100644 --- a/contracts/interfaces/ILlamalendController.vyi +++ b/contracts/interfaces/ILlamalendController.vyi @@ -1,6 +1,6 @@ from contracts.interfaces import IVault -from ethereum.ercs import IERC20 +from ethereum.ercs import IERC20 from contracts.interfaces import ILMGauge from contracts.interfaces import IAMM from contracts.interfaces import IMonetaryPolicy @@ -389,4 +389,3 @@ def borrowed_balance() -> uint256: @view def borrow_cap() -> uint256: ... - diff --git a/contracts/interfaces/IMintController.vyi b/contracts/interfaces/IMintController.vyi index c1bd3522..b425d473 100644 --- a/contracts/interfaces/IMintController.vyi +++ b/contracts/interfaces/IMintController.vyi @@ -1,4 +1,4 @@ -from ethereum.ercs import IERC20 +from ethereum.ercs import IERC20 from contracts.interfaces import ILMGauge from contracts.interfaces import IAMM from contracts.interfaces import IMonetaryPolicy @@ -73,7 +73,7 @@ event SetView: view: address -# Structs +# Structs struct Position: user: address @@ -360,9 +360,7 @@ def processed() -> uint256: @external def version() -> String[1]: ... - + @external def set_view(view_impl: address): ... - - diff --git a/contracts/interfaces/IMonetaryPolicy.vyi b/contracts/interfaces/IMonetaryPolicy.vyi index 865808ba..161d9a67 100644 --- a/contracts/interfaces/IMonetaryPolicy.vyi +++ b/contracts/interfaces/IMonetaryPolicy.vyi @@ -1,2 +1,2 @@ def rate_write() -> uint256: - ... \ No newline at end of file + ... diff --git a/contracts/interfaces/IPartialRepayZap.vyi b/contracts/interfaces/IPartialRepayZap.vyi index e2e292b7..dbc6acf7 100644 --- a/contracts/interfaces/IPartialRepayZap.vyi +++ b/contracts/interfaces/IPartialRepayZap.vyi @@ -39,4 +39,3 @@ def FRAC() -> uint256: @external def HEALTH_THRESHOLD() -> int256: ... - diff --git a/contracts/interfaces/IPool.vyi b/contracts/interfaces/IPool.vyi index c754fb90..d1368d10 100644 --- a/contracts/interfaces/IPool.vyi +++ b/contracts/interfaces/IPool.vyi @@ -6,4 +6,4 @@ def price_oracle(i: uint256=0) -> uint256: @view @external def coins(i: uint256) -> address: - ... \ No newline at end of file + ... diff --git a/contracts/interfaces/IPriceOracle.vyi b/contracts/interfaces/IPriceOracle.vyi index 0d52c578..d8d09916 100644 --- a/contracts/interfaces/IPriceOracle.vyi +++ b/contracts/interfaces/IPriceOracle.vyi @@ -3,4 +3,4 @@ def price() -> uint256: ... def price_w() -> uint256: - ... \ No newline at end of file + ... diff --git a/contracts/interfaces/IStablePool.vyi b/contracts/interfaces/IStablePool.vyi index beca05bf..65e1c5f9 100644 --- a/contracts/interfaces/IStablePool.vyi +++ b/contracts/interfaces/IStablePool.vyi @@ -1,5 +1,5 @@ @view -def coins(i: uint256) -> address: +def coins(i: uint256) -> address: ... @view @@ -7,5 +7,5 @@ def price_oracle(i: uint256 = 0) -> uint256: # Universal method! ... @view -def get_virtual_price() -> uint256: - ... \ No newline at end of file +def get_virtual_price() -> uint256: + ... diff --git a/contracts/interfaces/IVault.vyi b/contracts/interfaces/IVault.vyi index 83695e84..49d70582 100644 --- a/contracts/interfaces/IVault.vyi +++ b/contracts/interfaces/IVault.vyi @@ -47,4 +47,3 @@ def set_max_supply(max_supply: uint256): ... # TODO populate + implement - diff --git a/contracts/lib/liquidation_lib.vy b/contracts/lib/liquidation_lib.vy index 763a0bc4..880db46e 100644 --- a/contracts/lib/liquidation_lib.vy +++ b/contracts/lib/liquidation_lib.vy @@ -42,4 +42,3 @@ def users_with_health( ) ix += 1 return out - diff --git a/contracts/zaps/LeverageZap.vy b/contracts/zaps/LeverageZap.vy index 67d519cc..cd63f9df 100644 --- a/contracts/zaps/LeverageZap.vy +++ b/contracts/zaps/LeverageZap.vy @@ -268,14 +268,14 @@ 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 + # 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 + # 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) 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/source/conf.py b/docs/source/conf.py index 7715482f..6ba18668 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -6,17 +6,17 @@ # -- 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' +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'] +extensions = ["sphinx.ext.extlinks"] -templates_path = ['_templates'] +templates_path = ["_templates"] exclude_patterns = [] add_function_parentheses = False @@ -25,11 +25,9 @@ # -- 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'] +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 +extlinks = {"eip": ("https://eips.ethereum.org/EIPS/eip-%s", "EIP-%s")} +extlinks_detect_hardcoded_links = True diff --git a/docs/source/stablecoin.rst b/docs/source/stablecoin.rst index b7161888..ba806568 100644 --- a/docs/source/stablecoin.rst +++ b/docs/source/stablecoin.rst @@ -57,7 +57,7 @@ Functions :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 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. 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/pyproject.toml b/pyproject.toml index 9231a48e..a284d25f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,3 +29,11 @@ titanoboa = { git = "https://github.com/AlbertoCentonze/titanoboa", rev = "vvm-e [tool.uv] package = false # This is not a Python package + +[tool.ruff] + +ignore = [ + # Should be eventually fixed + "E722" , + "F821" +] 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 640d0723..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/deprecated/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 44f2377b..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/deprecated/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 812e72c6..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/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:]: + "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 34c84fc6..c6317985 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() # 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 + ) factory = boa.load( - '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:]: + "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 a24e103e..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/deprecated/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 b0e4d871..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/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:]: + "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 06a0bc0c..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/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('==========================') + 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 1b3b6c73..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/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) + 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/amm/conftest.py b/tests/amm/conftest.py index c3258fbf..e63cfe81 100644 --- a/tests/amm/conftest.py +++ b/tests/amm/conftest.py @@ -16,17 +16,25 @@ def get_amm(price_oracle, admin, accounts): def f(collateral_token, borrowed_token): with boa.env.prank(admin): 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) + 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 diff --git a/tests/amm/test_amount_for_price.py b/tests/amm/test_amount_for_price.py index 887cd262..9440c103 100644 --- a/tests/amm/test_amount_for_price.py +++ b/tests/amm/test_amount_for_price.py @@ -11,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) @@ -79,7 +91,9 @@ def test_amount_for_price(price_oracle, amm, accounts, collateral_token, borrowe # 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) + assert collateral_token.balanceOf( + amm + ) == 0 or p_o_ratio < a_ratio**-50 * (1 + 1e-8) else: if p_max == p_final: # The AMM gives the wrong price based on n0 band. It should be PUMP, not DUMP @@ -88,16 +102,26 @@ def test_amount_for_price(price_oracle, amm, accounts, collateral_token, borrowe # 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) + 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): +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 07695562..90fa70f6 100644 --- a/tests/amm/test_deposit_withdraw.py +++ b/tests/amm/test_deposit_withdraw.py @@ -9,12 +9,16 @@ @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): @@ -25,7 +29,7 @@ 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) @@ -36,9 +40,13 @@ def test_deposit_withdraw(amm, amounts, accounts, ns, dns, fracs, collateral_tok for user, n1 in zip(accounts, ns): if user in deposits: if n1 >= 0: - assert amm.get_y_up(user) == pytest.approx(deposits[user], rel=precisions[user], abs=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 @@ -47,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 before[1] - after[1] == pytest.approx(deposits[user] * frac / 1e18, rel=precisions[user], abs=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) @@ -55,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 57e5437a..5780dda4 100644 --- a/tests/amm/test_exchange.py +++ b/tests/amm/test_exchange.py @@ -6,9 +6,11 @@ @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): @@ -27,30 +29,33 @@ 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 dy == pytest.approx(dx * 10**(18 - 6) / 3000, rel=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.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) + 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)) @@ -85,14 +90,16 @@ 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 dx == pytest.approx(in_amount, rel=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 diff --git a/tests/amm/test_exchange_dy.py b/tests/amm/test_exchange_dy.py index 0050281a..0f4d5a3f 100644 --- a/tests/amm/test_exchange_dy.py +++ b/tests/amm/test_exchange_dy.py @@ -18,14 +18,16 @@ 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): @@ -40,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 dx == pytest.approx(dy * 3000 / 10**(collateral_decimals - borrowed_decimals), rel=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 @@ -60,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)) @@ -88,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 @@ -124,12 +131,14 @@ 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.001, 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)) @@ -167,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 diff --git a/tests/amm/test_flip.py b/tests/amm/test_flip.py index 7fba3f3a..3e3f4c1c 100644 --- a/tests/amm/test_flip.py +++ b/tests/amm/test_flip.py @@ -34,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: 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) @@ -56,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: @@ -68,7 +70,9 @@ def test_flip(amm, price_oracle, collateral_token, borrowed_token, accounts, adm 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 c1384eb6..ed6c6655 100644 --- a/tests/amm/test_flip_dy.py +++ b/tests/amm/test_flip_dy.py @@ -2,7 +2,6 @@ import pytest from pytest import mark # noqa from ..utils import mint_for_testing -from tests.utils.deployers import ERC20_MOCK_DEPLOYER # 1. deposit below (N > 0 in 5 bands) # 2. change price_oracle in a cycle downwards (by 15% just in case?) @@ -44,7 +43,13 @@ 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: @@ -53,8 +58,13 @@ def test_flip(amm, price_oracle, collateral_token, borrowed_token, accounts, adm 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() @@ -63,19 +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)) < 10**(18 - collateral_decimals) + 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: @@ -89,8 +116,13 @@ def test_flip(amm, price_oracle, collateral_token, borrowed_token, accounts, adm 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) @@ -104,7 +136,9 @@ 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)) < 10**(18 - borrowed_decimals) + 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 diff --git a/tests/amm/test_oracle_change_noloss.py b/tests/amm/test_oracle_change_noloss.py index b2bb956e..f9a1c153 100644 --- a/tests/amm/test_oracle_change_noloss.py +++ b/tests/amm/test_oracle_change_noloss.py @@ -5,7 +5,6 @@ from hypothesis import given, settings, example from hypothesis import strategies as st from ..utils import mint_for_testing -from tests.utils.deployers import ERC20_MOCK_DEPLOYER @pytest.fixture(scope="module") @@ -17,11 +16,21 @@ 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 @@ -62,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 @@ -109,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 @@ -146,9 +175,16 @@ def test_no_untradable_funds(amm, collateral_token, borrowed_token, price_oracle # Check that we cleaned up the last band new_b = borrowed_token.balanceOf(user) if borrowed_token.decimals() == 18: - assert sum(amm.bands_x(n) for n in range(61)) == borrowed_token.balanceOf(amm.address), "Insolvent" + 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 ( + 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 @@ -195,8 +241,15 @@ def test_no_untradable_funds_in(amm, collateral_token, borrowed_token, price_ora # Check that we cleaned up the last band new_b = borrowed_token.balanceOf(user) if borrowed_token.decimals() == 18: - assert sum(amm.bands_x(n) for n in range(61)) == borrowed_token.balanceOf(amm.address), "Insolvent" + 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 ( + 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_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 36cbae2b..2f044750 100644 --- a/tests/amm/test_st_exchange.py +++ b/tests/amm/test_st_exchange.py @@ -3,14 +3,22 @@ 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) @@ -23,8 +31,8 @@ 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 @@ -33,7 +41,7 @@ def initializer(self, amounts, ns, dns): self.amm.deposit_range(user, amount, n1, n2) 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 @@ -72,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 @@ -93,11 +103,13 @@ 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, - 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 = ERC20_MOCK_DEPLOYER.deploy(borrowed_digits) @@ -122,7 +134,9 @@ def test_raise_at_dy_back(admin, accounts, get_amm): 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) @@ -147,7 +161,9 @@ def test_raise_rounding(admin, accounts, get_amm): 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() @@ -166,7 +182,11 @@ def test_raise_rounding_2(admin, accounts, get_amm): 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) @@ -191,7 +211,9 @@ def test_raise_rounding_3(admin, accounts, get_amm): 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 d319675e..a5443ca7 100644 --- a/tests/amm/test_st_exchange_dy.py +++ b/tests/amm/test_st_exchange_dy.py @@ -3,7 +3,13 @@ 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 @@ -23,8 +29,8 @@ 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 @@ -33,7 +39,7 @@ def initializer(self, amounts, ns, dns): self.amm.deposit_range(user, amount, n1, n2) 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 @@ -69,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) @@ -78,13 +90,21 @@ 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: @@ -97,11 +117,13 @@ 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, - 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 = ERC20_MOCK_DEPLOYER.deploy(borrowed_digits) @@ -114,8 +136,11 @@ def test_exchange(admin, accounts, get_amm, 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)) + 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 @@ -128,7 +153,9 @@ def test_raise_at_dy_back(admin, accounts, get_amm): 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) @@ -141,8 +168,11 @@ def test_raise_at_dy_back(admin, accounts, get_amm): 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)) + 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 @@ -155,7 +185,17 @@ def test_raise_not_enough_left(admin, accounts, get_amm): 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 1a5f09f7..e1281cbb 100644 --- a/tests/amm/test_xdown_yup_invariants.py +++ b/tests/amm/test_xdown_yup_invariants.py @@ -3,6 +3,7 @@ import boa 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) @@ -17,15 +18,28 @@ 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) + 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() @@ -68,13 +82,18 @@ 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 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] @@ -109,14 +128,18 @@ 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 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] @@ -150,7 +173,9 @@ 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 y0 == pytest.approx(deposit_amount, rel=fee) assert x0 == pytest.approx(x1, rel=fee) @@ -165,14 +190,25 @@ 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) + 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) @@ -181,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 @@ -225,8 +269,20 @@ def test_adiabatic(amm, price_oracle, collateral_token, borrowed_token, accounts 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 4c055aa3..d9e91dce 100644 --- a/tests/amm/test_xdown_yup_invariants_dy.py +++ b/tests/amm/test_xdown_yup_invariants_dy.py @@ -3,7 +3,7 @@ import boa import pytest from ..utils import mint_for_testing -from tests.utils.deployers import ERC20_MOCK_DEPLOYER + """ Test that get_x_down and get_y_up don't change: * if we do trades at constant p_o (immediate trades) @@ -23,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) @@ -36,7 +49,7 @@ 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) + 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() @@ -63,7 +76,13 @@ def test_immediate(amm, price_oracle, collateral_token, borrowed_token, accounts 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): mint_for_testing(collateral_token, user, trade_amount) @@ -94,16 +113,27 @@ 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) + 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) @@ -112,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 diff --git a/tests/conftest.py b/tests/conftest.py index d257e92d..1506cc5f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,5 @@ import os from datetime import timedelta -import uuid import boa import pytest @@ -19,9 +18,14 @@ TESTING_DECIMALS = [2, 6, 8, 9, 18] -settings.register_profile("no-shrink", settings(phases=list(Phase)[:4]), deadline=timedelta(seconds=1000), print_blob=True) +settings.register_profile( + "no-shrink", + settings(phases=list(Phase)[:4]), + deadline=timedelta(seconds=1000), + print_blob=True, +) settings.register_profile("default", deadline=timedelta(seconds=1000), print_blob=True) -settings.load_profile(os.getenv(u"HYPOTHESIS_PROFILE", "default")) +settings.load_profile(os.getenv("HYPOTHESIS_PROFILE", "default")) @pytest.fixture(scope="module") @@ -43,6 +47,7 @@ def factory(proto): def stablecoin(proto): return proto.crvUSD + @pytest.fixture(scope="module") def collateral_token(): # TODO hook decimals fixture @@ -70,7 +75,9 @@ def lending_monetary_policy(): @pytest.fixture(scope="module") -def monetary_policy(market_type, controller, mint_monetary_policy, lending_monetary_policy): +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 @@ -192,7 +199,7 @@ 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'] + ctrl = market["controller"] if market_type == "lending" and borrow_cap is not None: with boa.env.prank(admin): ctrl.set_borrow_cap(borrow_cap) @@ -202,7 +209,7 @@ def controller(market, market_type, admin, borrow_cap): @pytest.fixture(scope="module") def amm(market): """AMM for the current market (mint or lending).""" - return market['amm'] + return market["amm"] @pytest.fixture(scope="module") @@ -231,10 +238,9 @@ def borrow_cap(seed_liquidity): return seed_liquidity - - # ============== Account Fixtures ============== + @pytest.fixture(scope="session") def accounts(): return [boa.env.generate_address() for _ in range(10)] @@ -247,10 +253,12 @@ def alice(): # ============== Token Fixtures ============== + @pytest.fixture(scope="module", params=TESTING_DECIMALS) def decimals(request): return request.param + @pytest.fixture(scope="session") def token_mock(): return ERC20_MOCK_DEPLOYER diff --git a/tests/controller/conftest.py b/tests/controller/conftest.py index 1719009c..d02b6dfa 100644 --- a/tests/controller/conftest.py +++ b/tests/controller/conftest.py @@ -3,6 +3,7 @@ # 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 @@ -11,7 +12,7 @@ def decimals(request): @fixture(scope="module") def collat(decimals): return ERC20_MOCK_DEPLOYER.deploy(decimals) - + @fixture(scope="module") def borrow(decimals): diff --git a/tests/controller/test_set_price_oracle.py b/tests/controller/test_set_price_oracle.py index 15167ab7..21821185 100644 --- a/tests/controller/test_set_price_oracle.py +++ b/tests/controller/test_set_price_oracle.py @@ -24,11 +24,11 @@ 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 @@ -36,7 +36,7 @@ def test_default_behavior(controller, amm, new_oracle, admin): 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) @@ -45,7 +45,7 @@ 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) @@ -70,8 +70,10 @@ def test_max_deviation_skip_check(controller, high_deviation_oracle, admin, amm, 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 - + 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 @@ -87,7 +89,7 @@ def broken_oracle(): 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) @@ -100,11 +102,13 @@ def different_price_oracle(admin): 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): +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 @@ -113,7 +117,7 @@ def test_price_deviation_check_exceeds_limit(controller, different_price_oracle, """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) @@ -121,8 +125,10 @@ def test_price_deviation_check_exceeds_limit(controller, different_price_oracle, 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) - + 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 @@ -131,8 +137,10 @@ def test_price_deviation_calculation_higher_new_price(controller, admin, amm): 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) - + 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 @@ -141,8 +149,10 @@ def test_price_deviation_calculation_lower_new_price(controller, admin, amm): 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) - + 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 @@ -151,8 +161,10 @@ def test_price_deviation_at_exact_limit(controller, admin, amm): 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) - + 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/flashloan/conftest.py b/tests/flashloan/conftest.py index 40fcd747..d7c8cff5 100644 --- a/tests/flashloan/conftest.py +++ b/tests/flashloan/conftest.py @@ -1,9 +1,6 @@ import boa import pytest -from tests.utils.deployers import ( - FLASH_LENDER_DEPLOYER, - DUMMY_FLASH_BORROWER_DEPLOYER -) +from tests.utils.deployers import FLASH_LENDER_DEPLOYER, DUMMY_FLASH_BORROWER_DEPLOYER @pytest.fixture(scope="module") @@ -13,7 +10,7 @@ def controller_factory(proto): @pytest.fixture(scope="module") def max_flash_loan(): - return 3 * 10**6 * 10 ** 18 + return 3 * 10**6 * 10**18 @pytest.fixture(scope="module") diff --git a/tests/flashloan/test_debt_ceiling.py b/tests/flashloan/test_debt_ceiling.py index 0fbfcfb9..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,7 +37,9 @@ 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): +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 @@ -43,7 +51,9 @@ def test_increase_debt_ceiling_with_excess(controller_factory, flash_lender, sta 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): +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 @@ -55,7 +65,9 @@ def test_decrease_debt_ceiling_with_excess(controller_factory, flash_lender, sta 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): +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 diff --git a/tests/flashloan/test_flashloan.py b/tests/flashloan/test_flashloan.py index 2fa088c1..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) diff --git a/tests/lending/conftest.py b/tests/lending/conftest.py index eda102b5..2d9276b4 100644 --- a/tests/lending/conftest.py +++ b/tests/lending/conftest.py @@ -25,5 +25,7 @@ def fake_leverage(collateral_token, borrowed_token, controller, admin): controller.address, 3000 * 10**18, ) - boa.deal(collateral_token, leverage.address, 1000 * 10**collateral_token.decimals()) + boa.deal( + collateral_token, leverage.address, 1000 * 10 ** collateral_token.decimals() + ) return leverage diff --git a/tests/lending/test_bigfuzz.py b/tests/lending/test_bigfuzz.py index 191178fb..d4af49b2 100644 --- a/tests/lending/test_bigfuzz.py +++ b/tests/lending/test_bigfuzz.py @@ -3,7 +3,12 @@ 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 @@ -43,22 +48,26 @@ class BigFuzz(RuleBasedStateMachine): def __init__(self): super().__init__() self.A = self.amm.A() - self.collateral_mul = 10**(18 - self.collateral_token.decimals()) - self.borrowed_mul = 10**(18 - self.borrowed_token.decimals()) + 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.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) + 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.controller.address) >= amount def get_max_good_band(self): - return ceil(log2(self.amm.get_base_price() / self.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): @@ -79,7 +88,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) @@ -99,9 +110,13 @@ def create_loan(self, y, n, ratio, uid): with boa.reverts(): self.controller.create_loan(y, debt, n) return - 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)): + 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.controller.create_loan(y, debt, n) except Exception: @@ -111,7 +126,7 @@ def create_loan(self, y, n, ratio, uid): 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: @@ -144,8 +159,18 @@ def repay(self, ratio, uid): self.controller.repay(amount, user) else: if amount > 0 and ( - (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)))): + ( + 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.controller.repay(amount, user) else: @@ -162,7 +187,11 @@ def add_collateral(self, y, uid): boa.deal(self.collateral_token, user, y) with boa.env.prank(user): - if (exists and n1 > n0 and self.amm.p_oracle_up(n1) < self.amm.price_oracle()) or y == 0: + 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(): @@ -216,7 +245,14 @@ def borrow_more(self, y, ratio, uid): sx, sy = self.amm.get_sum_xy(user) n1, n2 = self.amm.read_user_tick_numbers(user) n = n2 - n1 + 1 - amount = int(self.amm.price_oracle() * (sy + y) * self.collateral_mul / 1e18 * ratio / self.borrowed_mul) + 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 @@ -229,7 +265,9 @@ def borrow_more(self, y, ratio, uid): 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.controller.borrow_more(y, amount) except Exception: @@ -241,7 +279,10 @@ def borrow_more(self, y, ratio, uid): try: self.controller.borrow_more(y, amount) except Exception: - if self.get_max_good_band() > self.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 @@ -291,18 +332,20 @@ def self_liquidate_and_health(self, emode, frac): boa.deal(self.borrowed_token, user, diff) if emode == USE_FRACTION: try: - self.controller.liquidate( - user, 0, frac, ZERO_ADDRESS, b'') + self.controller.liquidate(user, 0, frac, ZERO_ADDRESS, b"") except Exception: 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.controller.liquidate( - user, 0, frac, - self.fake_leverage.address, b'') + user, 0, frac, self.fake_leverage.address, b"" + ) except Exception: if self.controller.debt(user) * frac // 10**18 == 0: return @@ -333,55 +376,67 @@ def liquidate(self, uid, luid, emode, frac): with boa.env.prank(liquidator): with boa.reverts(): if emode == USE_FRACTION: - self.controller.liquidate( - user, 0, frac, ZERO_ADDRESS, b'') + 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.borrowed_token.transfer( + self.fake_leverage.address, + self.borrowed_token.balanceOf(user), + ) self.controller.liquidate( - user, 0, frac, - self.fake_leverage.address, b'') + user, 0, frac, self.fake_leverage.address, b"" + ) else: 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.controller.liquidation_discount() try: 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.controller.liquidate( - user, 0, frac, ZERO_ADDRESS, b'') + 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.borrowed_token.transfer( + self.fake_leverage.address, + self.borrowed_token.balanceOf(user), + ) self.controller.liquidate( - user, 0, frac, - self.fake_leverage.address, b'') + user, 0, frac, self.fake_leverage.address, b"" + ) else: 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.controller.liquidate( - user, 0, frac, ZERO_ADDRESS, b'') + self.controller.liquidate(user, 0, frac, ZERO_ADDRESS, b"") except Exception: 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.controller.liquidate( - user, 0, frac, - self.fake_leverage.address, b'') + user, 0, frac, self.fake_leverage.address, b"" + ) except Exception: if self.controller.debt(user) * frac // 10**18 == 0: return @@ -407,7 +462,11 @@ 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.monetary_policy.set_rates(min_rate, max_rate) else: @@ -421,17 +480,33 @@ def time_travel(self, dt): def debt_supply(self): total_debt = self.controller.total_debt() if total_debt == 0: - 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 + 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.controller.repaid() + self.controller.total_debt() >= self.controller.lent() + assert ( + self.controller.repaid() + self.controller.total_debt() + >= self.controller.lent() + ) def test_big_fuzz( - vault, borrowed_token, collateral_token, monetary_policy, accounts, admin, amm, 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 index bd9cefb6..f6c553df 100644 --- a/tests/lending/test_fuzz_max_borrowable.py +++ b/tests/lending/test_fuzz_max_borrowable.py @@ -10,17 +10,27 @@ @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)) + 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): +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() + c_amount = 10 ** collateral_token.decimals() boa.deal(collateral_token, admin, c_amount) - collateral_token.approve(controller, 2**256-1) + 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) + borrowed_token.approve(amm.address, 2**256 - 1) amm.exchange(0, 1, 100, 0) # Change oracle @@ -33,31 +43,46 @@ def test_max_borrowable(borrowed_token, collateral_token, amm, controller, price if max_borrowable >= total: return with boa.reverts(): - controller.calculate_debt_n1(collateral_amount, int(max_borrowable * 1.001) + n, n) + 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)) + 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)) + 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): +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() + c_amount = 10 ** collateral_token.decimals() boa.deal(collateral_token, admin, c_amount) - collateral_token.approve(controller, 2**256-1) + 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) + borrowed_token.approve(amm.address, 2**256 - 1) amm.exchange(0, 1, 10**2, 0) # Change oracle @@ -67,4 +92,4 @@ def test_min_collateral(borrowed_token, collateral_token, amm, controller, price min_collateral = controller.min_collateral(debt_amount, n) - controller.calculate_debt_n1(min_collateral, debt_amount, n) \ No newline at end of file + 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 3d1687ea..47cb5913 100644 --- a/tests/lending/test_health_calculator_create.py +++ b/tests/lending/test_health_calculator_create.py @@ -8,8 +8,10 @@ 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(amm, 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: diff --git a/tests/lending/test_health_calculator_stateful.py b/tests/lending/test_health_calculator_stateful.py index 998dbdd2..f9499c62 100644 --- a/tests/lending/test_health_calculator_stateful.py +++ b/tests/lending/test_health_calculator_stateful.py @@ -1,11 +1,17 @@ """ 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 hypothesis.stateful import ( + RuleBasedStateMachine, + run_state_machine_as_test, + rule, + invariant, +) import pytest @@ -19,7 +25,7 @@ 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): @@ -27,13 +33,16 @@ def __init__(self): self.controller = self.controller self.amm = self.amm self.debt_ceiling = self.controller.borrowed_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) + 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 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) + 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,7 +119,7 @@ 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 @@ -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) @@ -164,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() @@ -173,13 +200,18 @@ 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): @@ -209,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: @@ -224,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() @@ -233,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): @@ -251,8 +298,12 @@ def health(self): assert self.controller.health(user) > 0 -def test_stateful_lendborrow(amm, 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) diff --git a/tests/lending/test_health_in_trades.py b/tests/lending/test_health_in_trades.py index eb2ddee8..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,7 +18,10 @@ 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): @@ -20,10 +29,12 @@ def __init__(self): self.collateral = self.collateral_token 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()) + 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.monetary_policy.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,9 +45,13 @@ 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): boa.deal(self.collateral, user, collateral_amount) @@ -75,7 +90,10 @@ 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) @@ -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.monetary_policy.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(amm, controller, monetary_policy, 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_monetary_policy.py b/tests/lending/test_monetary_policy.py index 93b6b4b3..140601d9 100644 --- a/tests/lending/test_monetary_policy.py +++ b/tests/lending/test_monetary_policy.py @@ -5,10 +5,18 @@ @given(fill=st.floats(min_value=0.0, max_value=2.0)) -def test_monetary_policy(controller, collateral_token, borrowed_token, monetary_policy, admin, fill): +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): diff --git a/tests/lending/test_oracle_attack.py b/tests/lending/test_oracle_attack.py index 5a6bc422..67bca32c 100644 --- a/tests/lending/test_oracle_attack.py +++ b/tests/lending/test_oracle_attack.py @@ -29,18 +29,30 @@ def borrow_cap(): @given( victim_gap=st.floats(min_value=15 / 866, max_value=0.9), - victim_bins=st.integers(min_value=4, max_value=50) + 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): +def test_vuln( + vault, + controller, + amm, + admin, + borrowed_token, + price_oracle, + collateral_token, + accounts, + victim_gap, + victim_bins, +): victim = accounts[1] hacker = accounts[2] # victim loan victim_collateral_lent = int(10_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() @@ -68,7 +80,10 @@ def test_vuln(vault, controller, amm, admin, borrowed_token, price_oracle, colla # victim creates a loan with boa.env.prank(victim): - victim_borrow = int((1 - victim_gap) * controller.max_borrowable(victim_collateral_lent, victim_bins)) + 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 @@ -101,7 +116,16 @@ def test_vuln(vault, controller, amm, admin, borrowed_token, price_oracle, colla @pytest.mark.xfail(strict=True) -def test_vuln_lite(vault, controller, amm, admin, borrowed_token, price_oracle, collateral_token, accounts): +def test_vuln_lite( + vault, + controller, + amm, + admin, + borrowed_token, + price_oracle, + collateral_token, + accounts, +): victim_gap = 0 victim_bins = 4 victim = accounts[1] @@ -109,7 +133,9 @@ def test_vuln_lite(vault, controller, amm, admin, borrowed_token, price_oracle, # 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() @@ -137,7 +163,10 @@ def test_vuln_lite(vault, controller, amm, admin, borrowed_token, price_oracle, # victim creates a loan with boa.env.prank(victim): - victim_borrow = int((1 - victim_gap) * controller.max_borrowable(victim_collateral_lent, victim_bins)) + 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 diff --git a/tests/lending/test_pool_price_oracle.py b/tests/lending/test_pool_price_oracle.py index 4c503c3f..087b3d31 100644 --- a/tests/lending/test_pool_price_oracle.py +++ b/tests/lending/test_pool_price_oracle.py @@ -4,7 +4,7 @@ from tests.utils.deployers import ( MOCK_SWAP2_DEPLOYER, MOCK_SWAP3_DEPLOYER, - CRYPTO_FROM_POOL_DEPLOYER + CRYPTO_FROM_POOL_DEPLOYER, ) diff --git a/tests/lending/test_secondary_mpolicy.py b/tests/lending/test_secondary_mpolicy.py index 526218a8..5f27fab5 100644 --- a/tests/lending/test_secondary_mpolicy.py +++ b/tests/lending/test_secondary_mpolicy.py @@ -4,11 +4,10 @@ from hypothesis import settings from hypothesis import strategies as st from tests.utils.deployers import ( - ERC20_MOCK_DEPLOYER, MOCK_FACTORY_DEPLOYER, MOCK_MARKET_DEPLOYER, MOCK_RATE_SETTER_DEPLOYER, - SECONDARY_MONETARY_POLICY_DEPLOYER + SECONDARY_MONETARY_POLICY_DEPLOYER, ) @@ -37,8 +36,15 @@ def amm(): @pytest.fixture(scope="module") def mp(factory, amm, borrowed_token): - return SECONDARY_MONETARY_POLICY_DEPLOYER.deploy(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( @@ -47,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: diff --git a/tests/lending/test_shifted_trades.py b/tests/lending/test_shifted_trades.py index 76ea5849..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(amm, controller, monetary_policy, 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 cbe5fdd4..8c4abcf2 100644 --- a/tests/lending/test_st_interest_conservation.py +++ b/tests/lending/test_st_interest_conservation.py @@ -1,18 +1,23 @@ 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% +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) @@ -24,13 +29,13 @@ def __init__(self): # 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()) + 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) boa.deal(self.borrowed_token, self.accounts[0], self.debt_ceiling) @@ -42,7 +47,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 @@ -50,9 +55,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 @@ -82,7 +87,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 @@ -90,10 +102,18 @@ 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) @@ -137,7 +157,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 @@ -145,7 +167,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) @@ -178,9 +202,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 @@ -189,7 +213,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 @@ -203,7 +230,13 @@ 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.monetary_policy.set_rates(min_rate, max_rate) else: @@ -223,17 +256,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, amm, controller, monetary_policy, 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, amm, controller, monetary_policy, 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_vault.py b/tests/lending/test_vault.py index 033ff63e..942cfa99 100644 --- a/tests/lending/test_vault.py +++ b/tests/lending/test_vault.py @@ -2,15 +2,29 @@ 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 hypothesis.stateful import ( + RuleBasedStateMachine, + run_state_machine_as_test, + rule, + invariant, +) # TODO get this from contract directly DEAD_SHARES = 1000 -def test_vault_creation(vault, controller, amm, monetary_policy, factory, price_oracle, - borrowed_token, collateral_token, stablecoin): +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 @@ -27,7 +41,7 @@ def test_vault_creation(vault, controller, amm, monetary_policy, factory, price_ assert factory.vaults(factory.vaults_index(vault.address)) == vault.address -@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 @@ -42,7 +56,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) @@ -50,7 +64,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 @@ -58,7 +74,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__() @@ -87,7 +105,9 @@ 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 pps == pytest.approx(self.pps, rel=1e-2) @@ -195,7 +215,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 @@ -224,7 +247,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 @@ -243,7 +269,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 @@ -259,7 +291,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 @@ -297,7 +332,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 @@ -326,7 +364,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 @@ -345,7 +386,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 @@ -361,7 +408,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 diff --git a/tests/lm_callback/conftest.py b/tests/lm_callback/conftest.py index 67e126f7..b57a230e 100644 --- a/tests/lm_callback/conftest.py +++ b/tests/lm_callback/conftest.py @@ -13,7 +13,7 @@ CONSTANT_MONETARY_POLICY_DEPLOYER, LM_CALLBACK_DEPLOYER, BLOCK_COUNTER_DEPLOYER, - LL_CONTROLLER_DEPLOYER + LL_CONTROLLER_DEPLOYER, ) @@ -26,7 +26,9 @@ def crv(admin): @pytest.fixture(scope="module") def voting_escrow(admin, crv): with boa.env.prank(admin): - return VOTING_ESCROW_DEPLOYER.deploy(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") @@ -34,7 +36,7 @@ def gauge_controller(admin, crv, voting_escrow): with boa.env.prank(admin): 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 @@ -59,7 +61,7 @@ def chad(collateral_token, admin): @pytest.fixture(scope="module") def stablecoin(admin, chad): with boa.env.prank(admin): - _stablecoin = STABLECOIN_DEPLOYER.deploy('Curve USD', 'crvUSD') + _stablecoin = STABLECOIN_DEPLOYER.deploy("Curve USD", "crvUSD") _stablecoin.mint(chad, 10**25) return _stablecoin @@ -74,7 +76,9 @@ def weth(admin): @pytest.fixture(scope="module") def controller_prefactory(stablecoin, weth, admin, accounts): with boa.env.prank(admin): - return CONTROLLER_FACTORY_DEPLOYER.deploy(stablecoin.address, admin, admin, weth.address) + return CONTROLLER_FACTORY_DEPLOYER.deploy( + stablecoin.address, admin, admin, weth.address + ) @pytest.fixture(scope="module") @@ -90,9 +94,13 @@ def amm_impl(stablecoin, admin): @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 @@ -106,27 +114,36 @@ def monetary_policy(admin): @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 @@ -141,17 +158,29 @@ def market_amm(market, collateral_token, stablecoin, accounts): @pytest.fixture(scope="module") -def market_controller(market, stablecoin, collateral_token, controller_factory, accounts): +def market_controller( + market, stablecoin, collateral_token, controller_factory, accounts +): return LL_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 = LM_CALLBACK_DEPLOYER.deploy(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 diff --git a/tests/lm_callback/test_add_new_lm_callback.py b/tests/lm_callback/test_add_new_lm_callback.py index 132e2d9a..7704d4cf 100644 --- a/tests/lm_callback/test_add_new_lm_callback.py +++ b/tests/lm_callback/test_add_new_lm_callback.py @@ -6,15 +6,15 @@ 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] @@ -31,9 +31,11 @@ def test_add_new_lm_callback( # Wire up the new LM Callback to the gauge controller to have proper rates and stuff with boa.env.prank(admin): - new_cb = LM_CALLBACK_DEPLOYER.deploy(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) @@ -47,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) @@ -57,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 be17fe96..3ec45f23 100644 --- a/tests/lm_callback/test_as_gauge.py +++ b/tests/lm_callback/test_as_gauge.py @@ -2,12 +2,14 @@ from random import random, randrange 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) @@ -21,7 +23,12 @@ def test_gauge_integral_one_user(accounts, admin, collateral_token, crv, lm_call boa.deal(collateral_token, alice, 1000 * 10**18) def update_integral(): - nonlocal checkpoint, checkpoint_rate, integral, checkpoint_balance, checkpoint_supply + nonlocal \ + checkpoint, \ + checkpoint_rate, \ + integral, \ + checkpoint_balance, \ + checkpoint_supply t1 = boa.env.timestamp rate1 = crv.rate() @@ -29,7 +36,9 @@ def update_integral(): 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 @@ -87,7 +104,9 @@ def update_integral(): 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] @@ -106,7 +125,12 @@ def test_gauge_integral(accounts, admin, collateral_token, crv, lm_callback, mar boa.deal(collateral_token, bob, 1000 * 10**18) def update_integral(): - nonlocal checkpoint, checkpoint_rate, integral, checkpoint_balance, checkpoint_supply + nonlocal \ + checkpoint, \ + checkpoint_rate, \ + integral, \ + checkpoint_balance, \ + checkpoint_supply t1 = boa.env.timestamp rate1 = crv.rate() @@ -114,7 +138,9 @@ def update_integral(): 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 lm_callback.integrate_fraction(alice) == pytest.approx(integral, rel=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): - boa.deal(collateral_token, 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 fb04e85c..273855cf 100644 --- a/tests/lm_callback/test_lm_callback.py +++ b/tests/lm_callback/test_lm_callback.py @@ -2,29 +2,29 @@ import pytest from random import random, randrange, choice -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): - boa.deal(collateral_token, alice, 1000 * 10 ** 18) - boa.deal(collateral_token, 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) @@ -64,16 +64,16 @@ 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] @@ -92,7 +92,12 @@ def test_gauge_integral_with_exchanges( boa.deal(collateral_token, bob, 1000 * 10**18) def update_integral(): - nonlocal checkpoint, checkpoint_rate, integral, checkpoint_balance, checkpoint_supply + nonlocal \ + checkpoint, \ + checkpoint_rate, \ + integral, \ + checkpoint_balance, \ + checkpoint_supply t1 = boa.env.timestamp t_epoch = crv.start_epoch_time_write(sender=admin) @@ -100,7 +105,9 @@ def update_integral(): 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 @@ -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 market_amm.get_sum_xy(bob)[1] == pytest.approx(lm_callback.user_collateral(bob), rel=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 market_amm.get_sum_xy(bob)[1] == pytest.approx(lm_callback.user_collateral(bob), rel=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 market_amm.get_sum_xy(bob)[1] == pytest.approx(lm_callback.user_collateral(bob), rel=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 market_amm.get_sum_xy(alice)[1] == pytest.approx(lm_callback.user_collateral(alice), rel=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 market_amm.get_sum_xy(alice)[1] == pytest.approx(lm_callback.user_collateral(alice), rel=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 market_amm.get_sum_xy(alice)[1] == pytest.approx(lm_callback.user_collateral(alice), rel=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) @@ -240,9 +326,15 @@ def update_integral(): 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 total_collateral_from_amm == pytest.approx(total_collateral_from_lm_cb, rel=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 lm_callback.integrate_fraction(alice) == pytest.approx(integral, rel=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,16 +358,16 @@ 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] @@ -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), - 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) @@ -334,8 +427,12 @@ def test_full_repay_underwater( 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 total_collateral_from_amm == pytest.approx(total_collateral_from_lm_cb, rel=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 3895b936..99b9d7ea 100644 --- a/tests/lm_callback/test_lm_callback_reverts.py +++ b/tests/lm_callback/test_lm_callback_reverts.py @@ -6,14 +6,14 @@ 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] @@ -23,7 +23,6 @@ def test_add_new_lm_callback( boa.env.time_travel(seconds=2 * WEEK + 5) 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)] @@ -43,7 +42,7 @@ def test_add_new_lm_callback( with boa.env.prank(admin): 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) @@ -52,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 a7c58260..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): - boa.deal(collateral_token, 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): - boa.deal(collateral_token, 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 09fd3288..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 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,11 +21,14 @@ def __init__(self): super().__init__() self.checkpoint_total_collateral = 0 self.checkpoint_rate = self.crv.rate() - self.integrals = {addr: { - "checkpoint": boa.env.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 @@ -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 self.lm_callback.integrate_fraction(user) == pytest.approx(self.integrals[user]["integral"], rel=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 self.lm_callback.integrate_fraction(user) == pytest.approx(self.integrals[user]["integral"], rel=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): @@ -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): """ @@ -153,7 +185,10 @@ def teardown(self): 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"] @@ -169,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): boa.deal(collateral_token, acct, 1000 * 10**18) - crv.transfer(acct, 10 ** 20) + 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 772e65d5..704fc5e2 100644 --- a/tests/lm_callback/test_st_lm_callback.py +++ b/tests/lm_callback/test_st_lm_callback.py @@ -1,7 +1,12 @@ 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, +) import pytest @@ -20,11 +25,14 @@ def __init__(self): super().__init__() self.checkpoint_total_collateral = 0 self.checkpoint_rate = self.crv.rate() - self.integrals = {addr: { - "checkpoint": boa.env.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 @@ -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.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: @@ -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 @@ -112,8 +138,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) @@ -139,19 +171,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: @@ -204,11 +260,15 @@ def invariant_collateral(self): for account, integral in self.integrals.items(): y1 = self.lm_callback.user_collateral(account) y2 = integral["collateral"] - assert y1 == pytest.approx(y2, rel=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 Y1 == pytest.approx(Y2, rel=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): """ @@ -224,7 +284,10 @@ def teardown(self): 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"] @@ -240,25 +303,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): boa.deal(collateral_token, acct, 1000 * 10**18) - crv.transfer(acct, 10 ** 20) + 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/price_oracles/lp-oracles/conftest.py b/tests/price_oracles/lp-oracles/conftest.py index af227a9d..f53aeb7f 100644 --- a/tests/price_oracles/lp-oracles/conftest.py +++ b/tests/price_oracles/lp-oracles/conftest.py @@ -11,9 +11,10 @@ PROXY_ORACLE_FACTORY_DEPLOYER, LP_ORACLE_STABLE_DEPLOYER, LP_ORACLE_CRYPTO_DEPLOYER, - LP_ORACLE_FACTORY_DEPLOYER + LP_ORACLE_FACTORY_DEPLOYER, ) + @pytest.fixture(scope="session") def user(accounts): return accounts[0] @@ -28,7 +29,9 @@ def broken_contract(admin): @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)) + return DUMMY_PRICE_ORACLE_DEPLOYER.deploy( + admin, random.randint(99 * 10**17, 101 * 10**17) + ) @pytest.fixture(scope="module") @@ -50,7 +53,9 @@ def crypto_swap(admin): @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)) + return MOCK_STABLE_SWAP_NO_ARGUMENT_DEPLOYER.deploy( + admin, random.randint(10**16, 10**23) + ) @pytest.fixture(scope="module") @@ -78,9 +83,13 @@ def lp_oracle_crypto_impl(admin): @pytest.fixture(scope="module") -def lp_oracle_factory(admin, lp_oracle_stable_impl, lp_oracle_crypto_impl, proxy_factory): +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) + return LP_ORACLE_FACTORY_DEPLOYER.deploy( + admin, lp_oracle_stable_impl, lp_oracle_crypto_impl, proxy_factory + ) @pytest.fixture(scope="module") diff --git a/tests/price_oracles/lp-oracles/test_lp_oracle.py b/tests/price_oracles/lp-oracles/test_lp_oracle.py index e69ca061..d7d09637 100644 --- a/tests/price_oracles/lp-oracles/test_lp_oracle.py +++ b/tests/price_oracles/lp-oracles/test_lp_oracle.py @@ -1,7 +1,5 @@ import boa -from tests.utils.constants import ZERO_ADDRESS - def _get_lp_stable_price(stable_swap): prices = [10**18] @@ -14,7 +12,14 @@ def _get_lp_stable_price(stable_swap): return min(prices) -def test_lp_oracle_stable(get_lp_oracle_stable, get_stable_swap, stable_swap_no_argument, coin0_oracle, broken_contract, admin): +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(): @@ -31,7 +36,7 @@ def test_lp_oracle_stable(get_lp_oracle_stable, get_stable_swap, stable_swap_no_ with boa.reverts("pool.price_oracle() returns 0"): get_lp_oracle_stable(stable_swap, coin0_oracle) else: - for i in range(N-1): + 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"): @@ -44,11 +49,19 @@ def test_lp_oracle_stable(get_lp_oracle_stable, get_stable_swap, stable_swap_no_ 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 + 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): +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(): diff --git a/tests/price_oracles/lp-oracles/test_lp_oracle_factory.py b/tests/price_oracles/lp-oracles/test_lp_oracle_factory.py index c74dbdde..e8fa2edb 100644 --- a/tests/price_oracles/lp-oracles/test_lp_oracle_factory.py +++ b/tests/price_oracles/lp-oracles/test_lp_oracle_factory.py @@ -3,7 +3,15 @@ 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): +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(): @@ -20,7 +28,7 @@ def test_lp_oracle_stable_factory(lp_oracle_factory, proxy_factory, get_stable_s with boa.reverts(): lp_oracle_factory.deploy_oracle(stable_swap, coin0_oracle) else: - for i in range(N-1): + for i in range(N - 1): with boa.env.anchor(): stable_swap.set_price(i, 0, sender=admin) with boa.reverts(): @@ -32,27 +40,45 @@ def test_lp_oracle_stable_factory(lp_oracle_factory, proxy_factory, get_stable_s 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, 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 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 + 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 + 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): +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(): @@ -69,13 +95,18 @@ def test_lp_oracle_crypto_factory(lp_oracle_factory, proxy_factory, crypto_swap, 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, 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 + 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) diff --git a/tests/price_oracles/proxy/conftest.py b/tests/price_oracles/proxy/conftest.py index 0d224039..7948d95a 100644 --- a/tests/price_oracles/proxy/conftest.py +++ b/tests/price_oracles/proxy/conftest.py @@ -4,7 +4,7 @@ DUMMY_PRICE_ORACLE_DEPLOYER, WETH_DEPLOYER, PROXY_ORACLE_DEPLOYER, - PROXY_ORACLE_FACTORY_DEPLOYER + PROXY_ORACLE_FACTORY_DEPLOYER, ) diff --git a/tests/price_oracles/proxy/test_proxy.py b/tests/price_oracles/proxy/test_proxy.py index 5d39b6b0..e851ccdd 100644 --- a/tests/price_oracles/proxy/test_proxy.py +++ b/tests/price_oracles/proxy/test_proxy.py @@ -37,8 +37,8 @@ def test_proxy(proxy_factory, get_price_oracle, user, admin, broken_price_oracle # --- Replace oracle --- - oracle2 = get_price_oracle(105 * 10 ** 18) - oracle_deviation_too_high = get_price_oracle(105 * 10 ** 18 + 1) + 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"): @@ -52,7 +52,9 @@ def test_proxy(proxy_factory, get_price_oracle, user, admin, broken_price_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 + 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 diff --git a/tests/stableborrow/conftest.py b/tests/stableborrow/conftest.py index cc6e7ec6..ba7ba306 100644 --- a/tests/stableborrow/conftest.py +++ b/tests/stableborrow/conftest.py @@ -8,12 +8,12 @@ MINT_CONTROLLER_DEPLOYER, AMM_DEPLOYER, CONSTANT_MONETARY_POLICY_DEPLOYER, - FAKE_LEVERAGE_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") @@ -24,7 +24,7 @@ def stablecoin_pre(): @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="module") @@ -41,7 +41,9 @@ def controller_factory_impl(): @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) + return controller_factory_impl.deploy( + stablecoin.address, admin, accounts[0], weth.address + ) @pytest.fixture(scope="module") @@ -57,9 +59,13 @@ def amm_impl(admin): @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 @@ -73,24 +79,33 @@ def monetary_policy(admin): @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 @@ -114,10 +129,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 = 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()) + 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 68882222..36946390 100644 --- a/tests/stableborrow/stabilize/conftest.py +++ b/tests/stableborrow/stabilize/conftest.py @@ -19,9 +19,10 @@ AGG_MONETARY_POLICY2_DEPLOYER, CHAINLINK_AGGREGATOR_MOCK_DEPLOYER, AMM_DEPLOYER, - LL_CONTROLLER_DEPLOYER + LL_CONTROLLER_DEPLOYER, ) from tests.utils.constants import ZERO_ADDRESS + BASE_AMOUNT = 10**6 @@ -86,8 +87,8 @@ def swap_impl_ng(admin, swap_deployer, rate_oracle): with boa.env.prank(admin): # Do not forget `git submodule init` and `git submodule update` 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}') + swap_deployer.eval(f"self.factory_ng = FactoryNG({factory.address})") + swap_deployer.eval(f"self.rate_oracle = {rate_oracle.address}") impl = CURVE_STABLESWAP_NG_DEPLOYER.deploy_as_blueprint() factory.set_pool_implementations(0, impl) @@ -110,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) @@ -118,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) @@ -136,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 @@ -157,25 +163,36 @@ def price_aggregator(stablecoin, stableswap_a, stableswap_b, admin): def dummy_tricrypto(stablecoin_a, admin): with boa.env.prank(admin): pool = TRICRYPTO_MOCK_DEPLOYER.deploy( - [stablecoin_a.address, - "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", - "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"]) + [ + 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): 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) @@ -186,18 +203,16 @@ def agg(stablecoin, stablecoin_a, stablecoin_b, stableswap_a, stableswap_b, pric def crypto_agg(dummy_tricrypto, agg, stableswap_a, admin): with boa.env.prank(admin): crypto_agg = CRYPTO_WITH_STABLE_PRICE_DEPLOYER.deploy( - dummy_tricrypto.address, - 0, - stableswap_a, - agg, - 5000 + 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 = CRYPTO_WITH_STABLE_PRICE_AND_CHAINLINK_DEPLOYER.deploy( dummy_tricrypto.address, @@ -206,7 +221,7 @@ def crypto_agg_with_external_oracle(dummy_tricrypto, agg, stableswap_a, chainlin agg, chainlink_price_oracle.address, 5000, - 1 + 1, ) crypto_agg.price_w() return crypto_agg @@ -214,9 +229,9 @@ 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 [ - MOCK_PEG_KEEPER_DEPLOYER.deploy(price, stablecoin) for price in [0, 2 ** 256 - 1] + MOCK_PEG_KEEPER_DEPLOYER.deploy(price, stablecoin) for price in [0, 2**256 - 1] ] @@ -226,21 +241,34 @@ def reg(agg, stablecoin, mock_peg_keepers, receiver, admin): 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( - PEG_KEEPER_V2_DEPLOYER.deploy( - 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 @@ -250,25 +278,39 @@ def peg_keepers(stablecoin_a, stablecoin_b, stableswap_a, stableswap_b, controll def agg_monetary_policy(peg_keepers, agg, controller_factory, admin): with boa.env.prank(admin): 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% + 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 @@ -279,25 +321,37 @@ 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_factory, accounts): - controller = LL_CONTROLLER_DEPLOYER.at(market_agg.get_controller(collateral_token.address)) +def market_controller_agg( + market_agg, + stablecoin, + collateral_token, + controller_impl, + controller_factory, + accounts, +): + controller = LL_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") @@ -310,20 +364,35 @@ def f(acct, coins, amounts): collateral_amount = amount * 50 // 3000 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: 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) @@ -331,8 +400,12 @@ 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) @@ -347,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 swap.balances(0) == pytest.approx(swap.balances(1) // rtoken_mul, rel=1e-6) + assert swap.balances(0) == pytest.approx( + swap.balances(1) // rtoken_mul, rel=1e-6 + ) @pytest.fixture(scope="module") @@ -359,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) @@ -376,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) 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 12823ab4..98f220c4 100644 --- a/tests/stableborrow/stabilize/stateful/test_agg_monetary_policy.py +++ b/tests/stableborrow/stabilize/stateful/test_agg_monetary_policy.py @@ -1,6 +1,12 @@ 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 ( @@ -8,15 +14,18 @@ ERC20_MOCK_DEPLOYER, PEG_KEEPER_V2_DEPLOYER, AGGREGATE_STABLE_PRICE3_DEPLOYER, - PEG_KEEPER_REGULATOR_DEPLOYER + PEG_KEEPER_REGULATOR_DEPLOYER, ) from tests.utils.constants import ZERO_ADDRESS + 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) @@ -37,8 +46,12 @@ def __init__(self): self.one_usd = [] self.swaps = [] self.peg_keepers = [] - 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) + 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): @@ -51,8 +64,8 @@ def initializer(self, digits): 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 + 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) @@ -63,7 +76,8 @@ 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): @@ -73,16 +87,17 @@ def add_stablecoin(self, digits): 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) @@ -143,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 06a5924c..a75416a9 100644 --- a/tests/stableborrow/stabilize/unitary/test_agg_monetary_policy_3.py +++ b/tests/stableborrow/stabilize/unitary/test_agg_monetary_policy_3.py @@ -5,7 +5,7 @@ MOCK_FACTORY_DEPLOYER, MOCK_MARKET_DEPLOYER, MOCK_PEG_KEEPER_DEPLOYER, - AGG_MONETARY_POLICY3_DEPLOYER + AGG_MONETARY_POLICY3_DEPLOYER, ) from tests.utils.constants import ZERO_ADDRESS @@ -27,13 +27,13 @@ def mock_peg_keepers(admin, stablecoin): with boa.env.prank(admin): pks = [] for i in range(4): - pk = MOCK_PEG_KEEPER_DEPLOYER.deploy(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) @@ -45,7 +45,8 @@ def mp(mock_factory, mock_peg_keepers, price_oracle, admin): [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): @@ -83,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.eval(f"self.read_debt({controller}, True)", return_type="(uint256, uint256)") + 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.eval(f"self.read_debt({controller}, False)", return_type="(uint256, uint256)") + 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 @@ -110,7 +115,9 @@ def test_add_controllers(mp, mock_factory, admin): additional_ceilings = [10**7, 10**8, 10**9] added_debt = 0 - initial_debt, _ = mp.eval(f"self.get_total_debt({ZERO_ADDRESS})", return_type="(uint256, uint256)") + 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): @@ -120,6 +127,8 @@ def test_add_controllers(mp, mock_factory, admin): controller = mock_factory.controllers(mock_factory.n_collaterals() - 1) added_debt += debt mock_factory.set_debt(controller, debt) - total_debt, debt_for = mp.eval(f"self.get_total_debt({controller})", return_type="(uint256, uint256)") + 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 e7cd0f85..85bc32f9 100644 --- a/tests/stableborrow/stabilize/unitary/test_pk_admin_functions.py +++ b/tests/stableborrow/stabilize/unitary/test_pk_admin_functions.py @@ -1,6 +1,7 @@ import boa from tests.utils.constants import ZERO_ADDRESS + ADMIN_ACTIONS_DEADLINE = 3 * 86400 @@ -16,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: diff --git a/tests/stableborrow/stabilize/unitary/test_pk_delay.py b/tests/stableborrow/stabilize/unitary/test_pk_delay.py index 831e4486..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): diff --git a/tests/stableborrow/stabilize/unitary/test_pk_offboarding.py b/tests/stableborrow/stabilize/unitary/test_pk_offboarding.py index 7e82a1e2..d8aa1382 100644 --- a/tests/stableborrow/stabilize/unitary/test_pk_offboarding.py +++ b/tests/stableborrow/stabilize/unitary/test_pk_offboarding.py @@ -16,8 +16,7 @@ def offboarding(receiver, admin, peg_keepers): # TODO should come from deployers hr = boa.load( - 'contracts/stabilizer/PegKeeperOffboarding.vy', - receiver, admin, admin + "contracts/stabilizer/PegKeeperOffboarding.vy", receiver, admin, admin ) with boa.env.prank(admin): for peg_keeper in peg_keepers: @@ -25,10 +24,19 @@ def offboarding(receiver, admin, peg_keepers): 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 @@ -49,7 +57,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 @@ -60,7 +68,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 @@ -109,4 +117,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 b2c58828..1286df9f 100644 --- a/tests/stableborrow/stabilize/unitary/test_pk_profit.py +++ b/tests/stableborrow/stabilize/unitary/test_pk_profit.py @@ -17,7 +17,7 @@ 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 @@ -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 4455260c..dedd45a0 100644 --- a/tests/stableborrow/stabilize/unitary/test_pk_regulator.py +++ b/tests/stableborrow/stabilize/unitary/test_pk_regulator.py @@ -2,15 +2,15 @@ import pytest from hypothesis import strategies as st, given from tests.utils.deployers import MOCK_PEG_KEEPER_DEPLOYER -from tests.utils.constants import ZERO_ADDRESS + 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) @@ -20,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) @@ -28,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 @@ -61,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 = MOCK_PEG_KEEPER_DEPLOYER.deploy(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) @@ -75,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 @@ -155,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 @@ -168,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(): @@ -187,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 @@ -215,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)", return_type="uint256")) + reg.peg_keepers(i)[0] + for i in range(reg.eval("len(self.peg_keepers)", return_type="uint256")) ] @@ -224,7 +240,8 @@ def preset_peg_keepers(reg, admin, stablecoin): with boa.env.prank(admin): reg.remove_peg_keepers(get_peg_keepers(reg)) return [ - MOCK_PEG_KEEPER_DEPLOYER.deploy((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) ] @@ -244,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) @@ -254,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/test_approval.py b/tests/stableborrow/test_approval.py index 68c57be8..111c5009 100644 --- a/tests/stableborrow/test_approval.py +++ b/tests/stableborrow/test_approval.py @@ -14,7 +14,15 @@ def existing_loan(collateral_token, market_controller, accounts): 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] @@ -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 @@ -142,9 +163,9 @@ def f(sleep_time, user, someone_else): debt = market_controller.max_borrowable(collateral_amount, N) with boa.env.prank(user): 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) + 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) @@ -174,10 +195,14 @@ def f(sleep_time, user, someone_else): 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=30 * 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 203f07b7..2b152a38 100644 --- a/tests/stableborrow/test_bigfuzz.py +++ b/tests/stableborrow/test_bigfuzz.py @@ -4,9 +4,13 @@ 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 from tests.utils.deployers import AMM_DEPLOYER, LL_CONTROLLER_DEPLOYER # Variables and methods to check @@ -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) @@ -94,9 +110,13 @@ def deposit(self, y, n, ratio, uid): 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: @@ -134,8 +154,14 @@ def repay(self, ratio, uid): self.market_controller.repay(amount, user) else: 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)))): + 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: @@ -153,7 +179,11 @@ def add_collateral(self, y, uid): 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() @@ -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 @@ -296,9 +341,13 @@ def self_liquidate_and_health(self, emode, frac): 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(user, 0, frac, self.fake_leverage.address, b'') + 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 @@ -328,32 +377,47 @@ def liquidate(self, uid, luid, emode, frac): if emode == USE_FRACTION: 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(user, 0, frac, self.fake_leverage.address, b'') + 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(user, 0, frac) elif emode == USE_CALLBACKS: - self.stablecoin.transfer(self.fake_leverage.address, self.stablecoin.balanceOf(user)) - self.market_controller.liquidate(user, 0, frac, self.fake_leverage.address, b'') + 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: @@ -363,9 +427,13 @@ def liquidate(self, uid, luid, emode, frac): 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(user, 0, frac, self.fake_leverage.address, b'') + 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 @@ -404,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() @@ -430,20 +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, collateral_digits, stablecoin, price_oracle, - accounts, get_fake_leverage, admin, _tmp): + 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_DEPLOYER.at(market.get_amm(collateral_token.address)) - market_controller = LL_CONTROLLER_DEPLOYER.at(market.get_controller(collateral_token.address)) + market_controller = LL_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) @@ -455,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(): @@ -469,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) @@ -480,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) @@ -501,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(): @@ -511,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(): @@ -522,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(): @@ -537,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(): @@ -548,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(): @@ -560,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(): @@ -576,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(): @@ -600,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(): @@ -620,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(): @@ -632,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(): @@ -644,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(): @@ -655,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() @@ -667,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() @@ -678,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() @@ -689,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() @@ -700,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() @@ -725,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() @@ -748,8 +1044,18 @@ def test_debt_too_high_2_users( def test_cannot_create_loan( - controller_factory, get_market, 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 05baded6..c201836d 100644 --- a/tests/stableborrow/test_create_repay.py +++ b/tests/stableborrow/test_create_repay.py @@ -4,7 +4,15 @@ from hypothesis import strategies as st -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): @@ -17,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"): @@ -28,19 +36,23 @@ 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 p_lim == pytest.approx((p_down * p_up)**0.5 / 1e18, rel=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 @@ -55,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) @@ -72,13 +86,15 @@ def existing_loan(collateral_token, market_controller, accounts): 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 @@ -88,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(): @@ -98,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) @@ -114,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(): @@ -139,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 491c8a8f..d219cfae 100644 --- a/tests/stableborrow/test_create_repay_stateful.py +++ b/tests/stableborrow/test_create_repay_stateful.py @@ -1,10 +1,16 @@ """ 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 @@ -12,8 +18,8 @@ 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,7 +66,7 @@ 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 @@ -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) @@ -118,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 @@ -159,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: @@ -187,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): @@ -200,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(): @@ -216,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 144e8f78..9c1d4b55 100644 --- a/tests/stableborrow/test_extra_health.py +++ b/tests/stableborrow/test_extra_health.py @@ -6,8 +6,16 @@ @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): @@ -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 4b533326..638a0089 100644 --- a/tests/stableborrow/test_factory.py +++ b/tests/stableborrow/test_factory.py @@ -21,7 +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): +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 = LL_CONTROLLER_DEPLOYER.at(controller_factory.get_controller(collateral_token.address)) + controller = LL_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 bdb2a326..b9695e55 100644 --- a/tests/stableborrow/test_leverage.py +++ b/tests/stableborrow/test_leverage.py @@ -6,7 +6,9 @@ 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 @@ -16,20 +18,35 @@ def test_leverage(collateral_token, stablecoin, market_controller, market_amm, f 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)] + 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 assert collateral_token.balanceOf(user) == 0 - assert fake_leverage_balances2[0] - fake_leverage_balances1[0] == amount * 2 * 3000, 5 + 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)] + 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 @@ -46,10 +63,21 @@ def test_leverage(collateral_token, stablecoin, market_controller, market_amm, f 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): @@ -57,15 +85,23 @@ def test_leverage_property(collateral_token, stablecoin, market_controller, mark debt = int(loan_mul * amount * 3000) 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) + 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(amount, debt, 5, user, fake_leverage.address, calldata) + 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 collateral_token.balanceOf(market_amm.address) == pytest.approx(expected_collateral, rel=1e-9, abs=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 xy[1] == pytest.approx(expected_collateral, rel=1e-9, abs=10) @@ -76,38 +112,56 @@ def test_leverage_property(collateral_token, stablecoin, market_controller, mark if (more_debt // 3000) <= collateral_token.balanceOf(fake_leverage.address): if more_debt > 0: boa.deal(collateral_token, user, amount) - market_controller.borrow_more(amount, more_debt, user, fake_leverage.address, calldata) + 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 collateral_token.balanceOf(market_amm.address) == pytest.approx(expected_collateral, rel=1e-9, abs=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 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 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(amount, more_debt, user, fake_leverage.address, calldata) + 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(0, user, market_amm.active_band(), fake_leverage.address, calldata) + market_controller.repay( + 0, user, market_amm.active_band(), fake_leverage.address, calldata + ) else: with boa.reverts(): - market_controller.repay(0, user, market_amm.active_band(), fake_leverage.address, calldata) + 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 @@ -117,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 6afaa34b..23efbc38 100644 --- a/tests/stableborrow/test_liquidate.py +++ b/tests/stableborrow/test_liquidate.py @@ -9,8 +9,16 @@ @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] @@ -21,11 +29,11 @@ def f(sleep_time, discount): monetary_policy.set_rate(int(1e18 * 1.0 / 365 / 86400)) # 100% APY 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) + 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) + collateral_token.approve(market_controller, 2**256 - 1) debt = market_controller.max_borrowable(collateral_amount, N) * 99 // 100 with boa.env.prank(user): @@ -54,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 stablecoin.balanceOf(fee_receiver) == pytest.approx(market_controller.tokens_to_liquidate(user), rel=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): @@ -83,13 +93,28 @@ def test_liquidate(accounts, admin, controller_for_liquidation, market_amm, stab @given(frac=st.integers(min_value=0, max_value=11 * 10**17)) @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,7 +135,7 @@ def test_liquidate_callback(accounts, admin, stablecoin, collateral_token, contr if f != 10**18: with boa.env.prank(fee_receiver): boa.deal(collateral_token, fee_receiver, 10**18) - collateral_token.approve(controller.address, 2**256-1) + 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) @@ -123,7 +148,9 @@ def test_liquidate_callback(accounts, admin, stablecoin, collateral_token, contr try: dy = collateral_token.balanceOf(fee_receiver) - controller.liquidate(user, int(0.999 * f * x / 1e18), frac, fake_leverage.address, b'') + 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: @@ -140,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) @@ -166,7 +197,9 @@ 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] diff --git a/tests/stableborrow/test_lm_callback.py b/tests/stableborrow/test_lm_callback.py index 0e86a3a0..4da1ada8 100644 --- a/tests/stableborrow/test_lm_callback.py +++ b/tests/stableborrow/test_lm_callback.py @@ -12,8 +12,10 @@ def lm_callback(market_amm, market_controller, admin): 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 """ @@ -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 user_amounts[acc] == pytest.approx(market_amm.get_sum_xy(acc)[1], rel=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 55ae5a83..6ffb6863 100644 --- a/tests/stableborrow/test_st_interest_conservation.py +++ b/tests/stableborrow/test_st_interest_conservation.py @@ -1,7 +1,12 @@ 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 @@ -9,22 +14,26 @@ 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,7 +71,7 @@ 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 @@ -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: @@ -119,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) @@ -143,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 @@ -151,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) @@ -184,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: @@ -217,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): @@ -229,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(): @@ -257,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(): @@ -265,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() @@ -287,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) @@ -300,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() @@ -322,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() @@ -338,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/test_math.py b/tests/test_math.py index eadb3251..392c7b56 100644 --- a/tests/test_math.py +++ b/tests/test_math.py @@ -14,7 +14,7 @@ def optimized_math(admin): return OPTIMIZE_MATH_DEPLOYER.deploy() -@given(st.integers(min_value=0, max_value=2**256-1)) +@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) @@ -36,7 +36,7 @@ def test_log2(optimized_math, x): assert abs(y2 / 1e18 - y) <= max(1e-9, 1e-9 * (abs(y) + 1)) -@given(st.integers(min_value=0, max_value=2**256-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: @@ -54,7 +54,7 @@ def test_sqrt(optimized_math, x): assert abs(y2 / 1e18 - y) <= max(1e-15, 1e-15 * y) -@given(st.integers(min_value=0, max_value=2**256-1)) +@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 @@ -62,7 +62,7 @@ def test_halfpow(optimized_math, power): assert abs(pow_int - pow_ideal) < max(5 * 1e10 / 1e18, 5e-16) -@given(st.integers(min_value=-2**255, max_value=2**254-1)) +@given(st.integers(min_value=-(2**255), max_value=2**254 - 1)) @settings(**SETTINGS) def test_exp(optimized_math, power): if power >= 135305999368893231589: @@ -76,7 +76,7 @@ def test_exp(optimized_math, power): assert abs(pow_int - pow_ideal) < max(1e8, pow_ideal * 1e-10) -@given(st.integers(min_value=0, max_value=2**256-1)) +@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: diff --git a/tests/test_packing.py b/tests/test_packing.py index a16a2e79..60315723 100644 --- a/tests/test_packing.py +++ b/tests/test_packing.py @@ -5,7 +5,7 @@ from tests.utils.deployers import TEST_PACKING_DEPLOYER MAX_N = 2**127 - 1 -MIN_N = -2**127 + 1 # <- not -2**127! +MIN_N = -(2**127) + 1 # <- not -2**127! @pytest.fixture(scope="module") @@ -16,7 +16,7 @@ def packing(admin): @given( n1=st.integers(min_value=MIN_N, max_value=MAX_N), - n2=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): diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py index c10e3b9f..ae1ca403 100644 --- a/tests/utils/__init__.py +++ b/tests/utils/__init__.py @@ -8,4 +8,6 @@ def mint_for_testing(token, 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] + return [ + e for e in contract.get_logs(strict=_strict) if type(e).__name__ == event_name + ] diff --git a/tests/utils/constants.py b/tests/utils/constants.py index c28e77b5..2f7dc7d4 100644 --- a/tests/utils/constants.py +++ b/tests/utils/constants.py @@ -9,4 +9,4 @@ SWAD = CONSTANTS_DEPLOYER._constants.SWAD # Constants from Controller.vy -MAX_ORACLE_PRICE_DEVIATION = CONTROLLER_DEPLOYER._constants.MAX_ORACLE_PRICE_DEVIATION \ No newline at end of file +MAX_ORACLE_PRICE_DEVIATION = CONTROLLER_DEPLOYER._constants.MAX_ORACLE_PRICE_DEVIATION diff --git a/tests/utils/deploy.py b/tests/utils/deploy.py index 49921410..c53610c0 100644 --- a/tests/utils/deploy.py +++ b/tests/utils/deploy.py @@ -4,7 +4,11 @@ """ import boa -from boa.contracts.vyper.vyper_contract import VyperDeployer, VyperBlueprint, VyperContract +from boa.contracts.vyper.vyper_contract import ( + VyperDeployer, + VyperBlueprint, + VyperContract, +) from typing import Dict, Any @@ -14,23 +18,17 @@ AMM_DEPLOYER, MINT_CONTROLLER_DEPLOYER, CONTROLLER_FACTORY_DEPLOYER, - # Lending contracts VAULT_DEPLOYER, LL_CONTROLLER_DEPLOYER, LL_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, - - # Testing contracts WETH_DEPLOYER, ERC20_MOCK_DEPLOYER, ) @@ -55,13 +53,11 @@ class Protocol: 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 - ): + + 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) @@ -76,51 +72,47 @@ def __init__( ll_controller=LL_CONTROLLER_DEPLOYER, ll_controller_view=LL_CONTROLLER_VIEW_DEPLOYER, price_oracle=CRYPTO_FROM_POOL_DEPLOYER, - mpolicy=CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER + mpolicy=CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER, ) # Deploy core infrastructure with boa.env.prank(self.admin): # Deploy stablecoin - self.crvUSD = STABLECOIN_DEPLOYER.deploy('Curve USD', 'crvUSD') + 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) - + 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 + 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 + 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 vault implementation self.vault_impl = VAULT_DEPLOYER.deploy() - + # Deploy lending factory self.lending_factory = LENDING_FACTORY_DEPLOYER.deploy( self.blueprints.amm.address, @@ -130,7 +122,7 @@ def __init_lend_markets(self): self.blueprints.ll_controller_view.address, self.blueprints.mpolicy.address, self.admin, - self.fee_receiver + self.fee_receiver, ) def create_mint_market( @@ -142,11 +134,11 @@ def create_mint_market( amm_fee: int, loan_discount: int, liquidation_discount: int, - debt_ceiling: 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 @@ -156,7 +148,7 @@ def create_mint_market( 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 """ @@ -170,17 +162,17 @@ def create_mint_market( loan_discount, liquidation_discount, debt_ceiling, - sender=self.admin + 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': MINT_CONTROLLER_DEPLOYER.at(controller_address), - 'amm': AMM_DEPLOYER.at(amm_address) + "controller": MINT_CONTROLLER_DEPLOYER.at(controller_address), + "amm": AMM_DEPLOYER.at(amm_address), } - + def create_lending_market( self, borrowed_token: VyperContract, @@ -198,7 +190,7 @@ def create_lending_market( ) -> 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 @@ -210,7 +202,7 @@ def create_lending_market( 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%) - + Returns: Dictionary with 'vault', 'controller', 'amm' contracts. """ @@ -225,9 +217,9 @@ def create_lending_market( name, min_borrow_rate, max_borrow_rate, - sender=self.admin + sender=self.admin, ) - + vault = VAULT_DEPLOYER.at(result[0]) controller = LL_CONTROLLER_DEPLOYER.at(result[1]) amm = AMM_DEPLOYER.at(result[2]) @@ -254,15 +246,15 @@ def create_lending_market( vault.deposit(seed_amount) return { - 'vault': vault, - 'controller': controller, - 'amm': amm, + "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( @@ -273,13 +265,13 @@ def create_lending_market( amm_fee=10**16, loan_discount=9 * 10**16, # 9% liquidation_discount=6 * 10**16, # 6% - debt_ceiling=10**6 * 10**18 + 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, @@ -290,5 +282,5 @@ def create_lending_market( 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 + max_borrow_rate=50 * 10**16 // (365 * 86400), # 50% APR ) diff --git a/tests/utils/deployers.py b/tests/utils/deployers.py index 79499de8..fa32b351 100644 --- a/tests/utils/deployers.py +++ b/tests/utils/deployers.py @@ -11,7 +11,10 @@ # Compiler args for different optimization levels # Contracts with #pragma optimize codesize -compiler_args_codesize = {**compiler_args_default, "optimize": OptimizationLevel.CODESIZE} +compiler_args_codesize = { + **compiler_args_default, + "optimize": OptimizationLevel.CODESIZE, +} # Contracts with #pragma optimize gas compiler_args_gas = {**compiler_args_default, "optimize": OptimizationLevel.GAS} @@ -27,105 +30,261 @@ STABLESWAP_NG_PATH = "contracts/testing/stableswap-ng/contracts/main/" # Constants contract (for accessing constants) -CONSTANTS_DEPLOYER = boa.load_partial(BASE_CONTRACT_PATH + "constants.vy", compiler_args=compiler_args_default) +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) +AMM_DEPLOYER = boa.load_partial( + BASE_CONTRACT_PATH + "AMM.vy", compiler_args=compiler_args_default +) # Controller.vy has #pragma optimize codesize -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) +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 +) controller_view_impl = CONTROLLER_VIEW_DEPLOYER.deploy_as_blueprint() # MintController.vy has #pragma optimize codesize # view_impl address has to be set in MintController.vy with open(BASE_CONTRACT_PATH + "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_impl.address},", 1 + "empty(address), # to replace at deployment with view blueprint", + f"{controller_view_impl.address},", + 1, ) assert f"{controller_view_impl.address}," in mint_controller_code -MINT_CONTROLLER_DEPLOYER = boa.loads_partial(mint_controller_code, 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) +MINT_CONTROLLER_DEPLOYER = boa.loads_partial( + mint_controller_code, 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) -LL_CONTROLLER_DEPLOYER = boa.load_partial(LENDING_CONTRACT_PATH + "LLController.vy", compiler_args=compiler_args_codesize) -LL_CONTROLLER_VIEW_DEPLOYER = boa.load_partial(LENDING_CONTRACT_PATH + "LLControllerView.vy", compiler_args=compiler_args_default) -LENDING_FACTORY_DEPLOYER = boa.load_partial(LENDING_CONTRACT_PATH + "LendingFactory.vy", compiler_args=compiler_args_codesize) +VAULT_DEPLOYER = boa.load_partial( + LENDING_CONTRACT_PATH + "Vault.vy", compiler_args=compiler_args_codesize +) +LL_CONTROLLER_DEPLOYER = boa.load_partial( + LENDING_CONTRACT_PATH + "LLController.vy", compiler_args=compiler_args_codesize +) +LL_CONTROLLER_VIEW_DEPLOYER = boa.load_partial( + LENDING_CONTRACT_PATH + "LLControllerView.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) +FLASH_LENDER_DEPLOYER = boa.load_partial( + FLASHLOAN_CONTRACT_PATH + "FlashLender.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) +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) +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) +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) +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) +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_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, +) # Callback contracts -LM_CALLBACK_DEPLOYER = boa.load_partial(BASE_CONTRACT_PATH + "LMCallback.vy", compiler_args=compiler_args_default) +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) -VE_DELEGATION_MOCK_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "VEDelegationMock.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) -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) -OPTIMIZE_MATH_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "OptimizeMath.vy", compiler_args=compiler_args_default) -TEST_PACKING_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "TestPacking.vy", compiler_args=compiler_args_default) -OLD_AMM_DEPLOYER = boa.load_partial(TESTING_CONTRACT_PATH + "OldAMM.vy", compiler_args=compiler_args_default) +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 +) +VE_DELEGATION_MOCK_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "VEDelegationMock.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 +) +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 +) +OPTIMIZE_MATH_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "OptimizeMath.vy", compiler_args=compiler_args_default +) +TEST_PACKING_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "TestPacking.vy", compiler_args=compiler_args_default +) +OLD_AMM_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "OldAMM.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) +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) +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) +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) +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/zaps/conftest.py b/tests/zaps/conftest.py index b1df4e94..f2791e1a 100644 --- a/tests/zaps/conftest.py +++ b/tests/zaps/conftest.py @@ -9,4 +9,4 @@ def amm_A(): @pytest.fixture(scope="module") def seed_liquidity(): # Match the seeding used by these tests (assumes 18 decimals) - return 100 * 10**6 * 10 ** 18 + return 100 * 10**6 * 10**18 diff --git a/tests/zaps/partial_liquidation/calculations/calculations.py b/tests/zaps/partial_liquidation/calculations/calculations.py index efeff874..50a65174 100644 --- a/tests/zaps/partial_liquidation/calculations/calculations.py +++ b/tests/zaps/partial_liquidation/calculations/calculations.py @@ -508,9 +508,11 @@ def test_position(position, frac): controller_address = position[1] user = position[2] - block = json.loads(Web3.to_json(Web3(HTTPProvider(url)).eth.get_block(block_number))) + block = json.loads( + Web3.to_json(Web3(HTTPProvider(url)).eth.get_block(block_number)) + ) - coin = f"ethereum:0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + 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"] @@ -531,7 +533,9 @@ def test_position(position, frac): f"stablecoin: {user_state[1] / 1e18}, debt: {user_state[2] / 1e18}, ratio: {ratio}" ) - stablecoin = boa.load_partial("contracts/Stablecoin.vy").at("0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E") + 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) @@ -580,7 +584,12 @@ def test_position(position, frac): for position in positions: try: - res += test_position(position, 0.05) + "\n" + "-----------------------------" + "\n" + res += ( + test_position(position, 0.05) + + "\n" + + "-----------------------------" + + "\n" + ) except KeyboardInterrupt: break except: diff --git a/tests_forked/price_oracles/conftest.py b/tests_forked/price_oracles/conftest.py index ef99e75c..5f087110 100644 --- a/tests_forked/price_oracles/conftest.py +++ b/tests_forked/price_oracles/conftest.py @@ -5,13 +5,20 @@ @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" + 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 + return boa.from_etherscan( + "0x18672b1b0c623a30089A280Ed9256379fb0E4E62", + "AggregatorStablePrice", + uri=EXPLORER_URL, + api_key=EXPLORER_TOKEN, + ) # USD/crvUSD @pytest.fixture(scope="module") @@ -27,29 +34,41 @@ def trader(): @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() + 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() + 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') + 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) + 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) + 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/test_lp_oracle_compare_to_spot.py b/tests_forked/price_oracles/test_lp_oracle_compare_to_spot.py index 07476a38..affb2210 100644 --- a/tests_forked/price_oracles/test_lp_oracle_compare_to_spot.py +++ b/tests_forked/price_oracles/test_lp_oracle_compare_to_spot.py @@ -6,16 +6,47 @@ 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 + 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 + 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 --- @@ -24,20 +55,49 @@ def test_tricrypto_usdc(lp_oracle_factory, stablecoin_aggregator, admin, trader) 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) + 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]] + 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) + 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) + 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 @@ -45,53 +105,135 @@ def test_tricrypto_usdc(lp_oracle_factory, stablecoin_aggregator, admin, trader) 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) + 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 + 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 + 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 + 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 + 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)) + 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]] + 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) + 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) + 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 @@ -99,26 +241,56 @@ def test_tricrypto_usdt(lp_oracle_factory, stablecoin_aggregator, admin, trader) 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) + 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 + 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 + 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) + 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 + 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 --- @@ -130,16 +302,43 @@ def test_tricrv(lp_oracle_factory, stablecoin_aggregator, admin, trader): 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]] + 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) + 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: + 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) @@ -148,51 +347,134 @@ def test_tricrv(lp_oracle_factory, stablecoin_aggregator, admin, trader): lp_oracle_price_usd = tricrv_usd_lp_oracle.price_w() # Spot price - crvusd_from_lp = tricrv_pool.calc_withdraw_one_coin(10 ** 18, 0) + 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 + 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 + 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 + 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 + 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) + 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]] + 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) + 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) + strategic_reserve_pool.exchange( + 0, 1, usdc.balanceOf(strategic_reserve_pool) // 500, 0 + ) boa.env.time_travel(3600) # Oracle price @@ -205,10 +487,28 @@ def test_strategic_reserve(lp_oracle_factory, stablecoin_aggregator, admin, trad 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 + 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): @@ -216,39 +516,97 @@ def test_weeth_weth(lp_oracle_factory, stablecoin_aggregator, admin, trader): 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 + 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 + 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)) + 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) + 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) + 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]] + 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) @@ -263,32 +621,73 @@ def test_weeth_weth(lp_oracle_factory, stablecoin_aggregator, admin, trader): 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) + 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 + 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 + 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 + 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 + 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 --- @@ -298,10 +697,35 @@ def test_cvxcrv(lp_oracle_factory, stablecoin_aggregator, admin, trader): 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]] + 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) @@ -316,12 +740,30 @@ def test_cvxcrv(lp_oracle_factory, stablecoin_aggregator, admin, trader): 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) + 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 + 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 + 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_forked_ape/conftest.py b/tests_forked_ape/conftest.py index cd784bd6..65f71f47 100644 --- a/tests_forked_ape/conftest.py +++ b/tests_forked_ape/conftest.py @@ -21,7 +21,9 @@ def pytest_configure(): 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.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% @@ -33,7 +35,7 @@ def pytest_configure(): 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" @@ -42,7 +44,7 @@ def pytest_configure(): pytest.new_id_created = False pytest.max_id_before = 0 pytest.handler_index = None - + # record stablecoin address: pytest.stablecoin = pytest.ZERO_ADDRESS @@ -70,13 +72,17 @@ def weth(): @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) + 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) + _stablecoin = forked_admin.deploy( + project.Stablecoin, pytest.FULL_NAME, pytest.SHORT_NAME + ) pytest.stablecoin = _stablecoin.address return _stablecoin @@ -154,39 +160,42 @@ def stableswap_impl(project, forked_admin, stableswap_factory, owner_proxy): @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: - + + 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), + 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 + 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): +def rtokens_pools( + project, forked_admin, owner_proxy, stablecoin, stableswap_impl, stableswap_factory +): pools = {} # Deploy pools @@ -204,7 +213,9 @@ def rtokens_pools(project, forked_admin, owner_proxy, stablecoin, stableswap_imp ) # 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) + pool = project.Stableswap.at( + tx.events.filter(stableswap_factory.PlainPoolDeployed)[0].pool + ) pools[name] = pool return pools @@ -218,57 +229,64 @@ def factory_handler(project, stableswap_factory, 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) + 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 + 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): +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( @@ -318,7 +336,9 @@ def chainlink_aggregator(): @pytest.fixture(scope="module", autouse=True) -def price_oracle_with_chainlink(project, forked_admin, rtokens_pools, agg_stable_price, chainlink_aggregator): +def price_oracle_with_chainlink( + project, forked_admin, rtokens_pools, agg_stable_price, chainlink_aggregator +): return forked_admin.deploy( project.CryptoWithStablePriceAndChainlink, pytest.TRICRYPTO, @@ -327,5 +347,5 @@ def price_oracle_with_chainlink(project, forked_admin, rtokens_pools, agg_stable agg_stable_price, chainlink_aggregator.address, 600, - 1 + 1, ) diff --git a/tests_forked_ape/test_lendborrow.py b/tests_forked_ape/test_lendborrow.py index 080634d0..8db199c6 100644 --- a/tests_forked_ape/test_lendborrow.py +++ b/tests_forked_ape/test_lendborrow.py @@ -5,7 +5,14 @@ class TestLendAndSwaps: @pytest.fixture() - def factory_with_market(self, forked_admin, controller_factory, weth, price_oracle_with_chainlink, policy): + def factory_with_market( + self, + forked_admin, + controller_factory, + weth, + price_oracle_with_chainlink, + policy, + ): controller_factory.add_market( weth, 100, @@ -20,22 +27,44 @@ def factory_with_market(self, forked_admin, controller_factory, weth, price_orac ) @pytest.fixture() - def stablecoin_lend(self, project, forked_user, controller_factory, factory_with_market, weth, stablecoin): + 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 + 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): + 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 + 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 @@ -62,7 +91,9 @@ def test_create_loan_fails( 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), + int( + 4 * coin_multiplier * pytest.initial_pool_coin_balance * 10**18 + ), 30, value=weth_amount, ) @@ -70,10 +101,20 @@ def test_create_loan_fails( 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() + 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 + self, + project, + forked_admin, + forked_user, + controller_factory, + stablecoin_lend, + stablecoin, + weth, ): controller = project.Controller.at(controller_factory.controllers(0)) @@ -81,13 +122,20 @@ def test_controller( 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[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 + 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 @@ -114,20 +162,31 @@ def test_controller( <= 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 + 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) + 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 + 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) + 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 @@ -163,7 +222,9 @@ def rtokens_pools_with_liquidity( return rtokens_pools - def test_stableswap_liquidity(self, forked_user, rtokens_pools_with_liquidity, stablecoin): + def test_stableswap_liquidity( + self, forked_user, rtokens_pools_with_liquidity, stablecoin + ): for pool in rtokens_pools_with_liquidity.values(): n_coins = 2 addresses = [] @@ -172,25 +233,51 @@ def test_stableswap_liquidity(self, forked_user, rtokens_pools_with_liquidity, s addresses.append(addr) coin = stablecoin if addr == stablecoin.address else Contract(addr) - assert pool.balances(n) == self.pool_coin_balance * 10 ** coin.decimals() + 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): + 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 + 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 + 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 + 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 + self, + project, + accounts, + forked_admin, + controller_factory, + rtokens_pools_with_liquidity, + stablecoin, ): user = accounts[3] lend_amount = 1000 @@ -198,14 +285,18 @@ def test_full_repay( 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 + 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) + 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 diff --git a/tests_forked_ape/test_registry_integration.py b/tests_forked_ape/test_registry_integration.py index 6fca575f..fcd88278 100644 --- a/tests_forked_ape/test_registry_integration.py +++ b/tests_forked_ape/test_registry_integration.py @@ -2,9 +2,11 @@ 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.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 @@ -13,12 +15,13 @@ def test_factory_handler_integration(metaregistry, stableswap_factory, factory_h 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) + 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 index 8e3968f3..2c6ff580 100644 --- a/tests_forked_ape/utils.py +++ b/tests_forked_ape/utils.py @@ -10,7 +10,12 @@ def deploy_test_blueprint(project: Project, contract: Contract, account): 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 + 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, @@ -25,7 +30,9 @@ def deploy_test_blueprint(project: Project, contract: Contract, account): return receipt.contract_address -def mint_tokens_for_testing(project: Project, account, stablecoin_amount: int, eth_amount: int): +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 @@ -64,7 +71,9 @@ def mint_tokens_for_testing(project: Project, account, stablecoin_amount: int, e token_contract = Contract("0x0000000000085d4780B73119b644AE5ecd22b376") # apply proxy try: - token_impl = ContractContainer(Contract(token_contract.implementation()).contract_type) + token_impl = ContractContainer( + Contract(token_contract.implementation()).contract_type + ) token_contract = token_impl.at("0x0000000000085d4780B73119b644AE5ecd22b376") except AttributeError: # already applied diff --git a/tests_leverage/test_v1/conftest.py b/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 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 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 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 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 index 32b60760..830ec574 100644 --- a/tests_leverage/test_v1/utils.py +++ b/tests_leverage/test_v1/utils.py @@ -19,9 +19,11 @@ def mint_tokens_for_testing(project: Project, account): # tBTC token_contract = Contract("0x18084fbA666a33d37592fA2633fD49a74DD93a88") - token_owner = "0x3ee18B2214AFF97000D974cf647E7C347E8fa585" # Wormhole: Portal Token Bridge + token_owner = ( + "0x3ee18B2214AFF97000D974cf647E7C347E8fa585" # Wormhole: Portal Token Bridge + ) project.provider.set_balance(token_owner, 10**20) - amount = 100 * 10 ** 18 + amount = 100 * 10**18 token_contract.transfer(account, amount, sender=accounts[token_owner]) assert token_contract.balanceOf(account.address) >= amount @@ -42,7 +44,7 @@ def mint_tokens_for_testing(project: Project, account): token_contract = Contract("0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0") token_owner = "0x0B925eD163218f6662a35e0f0371Ac234f9E9371" project.provider.set_balance(token_owner, 10**20) - amount = 1000 * 10 ** 18 + amount = 1000 * 10**18 token_contract.transfer(account, amount, sender=accounts[token_owner]) assert token_contract.balanceOf(account.address) >= amount @@ -59,7 +61,7 @@ def mint_crvusd_tokens_for_testing(project: Project, account): token_contract = Contract("0xf939e0a03fb07f59a73314e73794be0e57ac1b4e") token_owner = "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635" # WETH controller project.provider.set_balance(token_owner, 10**20) - amount = 100_000_000 * 10 ** 18 + amount = 100_000_000 * 10**18 token_contract.transfer(account, amount, sender=accounts[token_owner]) assert token_contract.balanceOf(account.address) >= amount @@ -120,20 +122,20 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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": { @@ -141,20 +143,20 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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": { @@ -162,20 +164,20 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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": { @@ -183,20 +185,20 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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": { @@ -204,20 +206,20 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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", ], }, }, @@ -227,20 +229,20 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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": { @@ -248,20 +250,20 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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": { @@ -269,20 +271,20 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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": { @@ -290,20 +292,20 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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": { @@ -311,20 +313,20 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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", ], }, }, @@ -334,20 +336,20 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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": { @@ -355,20 +357,20 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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": { @@ -376,20 +378,20 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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": { @@ -397,20 +399,20 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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": { @@ -418,20 +420,20 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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", ], }, }, @@ -441,20 +443,20 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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": { @@ -462,20 +464,20 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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": { @@ -483,20 +485,20 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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": { @@ -504,20 +506,20 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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": { @@ -525,20 +527,20 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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", ], }, }, @@ -548,23 +550,29 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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": { @@ -572,23 +580,29 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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": { @@ -596,23 +610,29 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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": { @@ -620,23 +640,29 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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": { @@ -644,23 +670,29 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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", ], }, }, @@ -669,24 +701,30 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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": { @@ -694,23 +732,29 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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": { @@ -718,23 +762,29 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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": { @@ -742,23 +792,29 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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": { @@ -766,23 +822,29 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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", ], }, }, @@ -794,95 +856,119 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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"], ], }, @@ -890,23 +976,29 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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"], ], }, @@ -916,95 +1008,119 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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"], ], }, @@ -1012,23 +1128,29 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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"], ], }, @@ -1038,120 +1160,150 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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", ], }, }, @@ -1160,120 +1312,150 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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", ], }, }, @@ -1282,95 +1464,119 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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"], ], }, @@ -1378,23 +1584,29 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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"], ], }, @@ -1404,121 +1616,151 @@ def mint_crvusd_tokens_for_testing(project: Project, account): "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", ], }, - } + }, } diff --git a/tests_leverage/test_v2/conftest.py b/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/tests/test_leverage.py b/tests_leverage/test_v2/tests/test_leverage.py index 4d0d160d..0791c0c4 100644 --- a/tests_leverage/test_v2/tests/test_leverage.py +++ b/tests_leverage/test_v2/tests/test_leverage.py @@ -1,16 +1,32 @@ class TestLeverage: class TestCreateLoan: - def test_max_borrowable(self, alice, collateral, controller, leverage_zap_1inch, router_api_1inch): + 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) + 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_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) + 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) + 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 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: From 3e9e74948b0e22f18a3649a04b697915dc8b5cb6 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 15 Sep 2025 15:15:08 +0200 Subject: [PATCH 227/413] style: add pre-commit hooks --- .git-blame-ignore-revs | 1 + .pre-commit-config.yaml | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 .git-blame-ignore-revs create mode 100644 .pre-commit-config.yaml diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..a916178c --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1 @@ +05e812182aa1267beca1b619d5882f4eb917e423 \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..8d401bdb --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,22 @@ +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 \ No newline at end of file From c3bd764b265587fb4d31395b68019461131d3b31 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 16 Sep 2025 11:58:20 +0400 Subject: [PATCH 228/413] test: override seed_liquidity=0 for test_vault --- tests/lending/test_vault.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/lending/test_vault.py b/tests/lending/test_vault.py index 033ff63e..653be755 100644 --- a/tests/lending/test_vault.py +++ b/tests/lending/test_vault.py @@ -9,6 +9,12 @@ DEAD_SHARES = 1000 +@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 From 5eba85d2f8dae836b5284e69392125eba40a1731 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 16 Sep 2025 11:59:36 +0400 Subject: [PATCH 229/413] test: fix repay(0) --- tests/lm_callback/test_st_lm_callback.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/lm_callback/test_st_lm_callback.py b/tests/lm_callback/test_st_lm_callback.py index 772e65d5..a02399da 100644 --- a/tests/lm_callback/test_st_lm_callback.py +++ b/tests/lm_callback/test_st_lm_callback.py @@ -105,6 +105,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 From a968ff9309c55a46a40f33f89301e01137ea3e8e Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 16 Sep 2025 12:02:17 +0400 Subject: [PATCH 230/413] test: override seed_liquidity=0 for test_vault (again) --- tests/lending/test_vault.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/lending/test_vault.py b/tests/lending/test_vault.py index 942cfa99..6f554226 100644 --- a/tests/lending/test_vault.py +++ b/tests/lending/test_vault.py @@ -14,6 +14,12 @@ DEAD_SHARES = 1000 +@pytest.fixture(scope="module") +def seed_liquidity(): + """Override to 0""" + return 0 + + def test_vault_creation( vault, controller, From 98babff98b3e4057543787e84895dde5f4534f53 Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 16 Sep 2025 14:52:10 +0200 Subject: [PATCH 231/413] fix: wrong arg passed to liq lib --- contracts/ControllerView.vy | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/contracts/ControllerView.vy b/contracts/ControllerView.vy index 1e2cb1e8..18b325f9 100644 --- a/contracts/ControllerView.vy +++ b/contracts/ControllerView.vy @@ -212,7 +212,15 @@ def users_to_liquidate( """ @notice Natspec for this function is available in its controller contract """ - return liq.users_with_health(IController(self), _from, _limit, 0, False, empty(address), True) + return liq.users_with_health( + CONTROLLER, + _from, + _limit, + 0, + False, + empty(address), + True, + ) @view From 62b2e952d7149a780b805c229484158f1a424ff3 Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 16 Sep 2025 14:56:45 +0200 Subject: [PATCH 232/413] test: initial stateful tests rewrite (wip) --- tests/stateful/test_controller_stateful.py | 363 +++++++++++++++++++++ tests/utils/constants.py | 16 +- tests/utils/deploy.py | 31 +- 3 files changed, 403 insertions(+), 7 deletions(-) create mode 100644 tests/stateful/test_controller_stateful.py diff --git a/tests/stateful/test_controller_stateful.py b/tests/stateful/test_controller_stateful.py new file mode 100644 index 00000000..5a2f5a2c --- /dev/null +++ b/tests/stateful/test_controller_stateful.py @@ -0,0 +1,363 @@ +from decimal import Decimal +from hypothesis import note, assume +from hypothesis.strategies import composite, integers, builds, decimals, SearchStrategy, data, sampled_from +from hypothesis.stateful import RuleBasedStateMachine, initialize, rule, precondition, invariant + +import boa + +from tests.utils.deploy import Protocol +from tests.utils.deployers import AMM_DEPLOYER, ERC20_MOCK_DEPLOYER, STABLECOIN_DEPLOYER +from tests.utils.constants import ( + MAX_TICKS, + MAX_UINT256, + WAD, + MIN_A, + MAX_A, + MIN_FEE, + MAX_FEE, + MAX_LOAN_DISCOUNT, + MIN_LIQUIDATION_DISCOUNT, + MIN_TICKS, +) + + +# Debt ceiling has no explicit on-chain limit; choose a realistic test bound +DEBT_CEILING_MAX = 10**8 * 10**18 + +# ---------------- simple parameter strategies ---------------- +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 Protocol using Hypothesis builds +protocols = builds(Protocol, 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 + +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 ---------------- +@composite +def mint_markets( + draw, + As=As, + amm_fees=amm_fees, + discounts=discounts(), + debt_ceilings=debt_ceilings, + initial_prices=prices, +): + """Creates a Protocol and a mint market with fuzzed parameters. + Custom strategies can be passed to override defaults. + Returns a dict with proto, controller, amm, collateral_token, and params. + """ + _A = draw(As) + _fee = draw(amm_fees) + _loan_discount, _liq_discount = draw(discounts) + _dc = draw(debt_ceilings) + _price = draw(initial_prices) + + # Deploy protocol with initial oracle price + proto = Protocol(initial_price=_price) + + # Fresh collateral token (via strategy built from decimals if not provided) + _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 + +# ------------ controller interaction params ------------- + +# TODO eventually fix this in SC +ticks = integers(min_value=MIN_TICKS + 1, max_value=MAX_TICKS) + + +# ===================== +# Stateful skeleton (to be expanded) +# ===================== + + +class ControllerStateful(RuleBasedStateMachine): + @initialize(market=mint_markets()) + def initialize_protocol(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()) + + + @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 + + # 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): + underwater_positions = self.controller.users_to_liquidate(0, len(self.users)) + + self.controller.users_to_liquidate(0, len(self.users)) + for pos in underwater_positions: + note("[HARD LIQUIDATE]") + note(f"liquidating user {pos.user} with health {self.controller.health(pos.user, True)}") + # Choose a fraction so that debt portion to repay fits into x (avoid external funding) + frac = 1 + if pos.debt > 0: + # Ceil division guard in contract: debt' = (debt * frac + WAD - 1) // WAD + num = pos.x * WAD - (WAD - 1) + if num > 0: + frac = max(1, min(WAD, num // pos.debt)) + self.controller.liquidate(pos.user, 0, frac) + + # After liquidation there should be no more liquidatable users + assert len(self.controller.users_to_liquidate(0, 1000)) == 0 + + # Keep our local tracking list in sync with on-chain open loans + n = self.controller.n_loans() + self.users = [self.controller.loans(i) for i in range(n)] + +TestControllerStateful = ControllerStateful.TestCase \ No newline at end of file diff --git a/tests/utils/constants.py b/tests/utils/constants.py index 2f7dc7d4..6e3a5389 100644 --- a/tests/utils/constants.py +++ b/tests/utils/constants.py @@ -1,5 +1,9 @@ import boa -from tests.utils.deployers import CONSTANTS_DEPLOYER, CONTROLLER_DEPLOYER +from tests.utils.deployers import ( + CONSTANTS_DEPLOYER, + CONTROLLER_DEPLOYER, + LENDING_FACTORY_DEPLOYER, +) ZERO_ADDRESS = boa.eval("empty(address)") MAX_UINT256 = boa.eval("max_value(uint256)") @@ -8,5 +12,13 @@ WAD = CONSTANTS_DEPLOYER._constants.WAD SWAD = CONSTANTS_DEPLOYER._constants.SWAD -# Constants from Controller.vy 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 diff --git a/tests/utils/deploy.py b/tests/utils/deploy.py index c53610c0..d7d9b4a3 100644 --- a/tests/utils/deploy.py +++ b/tests/utils/deploy.py @@ -16,8 +16,9 @@ # Core contracts STABLECOIN_DEPLOYER, AMM_DEPLOYER, - MINT_CONTROLLER_DEPLOYER, CONTROLLER_FACTORY_DEPLOYER, + CONTROLLER_VIEW_DEPLOYER, + # Lending contracts VAULT_DEPLOYER, LL_CONTROLLER_DEPLOYER, @@ -31,6 +32,8 @@ CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER, WETH_DEPLOYER, ERC20_MOCK_DEPLOYER, + # Compiler flags + compiler_args_codesize, ) @@ -47,7 +50,7 @@ def __init__(self, **deployers: VyperDeployer): mpolicy: VyperBlueprint -# TODO rename to Llamalend +# TODO rename to this class to Llamalend and the file to protocol.py class Protocol: """ Protocol deployment and management class for llamalend. @@ -65,10 +68,28 @@ def __init__(self, initial_price: int = 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=MINT_CONTROLLER_DEPLOYER, + mint_controller=self._mint_controller_deployer, ll_controller=LL_CONTROLLER_DEPLOYER, ll_controller_view=LL_CONTROLLER_VIEW_DEPLOYER, price_oracle=CRYPTO_FROM_POOL_DEPLOYER, @@ -169,8 +190,8 @@ def create_mint_market( amm_address = self.mint_factory.get_amm(collateral_token.address) return { - "controller": MINT_CONTROLLER_DEPLOYER.at(controller_address), - "amm": AMM_DEPLOYER.at(amm_address), + 'controller': self._mint_controller_deployer.at(controller_address), + 'amm': AMM_DEPLOYER.at(amm_address) } def create_lending_market( From 1d04fdf411d5c672cbbb793098d05a1dee7bddce Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 16 Sep 2025 15:15:03 +0200 Subject: [PATCH 233/413] test: improve stateful liquidation logic --- tests/stateful/test_controller_stateful.py | 32 ++++++++++------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/tests/stateful/test_controller_stateful.py b/tests/stateful/test_controller_stateful.py index 5a2f5a2c..553e284c 100644 --- a/tests/stateful/test_controller_stateful.py +++ b/tests/stateful/test_controller_stateful.py @@ -210,6 +210,7 @@ def initialize_protocol(self, market): 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()) @@ -338,26 +339,21 @@ def open_users_invariant(self): @precondition(lambda self: len(self.users) > 0) @invariant() def liquidate(self): - underwater_positions = self.controller.users_to_liquidate(0, len(self.users)) + positions = self.controller.users_to_liquidate(0, len(self.users)) + if len(positions) == 0: + return - self.controller.users_to_liquidate(0, len(self.users)) - for pos in underwater_positions: - note("[HARD LIQUIDATE]") + note("[HARD LIQUIDATE]") + for pos in positions: note(f"liquidating user {pos.user} with health {self.controller.health(pos.user, True)}") - # Choose a fraction so that debt portion to repay fits into x (avoid external funding) - frac = 1 - if pos.debt > 0: - # Ceil division guard in contract: debt' = (debt * frac + WAD - 1) // WAD - num = pos.x * WAD - (WAD - 1) - if num > 0: - frac = max(1, min(WAD, num // pos.debt)) - self.controller.liquidate(pos.user, 0, frac) - - # After liquidation there should be no more liquidatable users - assert len(self.controller.users_to_liquidate(0, 1000)) == 0 - - # Keep our local tracking list in sync with on-chain open loans + 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 \ No newline at end of file +TestControllerStateful = ControllerStateful.TestCase From 6a7c799c3c25716add4cc8001308909080d767e3 Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 16 Sep 2025 15:25:56 +0200 Subject: [PATCH 234/413] test: legacy stateful test xfail --- tests/lending/test_bigfuzz.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/lending/test_bigfuzz.py b/tests/lending/test_bigfuzz.py index d4af49b2..87f9cc1b 100644 --- a/tests/lending/test_bigfuzz.py +++ b/tests/lending/test_bigfuzz.py @@ -1,4 +1,5 @@ import boa +import pytest from math import log2, ceil from boa import BoaError from hypothesis import settings @@ -13,6 +14,9 @@ from tests.utils.constants import ZERO_ADDRESS +pytestmark = pytest.mark.xfail(strict=True, reason="stateful fuzz currently unstable") + + # Variables and methods to check # * A From dc492d227e370c908fda3e72f76811402bfec0f0 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 16 Sep 2025 17:31:36 +0400 Subject: [PATCH 235/413] refactor: rename stablecoin -> borrowed --- contracts/AMM.vy | 26 +++---- contracts/Controller.vy | 72 +++++++++---------- contracts/interfaces/ILlamalendController.vyi | 2 +- contracts/interfaces/IMintController.vyi | 4 +- contracts/lending/LLController.vy | 4 +- contracts/lending/LLControllerView.vy | 2 +- contracts/zaps/LeverageZap.vy | 8 +-- 7 files changed, 59 insertions(+), 59 deletions(-) diff --git a/contracts/AMM.vy b/contracts/AMM.vy index 3e72d645..87914128 100644 --- a/contracts/AMM.vy +++ b/contracts/AMM.vy @@ -458,9 +458,9 @@ def p_oracle_down(n: int256) -> uint256: 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 @@ -488,7 +488,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 """ @@ -749,7 +749,7 @@ 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 @@ -1352,9 +1352,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) @@ -1499,7 +1499,7 @@ def get_y_up(user: address) -> uint256: @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 """ @@ -1509,10 +1509,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] = [] @@ -1548,9 +1548,9 @@ def _get_xy(user: address, is_sum: bool) -> DynArray[uint256, MAX_TICKS_UINT][2] @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]] @@ -1560,9 +1560,9 @@ def get_sum_xy(user: address) -> uint256[2]: @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) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index a7c7c8b8..eff13cb2 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -534,7 +534,7 @@ def max_borrowable( @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 + @return Maximum amount of borrowed asset to borrow """ return staticcall self._view.max_borrowable( collateral, N, current_debt, user @@ -596,7 +596,7 @@ def execute_callback( callbacker: address, callback_sig: bytes4, user: address, - stablecoins: uint256, + borrowed: uint256, collateral: uint256, debt: uint256, calldata: Bytes[CALLDATA_MAX_SIZE], @@ -614,11 +614,11 @@ def execute_callback( callbacker, concat( callback_sig, - abi_encode(user, stablecoins, collateral, debt, calldata), + 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 @@ -716,9 +716,9 @@ def create_loan( calldata: Bytes[CALLDATA_MAX_SIZE] = b"", ): """ - @notice Create loan but pass stablecoin to a callback first so that it can build leverage + @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 @@ -840,9 +840,9 @@ def borrow_more( calldata: Bytes[CALLDATA_MAX_SIZE] = b"", ): """ - @notice Borrow more stablecoins while adding more collateral using a callback (to leverage more) + @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 @param callbacker Address of the callback contract @param calldata Any data for callbacker @@ -947,38 +947,38 @@ def repay( callbacker, CALLBACK_REPAY, _for, xy[0], xy[1], debt, calldata ) - total_stablecoins: uint256 = _d_debt + xy[0] + cb.stablecoins - assert total_stablecoins > 0 # dev: no coins to repay + total_borrowed: uint256 = _d_debt + xy[0] + cb.borrowed + assert total_borrowed > 0 # dev: no coins to repay d_debt: uint256 = 0 - # If we have more stablecoins than the debt - full repayment and closing the position - if total_stablecoins >= debt: + # If we have more borrowed tokens than the debt - full repayment and closing the position + if total_borrowed >= debt: d_debt = debt debt = 0 if callbacker == empty(address): xy = extcall AMM.withdraw(_for, WAD) - total_stablecoins = 0 + total_borrowed = 0 if xy[0] > 0: # Only allow full repayment when underwater for the sender to do assert approval self.transferFrom(BORROWED_TOKEN, AMM.address, self, xy[0]) - total_stablecoins += xy[0] - if cb.stablecoins > 0: - self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins) - total_stablecoins += cb.stablecoins - if total_stablecoins < d_debt: + total_borrowed += xy[0] + if cb.borrowed > 0: + self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.borrowed) + total_borrowed += cb.borrowed + if total_borrowed < d_debt: _d_debt_effective: uint256 = unsafe_sub( - d_debt, xy[0] + cb.stablecoins + d_debt, xy[0] + cb.borrowed ) # <= _d_debt self.transferFrom( BORROWED_TOKEN, msg.sender, self, _d_debt_effective ) - total_stablecoins += _d_debt_effective + total_borrowed += _d_debt_effective - if total_stablecoins > d_debt: + if total_borrowed > d_debt: self.transfer( - BORROWED_TOKEN, _for, unsafe_sub(total_stablecoins, d_debt) + BORROWED_TOKEN, _for, unsafe_sub(total_borrowed, d_debt) ) # Transfer collateral to _for if callbacker == empty(address): @@ -1001,7 +1001,7 @@ def repay( active_band: int256 = staticcall AMM.active_band_with_skip() assert active_band <= max_active_band - d_debt = total_stablecoins + d_debt = total_borrowed debt = unsafe_sub(debt, d_debt) ns: int256[2] = staticcall AMM.read_user_tick_numbers(_for) size: int256 = unsafe_sub(ns[1], ns[0]) @@ -1035,8 +1035,8 @@ def repay( # full = False to make this condition non-manipulatable (and also cheaper on gas) assert self._health(_for, debt, False, liquidation_discount) > 0 - if cb.stablecoins > 0: - self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins) + if cb.borrowed > 0: + self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.borrowed) if _d_debt > 0: self.transferFrom(BORROWED_TOKEN, msg.sender, self, _d_debt) @@ -1155,7 +1155,7 @@ def liquidate( ): """ @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 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 calldata Any data for callbacker @@ -1179,7 +1179,7 @@ def liquidate( 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. @@ -1217,13 +1217,13 @@ def liquidate( debt, calldata, ) - assert cb.stablecoins >= to_repay, "no enough proceeds" - if cb.stablecoins > to_repay: + assert cb.borrowed >= to_repay, "no enough proceeds" + if cb.borrowed > to_repay: self.transferFrom( BORROWED_TOKEN, callbacker, msg.sender, - unsafe_sub(cb.stablecoins, to_repay), + unsafe_sub(cb.borrowed, to_repay), ) self.transferFrom(BORROWED_TOKEN, callbacker, self, to_repay) self.transferFrom( @@ -1250,7 +1250,7 @@ def liquidate( liquidator=msg.sender, user=user, collateral_received=xy[1], - stablecoin_received=xy[0], + borrowed_received=xy[0], debt=debt, ) if final_debt == 0: @@ -1269,22 +1269,22 @@ def liquidate( @external 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( + 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 @@ -1342,7 +1342,7 @@ 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) """ return staticcall self._view.user_state(user) diff --git a/contracts/interfaces/ILlamalendController.vyi b/contracts/interfaces/ILlamalendController.vyi index 95050cb5..d4bb4cc6 100644 --- a/contracts/interfaces/ILlamalendController.vyi +++ b/contracts/interfaces/ILlamalendController.vyi @@ -21,7 +21,7 @@ event Liquidate: liquidator: address user: address collateral_received: uint256 - stablecoin_received: uint256 + borrowed_received: uint256 debt: uint256 diff --git a/contracts/interfaces/IMintController.vyi b/contracts/interfaces/IMintController.vyi index b425d473..d5654a0c 100644 --- a/contracts/interfaces/IMintController.vyi +++ b/contracts/interfaces/IMintController.vyi @@ -16,7 +16,7 @@ event Liquidate: liquidator: address user: address collateral_received: uint256 - stablecoin_received: uint256 + borrowed_received: uint256 debt: uint256 @@ -90,7 +90,7 @@ struct Loan: struct CallbackData: active_band: int256 - stablecoins: uint256 + borrowed: uint256 collateral: uint256 # Functions diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy index e977ca50..795008de 100644 --- a/contracts/lending/LLController.vy +++ b/contracts/lending/LLController.vy @@ -188,9 +188,9 @@ def create_loan( calldata: Bytes[10**4] = b"", ): """ - @notice Create loan but pass stablecoin to a callback first so that it can build leverage + @notice Create loan but pass borrowed 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 diff --git a/contracts/lending/LLControllerView.vy b/contracts/lending/LLControllerView.vy index c350337c..63035436 100644 --- a/contracts/lending/LLControllerView.vy +++ b/contracts/lending/LLControllerView.vy @@ -91,7 +91,7 @@ def max_borrowable( @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 + @return Maximum amount of borrowed asset to borrow """ # Cannot borrow beyond the amount of coins Controller has or beyond borrow_cap total_debt: uint256 = self._total_debt() diff --git a/contracts/zaps/LeverageZap.vy b/contracts/zaps/LeverageZap.vy index cd63f9df..ec64fc8d 100644 --- a/contracts/zaps/LeverageZap.vy +++ b/contracts/zaps/LeverageZap.vy @@ -282,12 +282,12 @@ def _approve(coin: address, spender: address): @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] @@ -318,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] From 6d49b8d2e8953465349ad9ec5383de16b8bc375d Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 16 Sep 2025 15:46:10 +0200 Subject: [PATCH 236/413] ci: add qa to workflows --- .github/workflows/test.yaml | 39 +++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 86c301ab..2b69b1d0 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -8,9 +8,30 @@ env: HYPOTHESIS_PROFILE: no-shrink 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 + tests: - name: ${{ matrix.folder }} (${{ matrix.venom.name }}) + name: ${{ matrix.folder }} runs-on: ubuntu-latest + needs: pre-commit strategy: fail-fast: false matrix: @@ -24,20 +45,15 @@ jobs: - "tests/lending" - "tests/stableborrow --ignore=tests/stableborrow/stabilize" - "tests/stableborrow/stabilize" - venom: - - { name: "standard mode", value: false } - # - { name: "venom mode", value: true } - continue-on-error: ${{ matrix.venom.value }} steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: submodules: recursive - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v6.7 with: - version: "0.8.7" enable-cache: true # Enables built-in caching for uv - name: Cache Compiler Installations @@ -53,14 +69,7 @@ jobs: - name: Install Requirements run: uv sync - - name: Install Nightly Vyper if Venom is enabled - if: ${{ matrix.venom.value }} - run: | - uv pip install --force-reinstall 'git+https://github.com/vyperlang/vyper.git@master#egg=vyper' - - name: Run tests run: | export VENOM=${{ matrix.venom.value }} uv run pytest ${{ matrix.folder }} -n auto - # source .venv/bin/activate - # uv pip freeze From c26e18a209376d4d7827f1e505e8f438da9cf9e4 Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 16 Sep 2025 15:48:37 +0200 Subject: [PATCH 237/413] style: lint --- .git-blame-ignore-revs | 2 +- .pre-commit-config.yaml | 2 +- pyproject.toml | 3 +- tests/stateful/test_controller_stateful.py | 67 +++++++++++++++++----- tests/utils/deploy.py | 9 +-- 5 files changed, 60 insertions(+), 23 deletions(-) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index a916178c..e0f331dd 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1 +1 @@ -05e812182aa1267beca1b619d5882f4eb917e423 \ No newline at end of file +05e812182aa1267beca1b619d5882f4eb917e423 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8d401bdb..3e6b6fa9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,4 +19,4 @@ repos: hooks: - id: ruff-check args: [--fix] - - id: ruff-format \ No newline at end of file + - id: ruff-format diff --git a/pyproject.toml b/pyproject.toml index a284d25f..a038f941 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,8 +30,7 @@ titanoboa = { git = "https://github.com/AlbertoCentonze/titanoboa", rev = "vvm-e [tool.uv] package = false # This is not a Python package -[tool.ruff] - +[tool.ruff.lint] ignore = [ # Should be eventually fixed "E722" , diff --git a/tests/stateful/test_controller_stateful.py b/tests/stateful/test_controller_stateful.py index 553e284c..0b0583d8 100644 --- a/tests/stateful/test_controller_stateful.py +++ b/tests/stateful/test_controller_stateful.py @@ -1,7 +1,21 @@ from decimal import Decimal from hypothesis import note, assume -from hypothesis.strategies import composite, integers, builds, decimals, SearchStrategy, data, sampled_from -from hypothesis.stateful import RuleBasedStateMachine, initialize, rule, precondition, invariant +from hypothesis.strategies import ( + composite, + integers, + builds, + decimals, + SearchStrategy, + data, + sampled_from, +) +from hypothesis.stateful import ( + RuleBasedStateMachine, + initialize, + rule, + precondition, + invariant, +) import boa @@ -42,11 +56,21 @@ @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)) + 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 -def token_amounts(decimal_places: int, min_value: int = 0, max_value: int = None) -> SearchStrategy[int]: + +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, @@ -190,6 +214,7 @@ def mint_markets( return market + # ------------ controller interaction params ------------- # TODO eventually fix this in SC @@ -208,11 +233,12 @@ def initialize_protocol(self, market): self.controller = market["controller"] self.amm = market["amm"] self.users = [] - self.collateral_token = ERC20_MOCK_DEPLOYER.at(self.controller.collateral_token()) + 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]") @@ -221,7 +247,10 @@ def create_loan_rule(self, N, data): 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})") + 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( @@ -234,7 +263,7 @@ def create_loan_rule(self, N, data): self.controller.create_loan(collateral, debt, N) self.users.append(user) - + @invariant() def time_passes(self): # Snapshot current debts for tracked users @@ -260,7 +289,9 @@ def repay_rule(self, data): # 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") + 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()) @@ -288,7 +319,9 @@ def borrow_more_rule(self, data): # Draw increments using the shared strategy d_collateral, d_debt = data.draw( - loan_increments_for_borrow_more(self.controller, user, N, collateral0, debt0), + loan_increments_for_borrow_more( + self.controller, user, N, collateral0, debt0 + ), label="borrow_more_increments", ) @@ -299,7 +332,6 @@ def borrow_more_rule(self, data): 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): @@ -318,13 +350,15 @@ def remove_collateral_rule(self, data): # 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") + 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 @@ -345,7 +379,9 @@ def liquidate(self): note("[HARD LIQUIDATE]") for pos in positions: - note(f"liquidating user {pos.user} with health {self.controller.health(pos.user, True)}") + 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) @@ -356,4 +392,5 @@ def liquidate(self): n = self.controller.n_loans() self.users = [self.controller.loans(i) for i in range(n)] + TestControllerStateful = ControllerStateful.TestCase diff --git a/tests/utils/deploy.py b/tests/utils/deploy.py index d7d9b4a3..9beddbef 100644 --- a/tests/utils/deploy.py +++ b/tests/utils/deploy.py @@ -18,7 +18,6 @@ AMM_DEPLOYER, CONTROLLER_FACTORY_DEPLOYER, CONTROLLER_VIEW_DEPLOYER, - # Lending contracts VAULT_DEPLOYER, LL_CONTROLLER_DEPLOYER, @@ -84,7 +83,9 @@ def __init__(self, initial_price: int = 3000 * 10**18): # 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) + self._mint_controller_deployer = boa.loads_partial( + mint_controller_code, compiler_args=compiler_args_codesize + ) # Deploy all blueprints self.blueprints = Blueprints( @@ -190,8 +191,8 @@ def create_mint_market( 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) + "controller": self._mint_controller_deployer.at(controller_address), + "amm": AMM_DEPLOYER.at(amm_address), } def create_lending_market( From 9c57378cc7b9edbd42833b0b7d026b42280f7df3 Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 16 Sep 2025 16:39:54 +0200 Subject: [PATCH 238/413] style: use camel case for transferFrom --- contracts/lib/token_lib.vy | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/lib/token_lib.vy b/contracts/lib/token_lib.vy index 0973a195..3c27b7d9 100644 --- a/contracts/lib/token_lib.vy +++ b/contracts/lib/token_lib.vy @@ -1,3 +1,4 @@ +# TODO missing pragmas from ethereum.ercs import IERC20 @@ -8,7 +9,7 @@ def max_approve(token: IERC20, spender: address): @internal -def transferFrom(token: IERC20, _from: address, _to: address, amount: uint256): +def transfer_from(token: IERC20, _from: address, _to: address, amount: uint256): if amount > 0: assert extcall token.transferFrom(_from, _to, amount, default_return_value=True) From e682dd70103ab3091f1511166fad56965d5878cd Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 16 Sep 2025 16:48:54 +0200 Subject: [PATCH 239/413] test: simplify hypothesis profiles --- tests/conftest.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 1506cc5f..41d0102e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,3 @@ -import os from datetime import timedelta import boa @@ -18,14 +17,20 @@ TESTING_DECIMALS = [2, 6, 8, 9, 18] -settings.register_profile( +no_shrink = settings.register_profile( "no-shrink", - settings(phases=list(Phase)[:4]), + phases=list(Phase)[:4], deadline=timedelta(seconds=1000), print_blob=True, ) -settings.register_profile("default", deadline=timedelta(seconds=1000), print_blob=True) -settings.load_profile(os.getenv("HYPOTHESIS_PROFILE", "default")) +settings.register_profile( + "quick", + parent=no_shrink, + max_examples=3, + stateful_step_count=15, +) + +settings.load_profile("no-shrink") @pytest.fixture(scope="module") From fb57fed3b5b0cf7b2b3a2d3607390f97be91dc89 Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 16 Sep 2025 16:49:18 +0200 Subject: [PATCH 240/413] style: minor vyper adjustments --- contracts/interfaces/IPartialRepayZap.vyi | 8 +++--- contracts/zaps/PartialRepayZap.vy | 30 ++++++++++++++--------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/contracts/interfaces/IPartialRepayZap.vyi b/contracts/interfaces/IPartialRepayZap.vyi index dbc6acf7..3792057e 100644 --- a/contracts/interfaces/IPartialRepayZap.vyi +++ b/contracts/interfaces/IPartialRepayZap.vyi @@ -1,8 +1,8 @@ -from contracts.interfaces import ILlamalendController as IController +from contracts.interfaces import IMintController as IController event PartialRepay: - controller: address + controller: IController user: address collateral_decrease: uint256 borrowed_from_sender: uint256 @@ -20,12 +20,12 @@ struct Position: @view @external -def users_to_liquidate(_controller: address, _from: uint256, _limit: uint256) -> DynArray[Position, 1000]: +def users_to_liquidate(_controller: IController, _from: uint256, _limit: uint256) -> DynArray[Position, 1000]: ... @external -def liquidate_partial(_controller: address, _user: address, _min_x: uint256): +def liquidate_partial(_controller: IController, _user: address, _min_x: uint256): ... diff --git a/contracts/zaps/PartialRepayZap.vy b/contracts/zaps/PartialRepayZap.vy index 1f783a8f..1d0a15be 100644 --- a/contracts/zaps/PartialRepayZap.vy +++ b/contracts/zaps/PartialRepayZap.vy @@ -37,9 +37,17 @@ def __init__( 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: address, _from: uint256 = 0, _limit: uint256 = 0) -> DynArray[IZap.Position, 1000]: +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 @@ -47,8 +55,8 @@ def users_to_liquidate(_controller: address, _from: uint256 = 0, _limit: uint256 @param _limit Number of loans to inspect (0 = all) @return Dynamic array with position info and zap-specific estimates """ - CONTROLLER: IController = IController(_controller) - AMM: IAMM = staticcall CONTROLLER.amm() + # 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 @@ -58,9 +66,8 @@ def users_to_liquidate(_controller: address, _from: uint256 = 0, _limit: uint256 if i == len(base_positions): break pos: IController.Position = base_positions[i] - # Compute zap-specific estimates to_repay: uint256 = staticcall CONTROLLER.tokens_to_liquidate(pos.user, FRAC) - x_down: uint256 = staticcall AMM.get_x_down(pos.user) + x_down: uint256 = self._x_down(CONTROLLER, pos.user) ratio: uint256 = unsafe_div(unsafe_mul(x_down, WAD), pos.debt) out.append( IZap.Position( @@ -76,7 +83,7 @@ def users_to_liquidate(_controller: address, _from: uint256 = 0, _limit: uint256 @external -def liquidate_partial(_controller: address, _user: address, _min_x: uint256): +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. @@ -84,18 +91,19 @@ def liquidate_partial(_controller: address, _user: address, _min_x: uint256): @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 """ - CONTROLLER: IController = IController(_controller) - AMM: IAMM = staticcall CONTROLLER.amm() + # 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) + tkn.max_approve(BORROWED, CONTROLLER.address) total_debt: uint256 = staticcall CONTROLLER.debt(_user) - x_down: uint256 = staticcall AMM.get_x_down(_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" @@ -104,7 +112,7 @@ def liquidate_partial(_controller: address, _user: address, _min_x: uint256): to_repay: uint256 = staticcall CONTROLLER.tokens_to_liquidate(_user, FRAC) borrowed_from_sender: uint256 = unsafe_div(unsafe_mul(to_repay, ratio), WAD) - tkn.transferFrom(BORROWED, msg.sender, self, borrowed_from_sender) + tkn.transfer_from(BORROWED, msg.sender, self, borrowed_from_sender) extcall CONTROLLER.liquidate(_user, _min_x, FRAC) collateral_received: uint256 = staticcall COLLATERAL.balanceOf(self) From 8c9d4dde0233997fd563d874df184ea6a0e8ad7d Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 16 Sep 2025 17:53:02 +0200 Subject: [PATCH 241/413] feat: add unified token interface --- contracts/interfaces/IERC20.vyi | 52 +++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 contracts/interfaces/IERC20.vyi diff --git a/contracts/interfaces/IERC20.vyi b/contracts/interfaces/IERC20.vyi new file mode 100644 index 00000000..c552b915 --- /dev/null +++ b/contracts/interfaces/IERC20.vyi @@ -0,0 +1,52 @@ +# See https://github.com/vyperlang/vyper/pull/4349 for rationale + +# Events +event Transfer: + sender: indexed(address) + receiver: indexed(address) + value: uint256 + +event Approval: + owner: indexed(address) + spender: indexed(address) + value: uint256 + +# Functions +@view +def totalSupply() -> uint256: + ... + +@view +def balanceOf(_owner: address) -> uint256: + ... + +@view +def allowance(_owner: address, _spender: address) -> uint256: + ... + +def transfer(_to: address, _value: uint256) -> bool: + ... + +def transferFrom(_from: address, _to: address, _value: uint256) -> bool: + ... + +def approve(_spender: address, _value: uint256) -> bool: + ... + +#NOTE: interface uses `String[1]` where 1 is the lower bound of the string returned by the function. +# For end-users this means they can't use `implements: ERC20Detailed` unless their implementation +# uses a value n >= 1. Regardless this is fine as one can't do String[0] where n == 0. + +@view +def name() -> String[1]: + ... + +@view +def symbol() -> String[1]: + ... + +@view +def decimals() -> uint8: + ... + +# TODO replace all occurrences of IERC20 with this interface From b7b94cd0b75e48bfcae679153272509138e78ea2 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 11:46:32 +0200 Subject: [PATCH 242/413] ci: remove unused lines --- .github/workflows/test.yaml | 2 -- .pre-commit-config.yaml | 13 +++++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 2b69b1d0..73ae9213 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -4,7 +4,6 @@ on: [push] env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # RPC_ETHEREUM: ${{ secrets.RPC_ETHEREUM }} HYPOTHESIS_PROFILE: no-shrink jobs: @@ -71,5 +70,4 @@ jobs: - name: Run tests run: | - export VENOM=${{ matrix.venom.value }} uv run pytest ${{ matrix.folder }} -n auto diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3e6b6fa9..48530b6a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,3 +20,16 @@ repos: - 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$ # TODO slowly increase natrix coverage From 8711c7a91765c260a55ea67333d78e357e0f7b44 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 12:11:30 +0200 Subject: [PATCH 243/413] refactor: lending factory overhaul --- .pre-commit-config.yaml | 2 +- contracts/interfaces/ILendingFactory.vyi | 48 ++- contracts/lending/LendingFactory.vy | 501 +++++++++-------------- contracts/zaps/CreateFromPool.vy | 95 +++++ 4 files changed, 321 insertions(+), 325 deletions(-) create mode 100644 contracts/zaps/CreateFromPool.vy diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 48530b6a..441b9be2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,4 +32,4 @@ repos: hooks: - id: mamushi args: [--line-length=100] - files: ^contracts/lending/LendingFactory\.vy$ # TODO slowly increase natrix coverage + files: ^(contracts/lending/LendingFactory\.vy|contracts/zaps/CreateFromPool\.vy)$ # TODO slowly increase natrix coverage diff --git a/contracts/interfaces/ILendingFactory.vyi b/contracts/interfaces/ILendingFactory.vyi index 18c19ba0..31867ded 100644 --- a/contracts/interfaces/ILendingFactory.vyi +++ b/contracts/interfaces/ILendingFactory.vyi @@ -1,29 +1,45 @@ -event SetImplementations: +from contracts.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 price_oracle: address - monetary_policy: address - view: address - -event SetDefaultRates: - min_rate: uint256 - max_rate: uint256 + controller_view: address -event SetAdmin: - admin: address event SetFeeReceiver: fee_receiver: 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 + 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/lending/LendingFactory.vy b/contracts/lending/LendingFactory.vy index 977aa877..a866cc5c 100644 --- a/contracts/lending/LendingFactory.vy +++ b/contracts/lending/LendingFactory.vy @@ -1,28 +1,36 @@ # pragma version 0.4.3 -# pragma optimize codesize -# pragma evm-version shanghai +# pragma nonreentrancy on """ @title LlamaLend Factory -@notice Factory of non-rehypothecated lending vaults: collateral is not being lent out. +@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 ethereum.ercs import IERC20 -from ethereum.ercs import IERC20Detailed +from contracts.interfaces import IERC20 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 -# 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% +exports: ( + # owner is not exported as we refer to it as `admin` for backwards compatibility + ownable.renounce_ownership, + ownable.transfer_ownership, +) + +# TODO move A to constants +# TODO A this low is unsafe MIN_A: constant(uint256) = 2 MAX_A: constant(uint256) = 10000 MIN_FEE: constant(uint256) = 10**6 # 1e-12, still needs to be above 0 @@ -31,20 +39,13 @@ MAX_LOAN_DISCOUNT: constant(uint256) = 5 * 10**17 MIN_LIQUIDATION_DISCOUNT: constant(uint256) = 10**16 # 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) -view_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) +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 -admin: public(address) fee_receiver: public(address) # Vaults can only be created but not removed @@ -57,361 +58,245 @@ names: public(HashMap[uint256, String[64]]) @deploy def __init__( - amm_impl: address, - controller_impl: address, - vault_impl: address, - pool_price_oracle_impl: address, - view_impl: address, - monetary_policy: address, - admin: address, # TODO also add params votes? - fee_receiver: address, + _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 monetary_policy Address for implementation of monetary policy @param admin Admin address (DAO) @param fee_receiver Receiver of interest and admin fees """ - # TODO impl vs blueprint is confusing - self.amm_impl = amm_impl - self.controller_impl = controller_impl - self.vault_impl = vault_impl - # TODO everyone is forced to have the same price oracle? - self.pool_price_oracle_impl = pool_price_oracle_impl - # TODO everyone is forced to have the same monetary policy? - self.monetary_policy_impl = monetary_policy - self.view_impl = view_impl - - # TODO is this actually useful? - self.min_default_borrow_rate = 5 * 10**15 // (365 * 86400) - self.max_default_borrow_rate = 50 * 10**16 // (365 * 86400) - - self.admin = admin - self.fee_receiver = fee_receiver - - -@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 - ) -> address[3]: + self.amm_blueprint = _amm_blueprint + self.controller_blueprint = _controller_blueprint + self.vault_blueprint = _vault_blueprint + self.pool_price_oracle_blueprint = _pool_price_oracle_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 Internal method for creation of the 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 _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" - - 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" - # TODO code offset is not required anymore - monetary_policy: address = create_from_blueprint( - self.monetary_policy_impl, borrowed_token, min_rate, max_rate, code_offset=3) - - A_ratio: uint256 = 10**18 * A // (A - 1) - p: uint256 = staticcall IPriceOracle(price_oracle).price() # This also validates price oracle ABI + 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) + # TODO validate monetary policy here + p: uint256 = (staticcall _price_oracle.price()) # This also validates price oracle ABI assert p > 0 - assert extcall IPriceOracle(price_oracle).price_w() == p - - # TODO better diff blueprints from minimal proxy targets in naming - vault: IVault = IVault(create_minimal_proxy_to(self.vault_impl)) - amm: address = create_from_blueprint( - self.amm_impl, - borrowed_token, 10**convert(18 - staticcall IERC20Detailed(borrowed_token).decimals(), uint256), - collateral_token, 10**convert(18 - staticcall IERC20Detailed(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: address = create_from_blueprint( - self.controller_impl, - vault, amm, - borrowed_token, collateral_token, - monetary_policy, - loan_discount, - liquidation_discount, - self.view_impl, - code_offset=3) - extcall IAMM(amm).set_admin(controller) - - extcall vault.initialize(IAMM(amm), controller, IERC20(borrowed_token), IERC20(collateral_token)) + 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) market_count: uint256 = self.market_count log ILendingFactory.NewVault( id=market_count, - collateral_token=collateral_token, - borrowed_token=borrowed_token, - vault=vault.address, + collateral_token=_collateral_token, + borrowed_token=_borrowed_token, + vault=vault, controller=controller, amm=amm, - price_oracle=price_oracle, - monetary_policy=monetary_policy + 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.names[market_count] = _name self.market_count = market_count + 1 - return [vault.address, controller, amm] - + if _supply_limit < max_value(uint256): + extcall vault.set_max_supply(_supply_limit) -@external -@nonreentrant -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) - ) -> 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 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 - """ - res: address[3] = self._create(borrowed_token, collateral_token, A, fee, loan_discount, liquidation_discount, - price_oracle, name, min_borrow_rate, max_borrow_rate) - # TODO duplicate code - if supply_limit < max_value(uint256): - extcall IVault(res[0]).set_max_supply(supply_limit) - - return res + return [vault.address, controller.address, amm.address] @external -@nonreentrant -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) - ) -> 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 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 - # 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: 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) - - res: address[3] = self._create( - borrowed_token, collateral_token, A, fee, loan_discount, liquidation_discount, - price_oracle, name, min_borrow_rate, max_borrow_rate, - ) - # TODO duplicate code - if supply_limit < max_value(uint256): - extcall IVault(res[0]).set_max_supply(supply_limit) - - return res - - @view -@external -def controllers(n: uint256) -> address: - return staticcall self.vaults[n].controller() +@reentrant +def controllers(_n: uint256) -> IController: + return IController(staticcall self.vaults[_n].controller()) -@view @external -def amms(n: uint256) -> address: - return (staticcall self.vaults[n].amm()).address +@view +@reentrant +def amms(_n: uint256) -> IAMM: + return staticcall self.vaults[_n].amm() -@view @external -def borrowed_tokens(n: uint256) -> address: - return (staticcall self.vaults[n].borrowed_token()).address +@view +@reentrant +def borrowed_tokens(_n: uint256) -> IERC20: + return staticcall self.vaults[_n].borrowed_token() -@view @external -def collateral_tokens(n: uint256) -> address: - return (staticcall self.vaults[n].collateral_token()).address +@view +@reentrant +def collateral_tokens(_n: uint256) -> IERC20: + return staticcall self.vaults[_n].collateral_token() -@view @external -def price_oracles(n: uint256) -> address: - return (staticcall (staticcall self.vaults[n].amm()).price_oracle_contract()).address +@view +@reentrant +def price_oracles(_n: uint256) -> IPriceOracle: + return staticcall (staticcall self.vaults[_n].amm()).price_oracle_contract() -@view @external -def monetary_policies(n: uint256) -> address: - return (staticcall IController(staticcall self.vaults[n].controller()).monetary_policy()).address +@view +@reentrant +def monetary_policies(_n: uint256) -> IMonetaryPolicy: + return staticcall IController(staticcall self.vaults[_n].controller()).monetary_policy() -@view @external -def vaults_index(vault: IVault) -> uint256: - return self._vaults_index[vault] - 2**128 +@view +@reentrant +def vaults_index(_vault: IVault) -> uint256: + return self._vaults_index[_vault] - 2**128 @external -@nonreentrant def set_implementations( - controller: address, - amm: address, - vault: address, - pool_price_oracle: address, - monetary_policy: address, - view: address, # TODO improve naming + _controller_blueprint: address, + _amm_blueprint: address, + _vault_blueprint: address, + _pool_price_oracle_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 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 view Address of the view contract blueprint + @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 _pool_price_oracle_blueprint Address of the pool price oracle blueprint + @param _controller_view_blueprint Address of the view contract 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 view != empty(address): - self.view_impl = view - - log ILendingFactory.SetImplementations( - amm=amm, - controller=controller, - vault=vault, - price_oracle=pool_price_oracle, - monetary_policy=monetary_policy, - view=view + 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 _pool_price_oracle_blueprint != empty(address): + self.pool_price_oracle_blueprint = _pool_price_oracle_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, + price_oracle=_pool_price_oracle_blueprint, + controller_view=_controller_view_blueprint, ) @external -@nonreentrant -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 ILendingFactory.SetDefaultRates(min_rate=min_rate, max_rate=max_rate) - - -@external -@nonreentrant -def set_admin(admin: address): +@view +@reentrant +def admin() -> address: """ - @notice Set admin of the factory (should end up with DAO) - @param admin Address of the admin + @notice Get the admin of the factory """ - assert msg.sender == self.admin - self.admin = admin - log ILendingFactory.SetAdmin(admin=admin) + return ownable.owner @external -@nonreentrant -def set_fee_receiver(fee_receiver: address): +def set_fee_receiver(_fee_receiver: address): """ @notice Set fee receiver who earns interest (DAO) - @param fee_receiver Address of the receiver + @param _fee_receiver Address of the receiver """ - assert msg.sender == self.admin - assert fee_receiver != empty(address) - self.fee_receiver = fee_receiver - log ILendingFactory.SetFeeReceiver(fee_receiver=fee_receiver) + ownable._check_owner() + assert _fee_receiver != empty(address) + self.fee_receiver = _fee_receiver + log ILendingFactory.SetFeeReceiver(fee_receiver=_fee_receiver) @external @view -def coins(vault_id: uint256) -> address[2]: - vault: IVault = self.vaults[vault_id] - return [(staticcall vault.borrowed_token()).address, (staticcall vault.collateral_token()).address] +@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/zaps/CreateFromPool.vy b/contracts/zaps/CreateFromPool.vy new file mode 100644 index 00000000..f5a09288 --- /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 contracts.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: address, _pool_price_oracle_blueprint: address): + FACTORY = ILendingFactory(_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 From ad2c285106b7933440e5c79a6a61b6d79c083a41 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 12:20:17 +0200 Subject: [PATCH 244/413] refactor: use internal IERC20 in vault --- contracts/interfaces/IVault.vyi | 2 +- contracts/lending/Vault.vy | 26 ++++---------------------- 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/contracts/interfaces/IVault.vyi b/contracts/interfaces/IVault.vyi index 49d70582..93645749 100644 --- a/contracts/interfaces/IVault.vyi +++ b/contracts/interfaces/IVault.vyi @@ -1,4 +1,4 @@ -from ethereum.ercs import IERC20 +from contracts.interfaces import IERC20 from contracts.interfaces import IAMM # from contracts.interfaces import ILlamalendController as IController diff --git a/contracts/lending/Vault.vy b/contracts/lending/Vault.vy index 172a451d..f726b99c 100644 --- a/contracts/lending/Vault.vy +++ b/contracts/lending/Vault.vy @@ -8,8 +8,7 @@ @license Copyright (c) Curve.Fi, 2020-2025 - all rights reserved """ -from ethereum.ercs import IERC20 -from ethereum.ercs import IERC20Detailed +from contracts.interfaces import IERC20 from ethereum.ercs import IERC4626 from contracts.interfaces import IAMM @@ -19,13 +18,7 @@ from contracts.interfaces import IFactory from contracts import constants as c implements: IERC20 -implements: IERC20Detailed -# implements: IERC4626 - - -interface IERC20Custom: - def decimals() -> uint256: view - def symbol() -> String[32]: view +# implements: IERC4626 TODO fix this event SetMaxSupply: @@ -66,17 +59,6 @@ totalSupply: public(uint256) precision: uint256 -@deploy -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 = IERC20(0x0000000000000000000000000000000000000001) - - @external def initialize( amm: IAMM, @@ -94,7 +76,7 @@ def initialize( assert self.borrowed_token.address == empty(address) self.borrowed_token = borrowed_token - borrowed_precision: uint256 = 10**(18 - (staticcall IERC20Custom(borrowed_token.address).decimals())) + borrowed_precision: uint256 = 10**(18 - convert(staticcall borrowed_token.decimals(), uint256)) self.collateral_token = collateral_token self.factory = IFactory(msg.sender) @@ -103,7 +85,7 @@ def initialize( # ERC20 set up self.precision = borrowed_precision - borrowed_symbol: String[32] = staticcall IERC20Custom(borrowed_token.address).symbol() + borrowed_symbol: String[32] = staticcall borrowed_token.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 From ca846383e571b755ecf38551eceb1c4153c892c1 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 12:23:50 +0200 Subject: [PATCH 245/413] fix: remove pool price oracle from base factory --- contracts/interfaces/ILendingFactory.vyi | 1 - contracts/lending/LendingFactory.vy | 6 ------ 2 files changed, 7 deletions(-) diff --git a/contracts/interfaces/ILendingFactory.vyi b/contracts/interfaces/ILendingFactory.vyi index 31867ded..0d8f9d43 100644 --- a/contracts/interfaces/ILendingFactory.vyi +++ b/contracts/interfaces/ILendingFactory.vyi @@ -10,7 +10,6 @@ event SetBlueprints: amm: address controller: address vault: address - price_oracle: address controller_view: address diff --git a/contracts/lending/LendingFactory.vy b/contracts/lending/LendingFactory.vy index a866cc5c..e5e7b0b5 100644 --- a/contracts/lending/LendingFactory.vy +++ b/contracts/lending/LendingFactory.vy @@ -77,7 +77,6 @@ def __init__( self.amm_blueprint = _amm_blueprint self.controller_blueprint = _controller_blueprint self.vault_blueprint = _vault_blueprint - self.pool_price_oracle_blueprint = _pool_price_oracle_blueprint self.controller_view_blueprint = _controller_view_blueprint ownable.__init__() @@ -238,7 +237,6 @@ def set_implementations( _controller_blueprint: address, _amm_blueprint: address, _vault_blueprint: address, - _pool_price_oracle_blueprint: address, _controller_view_blueprint: address, ): """ @@ -247,7 +245,6 @@ def set_implementations( @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 _pool_price_oracle_blueprint Address of the pool price oracle blueprint @param _controller_view_blueprint Address of the view contract blueprint """ ownable._check_owner() @@ -258,8 +255,6 @@ def set_implementations( self.amm_blueprint = _amm_blueprint if _vault_blueprint != empty(address): self.vault_blueprint = _vault_blueprint - if _pool_price_oracle_blueprint != empty(address): - self.pool_price_oracle_blueprint = _pool_price_oracle_blueprint if _controller_view_blueprint != empty(address): self.controller_view_blueprint = _controller_view_blueprint @@ -267,7 +262,6 @@ def set_implementations( amm=_amm_blueprint, controller=_controller_blueprint, vault=_vault_blueprint, - price_oracle=_pool_price_oracle_blueprint, controller_view=_controller_view_blueprint, ) From 45e638b913b6353d5780c069bf052e99b4f48fd3 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 12:27:45 +0200 Subject: [PATCH 246/413] chore: remove TODOs --- contracts/lending/LendingFactory.vy | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/lending/LendingFactory.vy b/contracts/lending/LendingFactory.vy index e5e7b0b5..543553e8 100644 --- a/contracts/lending/LendingFactory.vy +++ b/contracts/lending/LendingFactory.vy @@ -29,8 +29,6 @@ exports: ( ownable.transfer_ownership, ) -# TODO move A to constants -# TODO A this low is unsafe MIN_A: constant(uint256) = 2 MAX_A: constant(uint256) = 10000 MIN_FEE: constant(uint256) = 10**6 # 1e-12, still needs to be above 0 From f99f3634efb42ee5d2e8c02d26a8a59e4e1d86a4 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 12:54:26 +0200 Subject: [PATCH 247/413] fix: add back symbol interface --- contracts/lending/Vault.vy | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/contracts/lending/Vault.vy b/contracts/lending/Vault.vy index f726b99c..12eabf1e 100644 --- a/contracts/lending/Vault.vy +++ b/contracts/lending/Vault.vy @@ -58,6 +58,9 @@ totalSupply: public(uint256) precision: uint256 +# Only needed for initialize +interface IERC20Symbol: + def symbol() -> String[32]: view @external def initialize( @@ -85,7 +88,7 @@ def initialize( # ERC20 set up self.precision = borrowed_precision - borrowed_symbol: String[32] = staticcall 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 From 1aab1e388a5d2f6b3a0bdebfff6dba10c8404df3 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 12:56:08 +0200 Subject: [PATCH 248/413] fix: mpolicy not deployed by factory anymore --- contracts/mpolicies/SemilogMonetaryPolicy.vy | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/contracts/mpolicies/SemilogMonetaryPolicy.vy b/contracts/mpolicies/SemilogMonetaryPolicy.vy index 9c843b14..b1748c79 100644 --- a/contracts/mpolicies/SemilogMonetaryPolicy.vy +++ b/contracts/mpolicies/SemilogMonetaryPolicy.vy @@ -39,7 +39,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 +53,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 ### From 0fad006a1ae70f5cfd0dbb0b4d41472a338ad438 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 13:00:15 +0200 Subject: [PATCH 249/413] test: fix deployment script --- tests/utils/deploy.py | 45 +++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/tests/utils/deploy.py b/tests/utils/deploy.py index 9beddbef..6b9d8403 100644 --- a/tests/utils/deploy.py +++ b/tests/utils/deploy.py @@ -11,6 +11,8 @@ ) from typing import Dict, Any +from tests.utils.constants import MAX_UINT256 + from tests.utils.deployers import ( # Core contracts @@ -29,6 +31,7 @@ # Monetary policies CONSTANT_MONETARY_POLICY_DEPLOYER, CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER, + SEMILOG_MONETARY_POLICY_DEPLOYER, WETH_DEPLOYER, ERC20_MOCK_DEPLOYER, # Compiler flags @@ -47,6 +50,7 @@ def __init__(self, **deployers: VyperDeployer): ll_controller_view: VyperBlueprint price_oracle: VyperBlueprint mpolicy: VyperBlueprint + vault: VyperBlueprint # TODO rename to this class to Llamalend and the file to protocol.py @@ -95,6 +99,7 @@ def __init__(self, initial_price: int = 3000 * 10**18): ll_controller_view=LL_CONTROLLER_VIEW_DEPLOYER, price_oracle=CRYPTO_FROM_POOL_DEPLOYER, mpolicy=CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER, + vault=VAULT_DEPLOYER, ) # Deploy core infrastructure @@ -132,17 +137,13 @@ def __init_mint_markets(self, initial_price): def __init_lend_markets(self): # Deploy Lending Protocol - # Deploy vault implementation - self.vault_impl = VAULT_DEPLOYER.deploy() - # Deploy lending factory self.lending_factory = LENDING_FACTORY_DEPLOYER.deploy( self.blueprints.amm.address, self.blueprints.ll_controller.address, - self.vault_impl.address, + self.blueprints.vault.address, self.blueprints.price_oracle.address, self.blueprints.ll_controller_view.address, - self.blueprints.mpolicy.address, self.admin, self.fee_receiver, ) @@ -209,6 +210,7 @@ def create_lending_market( 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. @@ -224,10 +226,25 @@ def create_lending_market( 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, @@ -236,9 +253,9 @@ def create_lending_market( loan_discount, liquidation_discount, price_oracle.address, + monetary_policy.address, name, - min_borrow_rate, - max_borrow_rate, + supply_limit, sender=self.admin, ) @@ -246,20 +263,6 @@ def create_lending_market( controller = LL_CONTROLLER_DEPLOYER.at(result[1]) amm = AMM_DEPLOYER.at(result[2]) - # Optionally override the market's monetary policy after creation. - if mpolicy_deployer is not None: - # Always deploy with FACTORY as msg.sender so policies that bind FACTORY at - # deploy time (e.g., SemilogMonetaryPolicy) initialize correctly. - # TODO report issue to boa (sender not available for constructor) - with boa.env.prank(self.lending_factory.address): - custom_mp = mpolicy_deployer.deploy( - borrowed_token.address, - min_borrow_rate, - max_borrow_rate, - ) - # Set on controller with admin privileges - controller.set_monetary_policy(custom_mp, sender=self.admin) - # Seed lending markets by depositing borrowed token into the vault if seed_amount and seed_amount > 0: with boa.env.prank(self.admin): From eef35f61d193ecb96a6798fc416fd9508c530fcc Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 13:01:55 +0200 Subject: [PATCH 250/413] chore: tackle TODOs --- tests/conftest.py | 1 - tests/stateful/test_controller_stateful.py | 1 - tests/utils/{deploy.py => protocols.py} | 3 +-- 3 files changed, 1 insertion(+), 4 deletions(-) rename tests/utils/{deploy.py => protocols.py} (99%) diff --git a/tests/conftest.py b/tests/conftest.py index 41d0102e..51a9bf9c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,6 @@ import boa import pytest from hypothesis import settings, Phase -from tests.utils.deploy import Protocol from tests.utils.deployers import ( ERC20_MOCK_DEPLOYER, CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER, diff --git a/tests/stateful/test_controller_stateful.py b/tests/stateful/test_controller_stateful.py index 0b0583d8..5591c057 100644 --- a/tests/stateful/test_controller_stateful.py +++ b/tests/stateful/test_controller_stateful.py @@ -19,7 +19,6 @@ import boa -from tests.utils.deploy import Protocol from tests.utils.deployers import AMM_DEPLOYER, ERC20_MOCK_DEPLOYER, STABLECOIN_DEPLOYER from tests.utils.constants import ( MAX_TICKS, diff --git a/tests/utils/deploy.py b/tests/utils/protocols.py similarity index 99% rename from tests/utils/deploy.py rename to tests/utils/protocols.py index 6b9d8403..8cee116e 100644 --- a/tests/utils/deploy.py +++ b/tests/utils/protocols.py @@ -53,8 +53,7 @@ def __init__(self, **deployers: VyperDeployer): vault: VyperBlueprint -# TODO rename to this class to Llamalend and the file to protocol.py -class Protocol: +class Llamalend: """ Protocol deployment and management class for llamalend. Handles deployment of core infrastructure and creation of markets. From 62b9cfaa64b904a84418087b1cbc948d6a5afb6c Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 13:04:46 +0200 Subject: [PATCH 251/413] test: fix missing imports --- tests/conftest.py | 4 ++-- tests/stateful/test_controller_stateful.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 51a9bf9c..34239751 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,7 +7,7 @@ ERC20_MOCK_DEPLOYER, CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER, ) - +from tests.utils.protocols import Llamalend boa.env.enable_fast_mode() @@ -34,7 +34,7 @@ @pytest.fixture(scope="module") def proto(): - return Protocol() + return Llamalend() @pytest.fixture(scope="module") diff --git a/tests/stateful/test_controller_stateful.py b/tests/stateful/test_controller_stateful.py index 5591c057..b6573df8 100644 --- a/tests/stateful/test_controller_stateful.py +++ b/tests/stateful/test_controller_stateful.py @@ -32,6 +32,7 @@ MIN_LIQUIDATION_DISCOUNT, MIN_TICKS, ) +from tests.utils.protocols import Llamalend # Debt ceiling has no explicit on-chain limit; choose a realistic test bound @@ -45,8 +46,8 @@ token_decimals = integers(min_value=2, max_value=18) prices = integers(min_value=int(1e12), max_value=int(1e24)) -# A simple strategy to initialize Protocol using Hypothesis builds -protocols = builds(Protocol, initial_price=prices) +# 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) From 203fc428f98edd8ecd91c4b5a85b4387926e2a39 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 13:22:17 +0200 Subject: [PATCH 252/413] refactor: use unified IERC20 interface --- contracts/AMM.vy | 2 +- contracts/Controller.vy | 11 +++-------- contracts/ControllerView.vy | 3 +-- contracts/LMCallback.vy | 2 +- contracts/Stableswap.vy | 2 +- contracts/interfaces/IERC20.vyi | 2 -- contracts/interfaces/IFactory.vyi | 2 +- contracts/interfaces/ILlamalendController.vyi | 3 +-- contracts/interfaces/IMintController.vyi | 2 +- contracts/lending/LLController.vy | 2 +- contracts/lending/LLControllerView.vy | 2 +- contracts/lib/token_lib.vy | 2 +- contracts/testing/ConstantMonetaryPolicyLending.vy | 2 +- contracts/testing/DummyFlashBorrower.vy | 2 +- contracts/testing/ERC20Mock.vy | 2 +- contracts/testing/FakeLeverage.vy | 2 +- contracts/zaps/PartialRepayZap.vy | 2 +- 17 files changed, 18 insertions(+), 27 deletions(-) diff --git a/contracts/AMM.vy b/contracts/AMM.vy index 87914128..eae19fc6 100644 --- a/contracts/AMM.vy +++ b/contracts/AMM.vy @@ -40,7 +40,7 @@ implements: IAMM from contracts.interfaces import IPriceOracle from contracts.interfaces import ILMGauge -from ethereum.ercs import IERC20 +from contracts.interfaces import IERC20 from contracts import constants as c diff --git a/contracts/Controller.vy b/contracts/Controller.vy index eff13cb2..57d30563 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -15,8 +15,7 @@ from contracts.interfaces import IMonetaryPolicy from contracts.interfaces import ILMGauge from contracts.interfaces import IFactory from contracts.interfaces import IPriceOracle -from ethereum.ercs import IERC20 -from ethereum.ercs import IERC20Detailed +from contracts.interfaces import IERC20 # TODO just rename interface of mint controller to "icontroller" from contracts.interfaces import IMintController as IController @@ -157,16 +156,12 @@ def __init__( MAX_AMM_FEE = min(WAD * MIN_TICKS_UINT // A, 10**17) COLLATERAL_TOKEN = _collateral_token - collateral_decimals: uint256 = convert( - staticcall IERC20Detailed(COLLATERAL_TOKEN.address).decimals(), uint256 - ) + collateral_decimals: uint256 = convert(staticcall COLLATERAL_TOKEN.decimals(), uint256) COLLATERAL_PRECISION = pow_mod256(10, 18 - collateral_decimals) BORROWED_TOKEN = _borrowed_token # TODO refactor this logic to be shared with view - borrowed_decimals: uint256 = convert( - staticcall IERC20Detailed(BORROWED_TOKEN.address).decimals(), uint256 - ) + borrowed_decimals: uint256 = convert(staticcall BORROWED_TOKEN.decimals(), uint256) BORROWED_PRECISION = pow_mod256(10, 18 - borrowed_decimals) self._monetary_policy = monetary_policy diff --git a/contracts/ControllerView.vy b/contracts/ControllerView.vy index 18b325f9..3f2c3259 100644 --- a/contracts/ControllerView.vy +++ b/contracts/ControllerView.vy @@ -12,8 +12,7 @@ from contracts.interfaces import IAMM from contracts.interfaces import IMintController as IController -from ethereum.ercs import IERC20 -from ethereum.ercs import IERC20Detailed +from contracts.interfaces import IERC20 from contracts import Controller as core import contracts.lib.liquidation_lib as liq diff --git a/contracts/LMCallback.vy b/contracts/LMCallback.vy index 0e658643..25ab9a94 100644 --- a/contracts/LMCallback.vy +++ b/contracts/LMCallback.vy @@ -6,7 +6,7 @@ @notice LM callback works like a gauge for collateral in LlamaLend/crvUSD AMMs """ -from ethereum.ercs import IERC20 +from contracts.interfaces import IERC20 interface ILLAMMA: def coins(i: uint256) -> address: view diff --git a/contracts/Stableswap.vy b/contracts/Stableswap.vy index 8b2382f8..e1340846 100644 --- a/contracts/Stableswap.vy +++ b/contracts/Stableswap.vy @@ -7,7 +7,7 @@ @dev ERC20 support for return True/revert, return True/False, return None """ -from ethereum.ercs import IERC20 +from contracts.interfaces import IERC20 interface Factory: def get_fee_receiver(_pool: address) -> address: view diff --git a/contracts/interfaces/IERC20.vyi b/contracts/interfaces/IERC20.vyi index c552b915..134dbd39 100644 --- a/contracts/interfaces/IERC20.vyi +++ b/contracts/interfaces/IERC20.vyi @@ -48,5 +48,3 @@ def symbol() -> String[1]: @view def decimals() -> uint8: ... - -# TODO replace all occurrences of IERC20 with this interface diff --git a/contracts/interfaces/IFactory.vyi b/contracts/interfaces/IFactory.vyi index 9815141f..68c2c4b9 100644 --- a/contracts/interfaces/IFactory.vyi +++ b/contracts/interfaces/IFactory.vyi @@ -1,4 +1,4 @@ -from ethereum.ercs import IERC20 +from contracts.interfaces import IERC20 @view def stablecoin() -> IERC20: diff --git a/contracts/interfaces/ILlamalendController.vyi b/contracts/interfaces/ILlamalendController.vyi index d4bb4cc6..4dd82df6 100644 --- a/contracts/interfaces/ILlamalendController.vyi +++ b/contracts/interfaces/ILlamalendController.vyi @@ -1,6 +1,5 @@ from contracts.interfaces import IVault - -from ethereum.ercs import IERC20 +from contracts.interfaces import IERC20 from contracts.interfaces import ILMGauge from contracts.interfaces import IAMM from contracts.interfaces import IMonetaryPolicy diff --git a/contracts/interfaces/IMintController.vyi b/contracts/interfaces/IMintController.vyi index d5654a0c..5d49a871 100644 --- a/contracts/interfaces/IMintController.vyi +++ b/contracts/interfaces/IMintController.vyi @@ -1,4 +1,4 @@ -from ethereum.ercs import IERC20 +from contracts.interfaces import IERC20 from contracts.interfaces import ILMGauge from contracts.interfaces import IAMM from contracts.interfaces import IMonetaryPolicy diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy index 795008de..2c447c06 100644 --- a/contracts/lending/LLController.vy +++ b/contracts/lending/LLController.vy @@ -10,7 +10,7 @@ @custom:security security@curve.fi """ -from ethereum.ercs import IERC20 +from contracts.interfaces import IERC20 from contracts.interfaces import IAMM from contracts.interfaces import IMonetaryPolicy from contracts.interfaces import IVault diff --git a/contracts/lending/LLControllerView.vy b/contracts/lending/LLControllerView.vy index 63035436..a5024653 100644 --- a/contracts/lending/LLControllerView.vy +++ b/contracts/lending/LLControllerView.vy @@ -8,7 +8,7 @@ main controller contract forwards all relevant calls. """ -from ethereum.ercs import IERC20 +from contracts.interfaces import IERC20 from contracts.interfaces import IMintController as IController from contracts.interfaces import ILlamalendController from contracts.interfaces import IAMM diff --git a/contracts/lib/token_lib.vy b/contracts/lib/token_lib.vy index 3c27b7d9..e6c7a22b 100644 --- a/contracts/lib/token_lib.vy +++ b/contracts/lib/token_lib.vy @@ -1,5 +1,5 @@ # TODO missing pragmas -from ethereum.ercs import IERC20 +from contracts.interfaces import IERC20 @internal diff --git a/contracts/testing/ConstantMonetaryPolicyLending.vy b/contracts/testing/ConstantMonetaryPolicyLending.vy index 453ab922..1cca0594 100644 --- a/contracts/testing/ConstantMonetaryPolicyLending.vy +++ b/contracts/testing/ConstantMonetaryPolicyLending.vy @@ -1,6 +1,6 @@ # pragma version 0.4.3 -from ethereum.ercs import IERC20 +from contracts.interfaces import IERC20 rate: public(uint256) diff --git a/contracts/testing/DummyFlashBorrower.vy b/contracts/testing/DummyFlashBorrower.vy index a75e8ba7..4df04a93 100644 --- a/contracts/testing/DummyFlashBorrower.vy +++ b/contracts/testing/DummyFlashBorrower.vy @@ -1,6 +1,6 @@ # pragma version 0.4.3 -from ethereum.ercs import IERC20 +from contracts.interfaces import IERC20 interface ERC3156FlashLender: def flashFee(token: address, amount: uint256) -> uint256: view diff --git a/contracts/testing/ERC20Mock.vy b/contracts/testing/ERC20Mock.vy index 00f6b505..8a297788 100644 --- a/contracts/testing/ERC20Mock.vy +++ b/contracts/testing/ERC20Mock.vy @@ -1,6 +1,6 @@ # pragma version 0.4.3 -from ethereum.ercs import IERC20 +from contracts.interfaces import IERC20 implements: IERC20 from snekmate.tokens import erc20 diff --git a/contracts/testing/FakeLeverage.vy b/contracts/testing/FakeLeverage.vy index 5e832595..fe6ea39c 100644 --- a/contracts/testing/FakeLeverage.vy +++ b/contracts/testing/FakeLeverage.vy @@ -1,5 +1,5 @@ # pragma version 0.4.3 -from ethereum.ercs import IERC20 +from contracts.interfaces import IERC20 STABLECOIN: immutable(IERC20) COLLATERAL: immutable(IERC20) diff --git a/contracts/zaps/PartialRepayZap.vy b/contracts/zaps/PartialRepayZap.vy index 1d0a15be..2244f7b1 100644 --- a/contracts/zaps/PartialRepayZap.vy +++ b/contracts/zaps/PartialRepayZap.vy @@ -9,7 +9,7 @@ Liquidator provides borrowed tokens, receives withdrawn collateral. """ -from ethereum.ercs import IERC20 +from contracts.interfaces import IERC20 from contracts.interfaces import IAMM from contracts.interfaces import IMintController as IController from contracts import Controller as ctrl From 04ce19c3a398d7a81f743f570e49ae585a523b30 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 13:24:03 +0200 Subject: [PATCH 253/413] fix: remove admin fee cap --- contracts/lending/LLController.vy | 7 ------- 1 file changed, 7 deletions(-) diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy index 2c447c06..1b541074 100644 --- a/contracts/lending/LLController.vy +++ b/contracts/lending/LLController.vy @@ -89,10 +89,6 @@ collected: public(uint256) # Unlike mint markets admin fee here is can be less than 100% admin_fee: public(uint256) -# TODO check this -MAX_ADMIN_FEE: constant(uint256) = 2 * 10**17 # 20% - -# TODO add natrix @deploy @@ -258,7 +254,4 @@ def set_admin_fee(admin_fee: uint256): @param admin_fee The fee which should be no higher than MAX_ADMIN_FEE """ core._check_admin() - assert ( - admin_fee <= MAX_ADMIN_FEE - ) # dev: admin_fee is higher than MAX_ADMIN_FEE self.admin_fee = admin_fee From 3a19b03f652c54dd10237e5a932625d4e21cc2f9 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 13:25:34 +0200 Subject: [PATCH 254/413] chore: remove tackled TODO --- contracts/lending/LLController.vy | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy index 1b541074..035e4d23 100644 --- a/contracts/lending/LLController.vy +++ b/contracts/lending/LLController.vy @@ -172,8 +172,6 @@ def borrowed_balance() -> uint256: return self._borrowed_balance() -# TODO delete deprecated and legacy files - @external def create_loan( collateral: uint256, From 0ed3f843dbaa7b80a02ea6986cef8436ce511626 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 13:30:09 +0200 Subject: [PATCH 255/413] refactor: reduce logic divergence --- contracts/Controller.vy | 5 +++++ contracts/MintController.vy | 8 ++------ contracts/constants.vy | 1 - 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 57d30563..eb0dff76 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -164,6 +164,11 @@ def __init__( borrowed_decimals: uint256 = convert(staticcall BORROWED_TOKEN.decimals(), uint256) BORROWED_PRECISION = pow_mod256(10, 18 - borrowed_decimals) + # This is useless for lending markets, but leaving it doesn't create any harm + assert extcall BORROWED_TOKEN.approve( + FACTORY.address, max_value(uint256), default_return_value=True + ) + self._monetary_policy = monetary_policy self.liquidation_discount = liquidation_discount self.loan_discount = loan_discount diff --git a/contracts/MintController.vy b/contracts/MintController.vy index 46646f5c..d00e7afa 100644 --- a/contracts/MintController.vy +++ b/contracts/MintController.vy @@ -16,7 +16,8 @@ initializes: core # Usually a bad practice to expose through # `__interface__` but this contract is just -# an adapter for the constructor of the Controller +# an adapter to make the construct of Controller +# compatible with the old mint factory. exports: core.__interface__ @@ -37,8 +38,3 @@ def __init__( amm, empty(address), # to replace at deployment with view blueprint ) - - # TODO do this differently - assert extcall core.BORROWED_TOKEN.approve( - core.FACTORY.address, max_value(uint256), default_return_value=True - ) diff --git a/contracts/constants.vy b/contracts/constants.vy index 7db1dc69..40b36e68 100644 --- a/contracts/constants.vy +++ b/contracts/constants.vy @@ -4,6 +4,5 @@ MIN_TICKS_UINT: constant(uint256) = 4 MAX_TICKS: constant(int256) = 50 DEAD_SHARES: constant(uint256) = 1000 -# TODO make sure this is used everywhere WAD: constant(uint256) = 10**18 SWAD: constant(int256) = 10**18 From e198e904fb909dd037d890c4fdfecd32b0c55822 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 13:35:14 +0200 Subject: [PATCH 256/413] chore: revert on illegal value --- contracts/Controller.vy | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index eb0dff76..ea46c997 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -160,7 +160,6 @@ def __init__( COLLATERAL_PRECISION = pow_mod256(10, 18 - collateral_decimals) BORROWED_TOKEN = _borrowed_token - # TODO refactor this logic to be shared with view borrowed_decimals: uint256 = convert(staticcall BORROWED_TOKEN.decimals(), uint256) BORROWED_PRECISION = pow_mod256(10, 18 - borrowed_decimals) @@ -1173,7 +1172,7 @@ def liquidate( ), "Not enough rekt" final_debt: uint256 = debt - # TODO shouldn't clamp max + assert _frac <= WAD, "frac>100%" frac: uint256 = min(_frac, WAD) debt = unsafe_div(debt * frac + (WAD - 1), WAD) assert debt > 0 From 7b92f8d6825359e7064b204b547b96bf16645fb3 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 13:35:55 +0200 Subject: [PATCH 257/413] chore: remove dead code --- contracts/Controller.vy | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index ea46c997..9d35c9ac 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -1173,7 +1173,6 @@ def liquidate( final_debt: uint256 = debt assert _frac <= WAD, "frac>100%" - frac: uint256 = min(_frac, WAD) debt = unsafe_div(debt * frac + (WAD - 1), WAD) assert debt > 0 final_debt = unsafe_sub(final_debt, debt) From 6f19fe82d3e2082a288579a661e7500e3237f6b7 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 13:36:53 +0200 Subject: [PATCH 258/413] fix: make compiler happy --- contracts/Controller.vy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 9d35c9ac..a93d9c39 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -1173,7 +1173,7 @@ def liquidate( final_debt: uint256 = debt assert _frac <= WAD, "frac>100%" - debt = unsafe_div(debt * frac + (WAD - 1), WAD) + debt = unsafe_div(debt * _frac + (WAD - 1), WAD) assert debt > 0 final_debt = unsafe_sub(final_debt, debt) @@ -1183,7 +1183,7 @@ def liquidate( # where h is health limit. # This is less than full h discount but more than no discount xy: uint256[2] = extcall AMM.withdraw( - user, self._get_f_remove(frac, health_limit) + user, self._get_f_remove(_frac, health_limit) ) # [stable, collateral] # x increase in same block -> price up -> good From 8fdc351fb6f22fc911aafb51d0db7f052723a96e Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 14:21:47 +0200 Subject: [PATCH 259/413] refactor: use token lib --- contracts/AMM.vy | 19 +++------- contracts/Controller.vy | 77 +++++++++++++++----------------------- contracts/lib/token_lib.vy | 2 +- 3 files changed, 38 insertions(+), 60 deletions(-) diff --git a/contracts/AMM.vy b/contracts/AMM.vy index eae19fc6..307ff306 100644 --- a/contracts/AMM.vy +++ b/contracts/AMM.vy @@ -44,6 +44,8 @@ from contracts.interfaces import IERC20 from contracts import constants as c +from contracts.lib import token_lib as tkn + # TODO common constants MAX_TICKS: constant(int256) = 50 @@ -167,15 +169,6 @@ def __init__( MAX_ORACLE_DN_POW = pow -@internal -def approve_max(token: IERC20, _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 extcall token.approve(_admin, max_value(uint256), default_return_value=True) - - @external def set_admin(_admin: address): """ @@ -184,8 +177,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 @@ -1135,8 +1128,8 @@ def _exchange(i: uint256, j: uint256, amount: uint256, minmax_amount: uint256, _ ), max_outsize=32, revert_on_failure=False) - assert extcall in_coin.transferFrom(msg.sender, self, in_amount_done, default_return_value=True) - assert extcall 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] diff --git a/contracts/Controller.vy b/contracts/Controller.vy index a93d9c39..e44653be 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -24,6 +24,8 @@ from contracts.interfaces import IControllerView as IView implements: IController implements: IView +from contracts.lib import token_lib as tkn + from snekmate.utils import math ################################################################ @@ -568,22 +570,6 @@ def calculate_debt_n1( return self._calculate_debt_n1(collateral, debt, N, user) -@internal -def transferFrom(token: IERC20, _from: address, _to: address, amount: uint256): - # TODO: use contracts.lib.token_lib.transferFrom - if amount > 0: - assert extcall token.transferFrom( - _from, _to, amount, default_return_value=True - ) - - -@internal -def transfer(token: IERC20, _to: address, amount: uint256): - # TODO: use contracts.lib.token_lib.transfer - if amount > 0: - assert extcall token.transfer(_to, amount, default_return_value=True) - - @internal @view def _check_loan_exists(debt: uint256): @@ -644,7 +630,7 @@ def _create_loan( more_collateral: uint256 = 0 if callbacker != empty(address): - self.transfer(BORROWED_TOKEN, callbacker, debt) + 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, @@ -694,13 +680,13 @@ def _create_loan( user=_for, collateral_increase=total_collateral, loan_increase=debt ) - self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) + tkn.transfer_from(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) if more_collateral > 0: - self.transferFrom( + tkn.transfer_from( COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral ) if callbacker == empty(address): - self.transfer(BORROWED_TOKEN, _for, debt) + tkn.transfer(BORROWED_TOKEN, _for, debt) return debt @@ -811,7 +797,7 @@ 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() @@ -826,7 +812,7 @@ 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() @@ -869,7 +855,7 @@ def _borrow_more( more_collateral: uint256 = 0 if callbacker != empty(address): - self.transfer(BORROWED_TOKEN, callbacker, debt) + 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, @@ -885,13 +871,13 @@ def _borrow_more( collateral + more_collateral, debt, _for, False, False ) - self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) + tkn.transfer_from(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) if more_collateral > 0: - self.transferFrom( + tkn.transfer_from( COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral ) if callbacker == empty(address): - self.transfer(BORROWED_TOKEN, _for, debt) + tkn.transfer(BORROWED_TOKEN, _for, debt) self.processed += debt self._save_rate() @@ -941,7 +927,7 @@ def repay( if callbacker != empty(address): assert approval xy = extcall AMM.withdraw(_for, WAD) - self.transferFrom(COLLATERAL_TOKEN, AMM.address, callbacker, xy[1]) + tkn.transfer_from(COLLATERAL_TOKEN, AMM.address, callbacker, xy[1]) cb = self.execute_callback( callbacker, CALLBACK_REPAY, _for, xy[0], xy[1], debt, calldata ) @@ -961,31 +947,31 @@ def repay( if xy[0] > 0: # Only allow full repayment when underwater for the sender to do assert approval - self.transferFrom(BORROWED_TOKEN, AMM.address, self, xy[0]) + tkn.transfer_from(BORROWED_TOKEN, AMM.address, self, xy[0]) total_borrowed += xy[0] if cb.borrowed > 0: - self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.borrowed) + tkn.transfer_from(BORROWED_TOKEN, callbacker, self, cb.borrowed) total_borrowed += cb.borrowed if total_borrowed < d_debt: _d_debt_effective: uint256 = unsafe_sub( d_debt, xy[0] + cb.borrowed ) # <= _d_debt - self.transferFrom( + tkn.transfer_from( BORROWED_TOKEN, msg.sender, self, _d_debt_effective ) total_borrowed += _d_debt_effective if total_borrowed > d_debt: - self.transfer( + tkn.transfer( BORROWED_TOKEN, _for, unsafe_sub(total_borrowed, d_debt) ) # Transfer collateral to _for if callbacker == empty(address): if xy[1] > 0: - self.transferFrom(COLLATERAL_TOKEN, AMM.address, _for, xy[1]) + tkn.transfer_from(COLLATERAL_TOKEN, AMM.address, _for, xy[1]) else: if cb.collateral > 0: - self.transferFrom( + tkn.transfer_from( COLLATERAL_TOKEN, callbacker, _for, cb.collateral ) self._remove_from_list(_for) @@ -1035,9 +1021,9 @@ def repay( assert self._health(_for, debt, False, liquidation_discount) > 0 if cb.borrowed > 0: - self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.borrowed) + tkn.transfer_from(BORROWED_TOKEN, callbacker, self, cb.borrowed) if _d_debt > 0: - self.transferFrom(BORROWED_TOKEN, msg.sender, self, _d_debt) + tkn.transfer_from(BORROWED_TOKEN, msg.sender, self, _d_debt) log IController.UserState( user=_for, @@ -1053,7 +1039,6 @@ def repay( self.loan[_for] = IController.Loan(initial_debt=debt, rate_mul=rate_mul) self._update_total_debt(d_debt, rate_mul, False) - # TODO unify naming between debt and d_debt self.repaid += d_debt self._save_rate() @@ -1191,20 +1176,20 @@ def liquidate( 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]) + tkn.transfer_from(COLLATERAL_TOKEN, AMM.address, callbacker, xy[1]) # Callback cb: IController.CallbackData = self.execute_callback( callbacker, @@ -1217,22 +1202,22 @@ def liquidate( ) assert cb.borrowed >= to_repay, "no enough proceeds" if cb.borrowed > to_repay: - self.transferFrom( + tkn.transfer_from( BORROWED_TOKEN, callbacker, msg.sender, unsafe_sub(cb.borrowed, to_repay), ) - self.transferFrom(BORROWED_TOKEN, callbacker, self, to_repay) - self.transferFrom( + 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( + tkn.transfer_from( BORROWED_TOKEN, AMM.address, msg.sender, @@ -1453,7 +1438,7 @@ def _collect_fees(admin_fee: uint256) -> uint256: if to_be_repaid > processed: self.processed = to_be_repaid fees: uint256 = unsafe_sub(to_be_repaid, processed) * admin_fee // WAD - self.transfer(BORROWED_TOKEN, _to, fees) + tkn.transfer(BORROWED_TOKEN, _to, fees) log IController.CollectFees(amount=fees, new_supply=loan.initial_debt) return fees else: diff --git a/contracts/lib/token_lib.vy b/contracts/lib/token_lib.vy index e6c7a22b..551d65e2 100644 --- a/contracts/lib/token_lib.vy +++ b/contracts/lib/token_lib.vy @@ -1,4 +1,4 @@ -# TODO missing pragmas +# pragma version 0.4.3 from contracts.interfaces import IERC20 From c95a645c201b7e2a609d61bab4b1380d3fb42f88 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 14:22:05 +0200 Subject: [PATCH 260/413] docs: add comment on bytecode savings --- contracts/Controller.vy | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index e44653be..bd0b0f3b 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -573,6 +573,7 @@ def calculate_debt_n1( @internal @view def _check_loan_exists(debt: uint256): + # Abstraction to save bytecode on error messages assert debt > 0, "Loan doesn't exist" From 865f14825b965c18358e66be3127e9d46a7e7f98 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 14:37:24 +0200 Subject: [PATCH 261/413] test: fix broken test --- tests/lending/test_vault.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/lending/test_vault.py b/tests/lending/test_vault.py index 6f554226..fafc4fe7 100644 --- a/tests/lending/test_vault.py +++ b/tests/lending/test_vault.py @@ -9,11 +9,23 @@ invariant, ) +SECONDS_PER_YEAR = 365 * 86400 # TODO get this from contract directly DEAD_SHARES = 1000 +@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""" From af2007c2e844504018ff737de33f9ce47df19f84 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 14:38:37 +0200 Subject: [PATCH 262/413] test: unify DEAD_SHARES constant --- scripts/deploy-lending-fraxtal.py | 2 +- tests/amm/test_deposit_withdraw.py | 2 +- tests/lending/test_fuzz_max_borrowable.py | 2 +- tests/lending/test_health_calculator_stateful.py | 2 +- tests/lending/test_st_interest_conservation.py | 3 ++- tests/lending/test_vault.py | 5 ++--- tests/stableborrow/test_create_repay_stateful.py | 2 +- tests/stableborrow/test_st_interest_conservation.py | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/scripts/deploy-lending-fraxtal.py b/scripts/deploy-lending-fraxtal.py index c6317985..f5d2dca0 100644 --- a/scripts/deploy-lending-fraxtal.py +++ b/scripts/deploy-lending-fraxtal.py @@ -135,7 +135,7 @@ def account_load(fname): vault_impl = boa.load("contracts/lending/Vault.vy") price_oracle_impl = boa.load_partial( "contracts/price_oracles/CryptoFromPool.vy" - ).deploy_as_blueprint() # XXX + ).deploy_as_blueprint() mpolicy_impl = boa.load_partial( "contracts/mpolicies/SemilogMonetaryPolicy.vy" ).deploy_as_blueprint() diff --git a/tests/amm/test_deposit_withdraw.py b/tests/amm/test_deposit_withdraw.py index 90fa70f6..0c61e4be 100644 --- a/tests/amm/test_deposit_withdraw.py +++ b/tests/amm/test_deposit_withdraw.py @@ -5,7 +5,7 @@ from ..utils import mint_for_testing -DEAD_SHARES = 10**3 +from tests.utils.constants import DEAD_SHARES @given( diff --git a/tests/lending/test_fuzz_max_borrowable.py b/tests/lending/test_fuzz_max_borrowable.py index f6c553df..63224c1c 100644 --- a/tests/lending/test_fuzz_max_borrowable.py +++ b/tests/lending/test_fuzz_max_borrowable.py @@ -4,7 +4,7 @@ from hypothesis import strategies as st -DEAD_SHARES = 1000 +from tests.utils.constants import DEAD_SHARES @given( diff --git a/tests/lending/test_health_calculator_stateful.py b/tests/lending/test_health_calculator_stateful.py index f9499c62..0c8a1cef 100644 --- a/tests/lending/test_health_calculator_stateful.py +++ b/tests/lending/test_health_calculator_stateful.py @@ -15,7 +15,7 @@ import pytest -DEAD_SHARES = 1000 +from tests.utils.constants import DEAD_SHARES class AllGood(Exception): diff --git a/tests/lending/test_st_interest_conservation.py b/tests/lending/test_st_interest_conservation.py index 8c4abcf2..e27c2f16 100644 --- a/tests/lending/test_st_interest_conservation.py +++ b/tests/lending/test_st_interest_conservation.py @@ -9,7 +9,8 @@ ) -DEAD_SHARES = 1000 +from tests.utils.constants import DEAD_SHARES + MIN_RATE = 10**15 // (365 * 86400) # 0.1% MAX_RATE = 10**19 // (365 * 86400) # 1000% diff --git a/tests/lending/test_vault.py b/tests/lending/test_vault.py index fafc4fe7..6d7cf355 100644 --- a/tests/lending/test_vault.py +++ b/tests/lending/test_vault.py @@ -9,10 +9,9 @@ invariant, ) -SECONDS_PER_YEAR = 365 * 86400 +from tests.utils.constants import DEAD_SHARES -# TODO get this from contract directly -DEAD_SHARES = 1000 +SECONDS_PER_YEAR = 365 * 86400 @pytest.fixture(scope="module") diff --git a/tests/stableborrow/test_create_repay_stateful.py b/tests/stableborrow/test_create_repay_stateful.py index d219cfae..5390be00 100644 --- a/tests/stableborrow/test_create_repay_stateful.py +++ b/tests/stableborrow/test_create_repay_stateful.py @@ -13,7 +13,7 @@ ) -DEAD_SHARES = 1000 +from tests.utils.constants import DEAD_SHARES class StatefulLendBorrow(RuleBasedStateMachine): diff --git a/tests/stableborrow/test_st_interest_conservation.py b/tests/stableborrow/test_st_interest_conservation.py index 6ffb6863..2fbb61e0 100644 --- a/tests/stableborrow/test_st_interest_conservation.py +++ b/tests/stableborrow/test_st_interest_conservation.py @@ -9,7 +9,7 @@ ) -DEAD_SHARES = 1000 +from tests.utils.constants import DEAD_SHARES class StatefulLendBorrow(RuleBasedStateMachine): From d718b6b9d102c2a2e9bcb2d5907989b3281ffaca Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 16:49:52 +0200 Subject: [PATCH 263/413] test: use deployers for consistency --- tests/utils/deployers.py | 34 +++++-------------- .../test_partial_repay_zap.py | 5 +-- 2 files changed, 11 insertions(+), 28 deletions(-) diff --git a/tests/utils/deployers.py b/tests/utils/deployers.py index fa32b351..70a853ac 100644 --- a/tests/utils/deployers.py +++ b/tests/utils/deployers.py @@ -28,6 +28,7 @@ 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( @@ -38,26 +39,14 @@ AMM_DEPLOYER = boa.load_partial( BASE_CONTRACT_PATH + "AMM.vy", compiler_args=compiler_args_default ) -# Controller.vy has #pragma optimize codesize 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 ) -controller_view_impl = CONTROLLER_VIEW_DEPLOYER.deploy_as_blueprint() -# MintController.vy has #pragma optimize codesize -# view_impl address has to be set in MintController.vy -with open(BASE_CONTRACT_PATH + "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_impl.address},", - 1, -) -assert f"{controller_view_impl.address}," in mint_controller_code -MINT_CONTROLLER_DEPLOYER = boa.loads_partial( - mint_controller_code, 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 @@ -88,6 +77,11 @@ 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 +) + # Monetary policies - all have no pragma CONSTANT_MONETARY_POLICY_DEPLOYER = boa.load_partial( TESTING_CONTRACT_PATH + "ConstantMonetaryPolicy.vy", @@ -191,9 +185,6 @@ VOTING_ESCROW_DEPLOYER = boa.load_partial( TESTING_CONTRACT_PATH + "VotingEscrow.vy", compiler_args=compiler_args_default ) -VE_DELEGATION_MOCK_DEPLOYER = boa.load_partial( - TESTING_CONTRACT_PATH + "VEDelegationMock.vy", compiler_args=compiler_args_default -) GAUGE_CONTROLLER_DEPLOYER = boa.load_partial( TESTING_CONTRACT_PATH + "GaugeController.vy", compiler_args=compiler_args_default ) @@ -247,15 +238,6 @@ SWAP_FACTORY_DEPLOYER = boa.load_partial( TESTING_CONTRACT_PATH + "SwapFactory.vy", compiler_args=compiler_args_default ) -OPTIMIZE_MATH_DEPLOYER = boa.load_partial( - TESTING_CONTRACT_PATH + "OptimizeMath.vy", compiler_args=compiler_args_default -) -TEST_PACKING_DEPLOYER = boa.load_partial( - TESTING_CONTRACT_PATH + "TestPacking.vy", compiler_args=compiler_args_default -) -OLD_AMM_DEPLOYER = boa.load_partial( - TESTING_CONTRACT_PATH + "OldAMM.vy", compiler_args=compiler_args_default -) # LP oracle testing contracts MOCK_STABLE_SWAP_DEPLOYER = boa.load_partial( diff --git a/tests/zaps/partial_liquidation/test_partial_repay_zap.py b/tests/zaps/partial_liquidation/test_partial_repay_zap.py index 4d77aad5..1d0c6e1f 100644 --- a/tests/zaps/partial_liquidation/test_partial_repay_zap.py +++ b/tests/zaps/partial_liquidation/test_partial_repay_zap.py @@ -1,12 +1,13 @@ import boa import pytest +from tests.utils.deployers import PARTIAL_REPAY_ZAP_DEPLOYER + @pytest.fixture(scope="module") def partial_repay_zap(admin): with boa.env.prank(admin): - # TODO this needs to be moved to deployers - return boa.load("contracts/zaps/PartialRepayZap.vy", 5 * 10**16, 1 * 10**16) + return PARTIAL_REPAY_ZAP_DEPLOYER.deploy(5 * 10**16, 1 * 10**16) @pytest.fixture(scope="module") From 2c0a019bdf0045f2bbf1a76938bfe8ff7953e8a5 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 16:52:47 +0200 Subject: [PATCH 264/413] test: offboarding is now in deployers --- .../stableborrow/stabilize/unitary/test_pk_offboarding.py | 7 +++---- tests/utils/deployers.py | 4 ++++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/stableborrow/stabilize/unitary/test_pk_offboarding.py b/tests/stableborrow/stabilize/unitary/test_pk_offboarding.py index d8aa1382..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", @@ -14,10 +16,7 @@ @pytest.fixture(scope="module") def offboarding(receiver, admin, peg_keepers): - # TODO should come from deployers - hr = boa.load( - "contracts/stabilizer/PegKeeperOffboarding.vy", receiver, admin, admin - ) + 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) diff --git a/tests/utils/deployers.py b/tests/utils/deployers.py index 70a853ac..f3e0e862 100644 --- a/tests/utils/deployers.py +++ b/tests/utils/deployers.py @@ -166,6 +166,10 @@ 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( From d36f76e2559177f605b02e2bad540c47f5179099 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 16:54:19 +0200 Subject: [PATCH 265/413] chore: move test into ci covered folder --- tests/controller/test_packing.py | 36 ++++++++++++ tests/test_math.py | 97 -------------------------------- tests/test_packing.py | 26 --------- 3 files changed, 36 insertions(+), 123 deletions(-) create mode 100644 tests/controller/test_packing.py delete mode 100644 tests/test_math.py delete mode 100644 tests/test_packing.py 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/test_math.py b/tests/test_math.py deleted file mode 100644 index 392c7b56..00000000 --- a/tests/test_math.py +++ /dev/null @@ -1,97 +0,0 @@ -import pytest -import boa -from math import log2, sqrt, exp, log -from hypothesis import given, settings -from hypothesis import strategies as st -from tests.utils.deployers import OPTIMIZE_MATH_DEPLOYER - -SETTINGS = dict(max_examples=2000) - - -@pytest.fixture(scope="module") -def optimized_math(admin): - with boa.env.prank(admin): - return OPTIMIZE_MATH_DEPLOYER.deploy() - - -@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 60315723..00000000 --- a/tests/test_packing.py +++ /dev/null @@ -1,26 +0,0 @@ -import boa -import pytest -from hypothesis import strategies as st -from hypothesis import given, settings -from tests.utils.deployers import TEST_PACKING_DEPLOYER - -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 TEST_PACKING_DEPLOYER.deploy() - - -@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 From 575d0d60f8b3942f38919b7cd6e3151cd9fdb07b Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 16:54:31 +0200 Subject: [PATCH 266/413] chore: remove TODO --- contracts/LMCallback.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/LMCallback.vy b/contracts/LMCallback.vy index 25ab9a94..168aeff3 100644 --- a/contracts/LMCallback.vy +++ b/contracts/LMCallback.vy @@ -158,7 +158,7 @@ def _checkpoint_collateral_shares(n_start: int256, collateral_per_share: DynArra 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) From fd291a2b08b99e9e163fc7df36889a6761694b52 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 16:54:44 +0200 Subject: [PATCH 267/413] chore: remove old testing code --- contracts/testing/OldAMM.vy | 1731 ------------------------- contracts/testing/OptimizeMath.vy | 338 ----- contracts/testing/VEDelegationMock.vy | 18 - 3 files changed, 2087 deletions(-) delete mode 100644 contracts/testing/OldAMM.vy delete mode 100644 contracts/testing/OptimizeMath.vy delete mode 100644 contracts/testing/VEDelegationMock.vy 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/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) From cfa241530917d1e56aa074b4b28a4d580198fdbd Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 16:57:47 +0200 Subject: [PATCH 268/413] test: add missing constant --- tests/utils/constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/utils/constants.py b/tests/utils/constants.py index 6e3a5389..f07bd0d8 100644 --- a/tests/utils/constants.py +++ b/tests/utils/constants.py @@ -11,6 +11,7 @@ # Constants from contracts/constants.vy WAD = CONSTANTS_DEPLOYER._constants.WAD SWAD = CONSTANTS_DEPLOYER._constants.SWAD +DEAD_SHARES = CONSTANTS_DEPLOYER._constants.DEAD_SHARES MAX_ORACLE_PRICE_DEVIATION = CONTROLLER_DEPLOYER._constants.MAX_ORACLE_PRICE_DEVIATION MIN_TICKS = CONTROLLER_DEPLOYER._constants.MIN_TICKS From a7e4e42bb9331efc4f56591e7cecb3d64b2492a9 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 18:46:23 +0200 Subject: [PATCH 269/413] test: unify fixture through Llamalend class --- tests/lm_callback/conftest.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/lm_callback/conftest.py b/tests/lm_callback/conftest.py index b57a230e..203b4ef9 100644 --- a/tests/lm_callback/conftest.py +++ b/tests/lm_callback/conftest.py @@ -8,7 +8,6 @@ STABLECOIN_DEPLOYER, WETH_DEPLOYER, CONTROLLER_FACTORY_DEPLOYER, - MINT_CONTROLLER_DEPLOYER, AMM_DEPLOYER, CONSTANT_MONETARY_POLICY_DEPLOYER, LM_CALLBACK_DEPLOYER, @@ -82,9 +81,8 @@ def controller_prefactory(stablecoin, weth, admin, accounts): @pytest.fixture(scope="module") -def controller_impl(admin): - with boa.env.prank(admin): - return MINT_CONTROLLER_DEPLOYER.deploy_as_blueprint() +def controller_impl(proto): + return proto.blueprints.mint_controller @pytest.fixture(scope="module") From 757c7f1bbe0f14fceb1ffd72ad54286766775c53 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 18:48:04 +0200 Subject: [PATCH 270/413] chore: IMintController -> IController --- contracts/Controller.vy | 3 +-- contracts/ControllerView.vy | 2 +- contracts/interfaces/{IMintController.vyi => IController.vyi} | 0 contracts/interfaces/IControllerView.vyi | 4 ++-- contracts/interfaces/ILlamalendController.vyi | 4 ++-- contracts/interfaces/IPartialRepayZap.vyi | 2 +- contracts/lending/LLController.vy | 2 +- contracts/lending/LLControllerView.vy | 2 +- contracts/lib/liquidation_lib.vy | 4 ++-- contracts/zaps/PartialRepayZap.vy | 2 +- 10 files changed, 12 insertions(+), 13 deletions(-) rename contracts/interfaces/{IMintController.vyi => IController.vyi} (100%) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index bd0b0f3b..2ee14a58 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -17,8 +17,7 @@ from contracts.interfaces import IFactory from contracts.interfaces import IPriceOracle from contracts.interfaces import IERC20 -# TODO just rename interface of mint controller to "icontroller" -from contracts.interfaces import IMintController as IController +from contracts.interfaces import IController from contracts.interfaces import IControllerView as IView implements: IController diff --git a/contracts/ControllerView.vy b/contracts/ControllerView.vy index 3f2c3259..0e283020 100644 --- a/contracts/ControllerView.vy +++ b/contracts/ControllerView.vy @@ -10,7 +10,7 @@ """ from contracts.interfaces import IAMM -from contracts.interfaces import IMintController as IController +from contracts.interfaces import IController from contracts.interfaces import IERC20 diff --git a/contracts/interfaces/IMintController.vyi b/contracts/interfaces/IController.vyi similarity index 100% rename from contracts/interfaces/IMintController.vyi rename to contracts/interfaces/IController.vyi diff --git a/contracts/interfaces/IControllerView.vyi b/contracts/interfaces/IControllerView.vyi index 4ec0c83c..b64b949a 100644 --- a/contracts/interfaces/IControllerView.vyi +++ b/contracts/interfaces/IControllerView.vyi @@ -1,4 +1,4 @@ -from contracts.interfaces import IMintController +from contracts.interfaces import IController # Functions @@ -10,7 +10,7 @@ def health_calculator(user: address, d_collateral: int256, d_debt: int256, full: @view @external -def users_to_liquidate(_from: uint256, _limit: uint256) -> DynArray[IMintController.Position, 1000]: +def users_to_liquidate(_from: uint256, _limit: uint256) -> DynArray[IController.Position, 1000]: ... diff --git a/contracts/interfaces/ILlamalendController.vyi b/contracts/interfaces/ILlamalendController.vyi index 4dd82df6..651f0b9b 100644 --- a/contracts/interfaces/ILlamalendController.vyi +++ b/contracts/interfaces/ILlamalendController.vyi @@ -4,7 +4,7 @@ from contracts.interfaces import ILMGauge from contracts.interfaces import IAMM from contracts.interfaces import IMonetaryPolicy from contracts.interfaces import IFactory -from contracts.interfaces import IMintController +from contracts.interfaces import IController # TODO only keep diff @@ -261,7 +261,7 @@ def health(user: address, full: bool) -> int256: @view @external -def users_to_liquidate(_from: uint256, _limit: uint256) -> DynArray[IMintController.Position, 1000]: +def users_to_liquidate(_from: uint256, _limit: uint256) -> DynArray[IController.Position, 1000]: ... diff --git a/contracts/interfaces/IPartialRepayZap.vyi b/contracts/interfaces/IPartialRepayZap.vyi index 3792057e..7501e064 100644 --- a/contracts/interfaces/IPartialRepayZap.vyi +++ b/contracts/interfaces/IPartialRepayZap.vyi @@ -1,4 +1,4 @@ -from contracts.interfaces import IMintController as IController +from contracts.interfaces import IController event PartialRepay: diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy index 035e4d23..bb6f65f8 100644 --- a/contracts/lending/LLController.vy +++ b/contracts/lending/LLController.vy @@ -15,7 +15,7 @@ from contracts.interfaces import IAMM from contracts.interfaces import IMonetaryPolicy from contracts.interfaces import IVault -from contracts.interfaces import IMintController as IController +from contracts.interfaces import IController implements: IController diff --git a/contracts/lending/LLControllerView.vy b/contracts/lending/LLControllerView.vy index a5024653..b7a7af55 100644 --- a/contracts/lending/LLControllerView.vy +++ b/contracts/lending/LLControllerView.vy @@ -9,7 +9,7 @@ """ from contracts.interfaces import IERC20 -from contracts.interfaces import IMintController as IController +from contracts.interfaces import IController from contracts.interfaces import ILlamalendController from contracts.interfaces import IAMM from contracts.interfaces import IControllerView diff --git a/contracts/lib/liquidation_lib.vy b/contracts/lib/liquidation_lib.vy index 880db46e..9e3fe31f 100644 --- a/contracts/lib/liquidation_lib.vy +++ b/contracts/lib/liquidation_lib.vy @@ -1,4 +1,4 @@ -from contracts.interfaces import IMintController as IController +from contracts.interfaces import IController from contracts.interfaces import IAMM @@ -16,7 +16,7 @@ def users_with_health( """ Enumerate controller loans and return positions with health < threshold. Optionally require controller.approval(user, approval_spender). - Returns IMintController.Position entries (user, x, y, debt, health). + Returns IController.Position entries (user, x, y, debt, health). """ AMM: IAMM = staticcall controller.amm() diff --git a/contracts/zaps/PartialRepayZap.vy b/contracts/zaps/PartialRepayZap.vy index 2244f7b1..36cec441 100644 --- a/contracts/zaps/PartialRepayZap.vy +++ b/contracts/zaps/PartialRepayZap.vy @@ -11,7 +11,7 @@ from contracts.interfaces import IERC20 from contracts.interfaces import IAMM -from contracts.interfaces import IMintController as IController +from contracts.interfaces import IController from contracts import Controller as ctrl import contracts.lib.token_lib as tkn from contracts.interfaces import IPartialRepayZap as IZap From a0372460d5213094d4e2506d3220817b09ab1eb7 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 18:51:01 +0200 Subject: [PATCH 271/413] chore: tackle TODOs --- contracts/Controller.vy | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 2ee14a58..c8d79521 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -32,8 +32,6 @@ from snekmate.utils import math ################################################################ AMM: immutable(IAMM) -# TODO move this comment to init -# let's set to MIN_TICKS / A: for example, 4% max fee for A=100 MAX_AMM_FEE: immutable(uint256) A: immutable(uint256) # log(A / (A - 1)) @@ -154,6 +152,7 @@ def __init__( 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 @@ -406,7 +405,6 @@ def total_debt() -> uint256: """ @notice Total debt of this controller @dev Marked as reentrant because used by monetary policy - # TODO check if @reentrant is actually needed """ return self._get_total_debt() From 137213839442cd0f518cef08eee83ae47014ae5f Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 19:00:14 +0200 Subject: [PATCH 272/413] feat: validate monetary policy on market creation --- contracts/Controller.vy | 1 - contracts/lending/LendingFactory.vy | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index c8d79521..f302eaeb 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -141,7 +141,6 @@ def __init__( # In MintController the correct way to limit borrowing # is through the debt ceiling. - # TODO move this to mint controller self.borrow_cap = max_value(uint256) FACTORY = IFactory(msg.sender) diff --git a/contracts/lending/LendingFactory.vy b/contracts/lending/LendingFactory.vy index 543553e8..f487441f 100644 --- a/contracts/lending/LendingFactory.vy +++ b/contracts/lending/LendingFactory.vy @@ -117,8 +117,10 @@ def create( assert (_loan_discount > _liquidation_discount), "need loan_discount>liquidation_discount" A_ratio: uint256 = 10**18 * _A // (_A - 1) - # TODO validate monetary policy here - p: uint256 = (staticcall _price_oracle.price()) # This also validates price oracle ABI + + # Validate price oracle and monetary policy + extcall _monetary_policy.rate_write() + p: uint256 = (staticcall _price_oracle.price()) assert p > 0 assert extcall _price_oracle.price_w() == p From 64091d7c902e9ea992c401ce82a85cdf4d5dd088 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 19:14:09 +0200 Subject: [PATCH 273/413] test: more blueprints from llamalend class --- tests/stableborrow/conftest.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/stableborrow/conftest.py b/tests/stableborrow/conftest.py index ba7ba306..5c7a7f89 100644 --- a/tests/stableborrow/conftest.py +++ b/tests/stableborrow/conftest.py @@ -47,9 +47,8 @@ def controller_prefactory(controller_factory_impl, stablecoin, weth, admin, acco @pytest.fixture(scope="module") -def controller_impl(admin): - with boa.env.prank(admin): - return MINT_CONTROLLER_DEPLOYER.deploy_as_blueprint() +def controller_impl(proto): + return proto.blueprints.mint_controller @pytest.fixture(scope="module") From 67f0a6f021696dd8490343a71261dc517e92d294 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 19:30:57 +0200 Subject: [PATCH 274/413] test: delete ape tests Sorry ape, nobody has run these in centuries and they always get picked up by pytest --- contracts/mpolicies/SemilogMonetaryPolicy.vy | 15 +- tests_forked_ape/__init__.py | 0 tests_forked_ape/conftest.py | 351 ------------------ tests_forked_ape/test_lendborrow.py | 309 --------------- tests_forked_ape/test_registry_integration.py | 27 -- tests_forked_ape/utils.py | 96 ----- 6 files changed, 14 insertions(+), 784 deletions(-) delete mode 100644 tests_forked_ape/__init__.py delete mode 100644 tests_forked_ape/conftest.py delete mode 100644 tests_forked_ape/test_lendborrow.py delete mode 100644 tests_forked_ape/test_registry_integration.py delete mode 100644 tests_forked_ape/utils.py diff --git a/contracts/mpolicies/SemilogMonetaryPolicy.vy b/contracts/mpolicies/SemilogMonetaryPolicy.vy index b1748c79..df4daff7 100644 --- a/contracts/mpolicies/SemilogMonetaryPolicy.vy +++ b/contracts/mpolicies/SemilogMonetaryPolicy.vy @@ -133,7 +133,20 @@ def ln_int(_x: uint256) -> int256: @internal @view def calculate_rate(_for: address, d_reserves: int256, d_debt: int256) -> uint256: - total_debt: int256 = convert(Controller(_for).total_debt(), int256) + total_debt: int256 = 0 + success: bool = False + debt_data: Bytes[32] = empty(Bytes[32]) + success, debt_data = raw_call( + _for, + method_id("total_debt()"), + max_outsize=32, + is_static_call=True, + revert_on_failure=False, + ) + data_length: uint256 = len(debt_data) + if success and data_length == 32: + total_debt = convert(convert(debt_data, uint256), int256) + total_reserves: int256 = convert(BORROWED_TOKEN.balanceOf(_for), int256) + total_debt + d_reserves total_debt += d_debt assert total_debt >= 0, "Negative debt" diff --git a/tests_forked_ape/__init__.py b/tests_forked_ape/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests_forked_ape/conftest.py b/tests_forked_ape/conftest.py deleted file mode 100644 index 65f71f47..00000000 --- a/tests_forked_ape/conftest.py +++ /dev/null @@ -1,351 +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 8db199c6..00000000 --- a/tests_forked_ape/test_lendborrow.py +++ /dev/null @@ -1,309 +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 fcd88278..00000000 --- a/tests_forked_ape/test_registry_integration.py +++ /dev/null @@ -1,27 +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 2c6ff580..00000000 --- a/tests_forked_ape/utils.py +++ /dev/null @@ -1,96 +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 From 88b7e300954cce7b1a6033809f166b6e59bf23f7 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 19:31:14 +0200 Subject: [PATCH 275/413] Revert "test: delete ape tests" This reverts commit 67f0a6f021696dd8490343a71261dc517e92d294. --- contracts/mpolicies/SemilogMonetaryPolicy.vy | 15 +- tests_forked_ape/__init__.py | 0 tests_forked_ape/conftest.py | 351 ++++++++++++++++++ tests_forked_ape/test_lendborrow.py | 309 +++++++++++++++ tests_forked_ape/test_registry_integration.py | 27 ++ tests_forked_ape/utils.py | 96 +++++ 6 files changed, 784 insertions(+), 14 deletions(-) create mode 100644 tests_forked_ape/__init__.py create mode 100644 tests_forked_ape/conftest.py create mode 100644 tests_forked_ape/test_lendborrow.py create mode 100644 tests_forked_ape/test_registry_integration.py create mode 100644 tests_forked_ape/utils.py diff --git a/contracts/mpolicies/SemilogMonetaryPolicy.vy b/contracts/mpolicies/SemilogMonetaryPolicy.vy index df4daff7..b1748c79 100644 --- a/contracts/mpolicies/SemilogMonetaryPolicy.vy +++ b/contracts/mpolicies/SemilogMonetaryPolicy.vy @@ -133,20 +133,7 @@ def ln_int(_x: uint256) -> int256: @internal @view def calculate_rate(_for: address, d_reserves: int256, d_debt: int256) -> uint256: - total_debt: int256 = 0 - success: bool = False - debt_data: Bytes[32] = empty(Bytes[32]) - success, debt_data = raw_call( - _for, - method_id("total_debt()"), - max_outsize=32, - is_static_call=True, - revert_on_failure=False, - ) - data_length: uint256 = len(debt_data) - if success and data_length == 32: - total_debt = convert(convert(debt_data, uint256), int256) - + total_debt: int256 = convert(Controller(_for).total_debt(), int256) total_reserves: int256 = convert(BORROWED_TOKEN.balanceOf(_for), int256) + total_debt + d_reserves total_debt += d_debt assert total_debt >= 0, "Negative debt" diff --git a/tests_forked_ape/__init__.py b/tests_forked_ape/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests_forked_ape/conftest.py b/tests_forked_ape/conftest.py new file mode 100644 index 00000000..65f71f47 --- /dev/null +++ b/tests_forked_ape/conftest.py @@ -0,0 +1,351 @@ +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 new file mode 100644 index 00000000..8db199c6 --- /dev/null +++ b/tests_forked_ape/test_lendborrow.py @@ -0,0 +1,309 @@ +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 new file mode 100644 index 00000000..fcd88278 --- /dev/null +++ b/tests_forked_ape/test_registry_integration.py @@ -0,0 +1,27 @@ +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 new file mode 100644 index 00000000..2c6ff580 --- /dev/null +++ b/tests_forked_ape/utils.py @@ -0,0 +1,96 @@ +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 From 5635d7436f1c910258155d7bc35d6d15e27d6e19 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 19:32:45 +0200 Subject: [PATCH 276/413] test: delete ape tests Sorry ape, nobody has run these in centuries and they always get picked up by pytest --- tests_forked_ape/__init__.py | 0 tests_forked_ape/conftest.py | 351 ------------------ tests_forked_ape/test_lendborrow.py | 309 --------------- tests_forked_ape/test_registry_integration.py | 27 -- tests_forked_ape/utils.py | 96 ----- 5 files changed, 783 deletions(-) delete mode 100644 tests_forked_ape/__init__.py delete mode 100644 tests_forked_ape/conftest.py delete mode 100644 tests_forked_ape/test_lendborrow.py delete mode 100644 tests_forked_ape/test_registry_integration.py delete mode 100644 tests_forked_ape/utils.py diff --git a/tests_forked_ape/__init__.py b/tests_forked_ape/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests_forked_ape/conftest.py b/tests_forked_ape/conftest.py deleted file mode 100644 index 65f71f47..00000000 --- a/tests_forked_ape/conftest.py +++ /dev/null @@ -1,351 +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 8db199c6..00000000 --- a/tests_forked_ape/test_lendborrow.py +++ /dev/null @@ -1,309 +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 fcd88278..00000000 --- a/tests_forked_ape/test_registry_integration.py +++ /dev/null @@ -1,27 +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 2c6ff580..00000000 --- a/tests_forked_ape/utils.py +++ /dev/null @@ -1,96 +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 From 0ec16507ee7c6d825a9c4243d041e48ed56a63bb Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 19:46:35 +0200 Subject: [PATCH 277/413] fix: correct mpol validation --- contracts/interfaces/IMonetaryPolicy.vyi | 4 ++++ contracts/lending/LendingFactory.vy | 2 +- contracts/mpolicies/SemilogMonetaryPolicy.vy | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/contracts/interfaces/IMonetaryPolicy.vyi b/contracts/interfaces/IMonetaryPolicy.vyi index 161d9a67..df3d26a3 100644 --- a/contracts/interfaces/IMonetaryPolicy.vyi +++ b/contracts/interfaces/IMonetaryPolicy.vyi @@ -1,2 +1,6 @@ def rate_write() -> uint256: ... + +@view +def rate() -> uint256: + ... diff --git a/contracts/lending/LendingFactory.vy b/contracts/lending/LendingFactory.vy index f487441f..23fe4b1a 100644 --- a/contracts/lending/LendingFactory.vy +++ b/contracts/lending/LendingFactory.vy @@ -119,7 +119,7 @@ def create( A_ratio: uint256 = 10**18 * _A // (_A - 1) # Validate price oracle and monetary policy - extcall _monetary_policy.rate_write() + unused: uint256 = staticcall _monetary_policy.rate() p: uint256 = (staticcall _price_oracle.price()) assert p > 0 assert extcall _price_oracle.price_w() == p diff --git a/contracts/mpolicies/SemilogMonetaryPolicy.vy b/contracts/mpolicies/SemilogMonetaryPolicy.vy index b1748c79..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 From 60249c6106f688b856c467cf1de5a79d333d0b5a Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 20:25:39 +0200 Subject: [PATCH 278/413] test: fix incorrect testing range --- tests/stableborrow/test_liquidate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/stableborrow/test_liquidate.py b/tests/stableborrow/test_liquidate.py index 23efbc38..45ccf05d 100644 --- a/tests/stableborrow/test_liquidate.py +++ b/tests/stableborrow/test_liquidate.py @@ -91,7 +91,7 @@ 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, From 4b4ddd2004baf1af2f4df5c5ab6e4e8640f5fe0f Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 17 Sep 2025 20:54:33 +0200 Subject: [PATCH 279/413] test: fix rate validation --- contracts/interfaces/IMonetaryPolicy.vyi | 4 ++-- contracts/lending/LendingFactory.vy | 6 ++++-- .../testing/ConstantMonetaryPolicyLending.vy | 16 +++++++++++----- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/contracts/interfaces/IMonetaryPolicy.vyi b/contracts/interfaces/IMonetaryPolicy.vyi index df3d26a3..932896cb 100644 --- a/contracts/interfaces/IMonetaryPolicy.vyi +++ b/contracts/interfaces/IMonetaryPolicy.vyi @@ -1,6 +1,6 @@ -def rate_write() -> uint256: +def rate_write(_for: address = msg.sender) -> uint256: ... @view -def rate() -> uint256: +def rate(_for: address = msg.sender) -> uint256: ... diff --git a/contracts/lending/LendingFactory.vy b/contracts/lending/LendingFactory.vy index 23fe4b1a..c36321d7 100644 --- a/contracts/lending/LendingFactory.vy +++ b/contracts/lending/LendingFactory.vy @@ -118,8 +118,7 @@ def create( A_ratio: uint256 = 10**18 * _A // (_A - 1) - # Validate price oracle and monetary policy - unused: uint256 = staticcall _monetary_policy.rate() + # Validate price oracle p: uint256 = (staticcall _price_oracle.price()) assert p > 0 assert extcall _price_oracle.price_w() == p @@ -160,6 +159,9 @@ def create( 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, diff --git a/contracts/testing/ConstantMonetaryPolicyLending.vy b/contracts/testing/ConstantMonetaryPolicyLending.vy index 1cca0594..8c877489 100644 --- a/contracts/testing/ConstantMonetaryPolicyLending.vy +++ b/contracts/testing/ConstantMonetaryPolicyLending.vy @@ -2,20 +2,26 @@ from contracts.interfaces import IERC20 -rate: public(uint256) +_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 + self._rate = min_rate @external -def rate_write() -> uint256: - return self.rate +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 + self._rate = rate + + +@external +@view +def rate(_for: address = msg.sender) -> uint256: + return self._rate From 48e9a5430b5c3efb83bf0aa521bc3eccaa457d72 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 18 Sep 2025 11:57:40 +0200 Subject: [PATCH 280/413] refactor: use max approve --- contracts/Controller.vy | 4 +--- contracts/lending/LLController.vy | 7 +++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index f302eaeb..784e6d2a 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -163,9 +163,7 @@ def __init__( BORROWED_PRECISION = pow_mod256(10, 18 - borrowed_decimals) # This is useless for lending markets, but leaving it doesn't create any harm - assert extcall BORROWED_TOKEN.approve( - FACTORY.address, max_value(uint256), default_return_value=True - ) + tkn.max_approve(BORROWED_TOKEN, FACTORY.address) self._monetary_policy = monetary_policy self.liquidation_discount = liquidation_discount diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy index bb6f65f8..9a35b841 100644 --- a/contracts/lending/LLController.vy +++ b/contracts/lending/LLController.vy @@ -14,7 +14,6 @@ from contracts.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 @@ -27,6 +26,8 @@ from contracts import Controller as core initializes: core +from contracts.lib import token_lib as tkn + exports: ( # Loan management core.add_collateral, @@ -124,9 +125,7 @@ def __init__( ) core.borrow_cap = 0 - assert extcall core.BORROWED_TOKEN.approve( - VAULT.address, max_value(uint256), default_return_value=True - ) + tkn.max_approve(core.BORROWED_TOKEN, VAULT.address) @external From b1d63f4067b0c0f2a05b4e59fbf076722c3c809e Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 18 Sep 2025 16:20:36 +0400 Subject: [PATCH 281/413] fix: full repay for max_value(uint256) --- contracts/Controller.vy | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index f302eaeb..84f648bb 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -907,7 +907,8 @@ def repay( ): """ @notice Repay debt (partially or fully) - @param _d_debt The amount of debt to repay from user's wallet. If higher than the current debt - will do full repayment + @param _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 @@ -929,18 +930,16 @@ def repay( callbacker, CALLBACK_REPAY, _for, xy[0], xy[1], debt, calldata ) - total_borrowed: uint256 = _d_debt + xy[0] + cb.borrowed - assert total_borrowed > 0 # dev: no coins to repay - d_debt: uint256 = 0 + d_debt: uint256 = min(min(_d_debt, debt) + xy[0] + cb.borrowed, debt) + assert d_debt > 0 # dev: no coins to repay # If we have more borrowed tokens than the debt - full repayment and closing the position - if total_borrowed >= debt: - d_debt = debt + if d_debt >= debt: debt = 0 if callbacker == empty(address): xy = extcall AMM.withdraw(_for, WAD) - total_borrowed = 0 + total_borrowed: uint256 = 0 if xy[0] > 0: # Only allow full repayment when underwater for the sender to do assert approval @@ -983,7 +982,6 @@ def repay( active_band: int256 = staticcall AMM.active_band_with_skip() assert active_band <= max_active_band - d_debt = total_borrowed debt = unsafe_sub(debt, d_debt) ns: int256[2] = staticcall AMM.read_user_tick_numbers(_for) size: int256 = unsafe_sub(ns[1], ns[0]) From db0631eb7d1a7610973f105ffbc8a67073aca0a7 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 18 Sep 2025 16:40:57 +0200 Subject: [PATCH 282/413] feat: add sanity check to set_admin_fee --- contracts/lending/LLController.vy | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy index 9a35b841..e7052501 100644 --- a/contracts/lending/LLController.vy +++ b/contracts/lending/LLController.vy @@ -246,9 +246,10 @@ def set_borrow_cap(_borrow_cap: uint256): @external -def set_admin_fee(admin_fee: uint256): +def set_admin_fee(_admin_fee: uint256): """ - @param admin_fee The fee which should be no higher than MAX_ADMIN_FEE + @param _admin_fee The percentage of interest that goes to the admin, scaled by 1e18 """ core._check_admin() - self.admin_fee = admin_fee + assert _admin_fee <= core.WAD # dev: admin fee higher than 100% + self.admin_fee = _admin_fee From 842f7f806252001fe9537d8f7d7f9819980cf893 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 18 Sep 2025 16:47:54 +0200 Subject: [PATCH 283/413] refactor: use custom IERC4626 --- contracts/interfaces/IERC4626.vyi | 76 +++++++++++++++++++++++++++++++ contracts/lending/Vault.vy | 6 +-- 2 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 contracts/interfaces/IERC4626.vyi diff --git a/contracts/interfaces/IERC4626.vyi b/contracts/interfaces/IERC4626.vyi new file mode 100644 index 00000000..f62910dd --- /dev/null +++ b/contracts/interfaces/IERC4626.vyi @@ -0,0 +1,76 @@ +from contracts.interfaces import IERC20 + +# 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 + +# Functions +@view +def asset() -> IERC20: + ... + +@view +def totalAssets() -> uint256: + ... + +@view +def convertToShares(assetAmount: uint256) -> uint256: + ... + +@view +def convertToAssets(shareAmount: uint256) -> uint256: + ... + +@view +def maxDeposit(owner: address) -> uint256: + ... + +@view +def previewDeposit(assets: uint256) -> uint256: + ... + +def deposit(assets: uint256, receiver: address) -> uint256: + ... + +@view +def maxMint(owner: address) -> uint256: + ... + +@view +def previewMint(shares: uint256) -> uint256: + ... + +def mint(shares: uint256, receiver: address) -> uint256: + ... + +@view +def maxWithdraw(owner: address) -> uint256: + ... + +@view +def previewWithdraw(assets: uint256) -> uint256: + ... + +def withdraw(assets: uint256, receiver: address, owner: address) -> uint256: + ... + +@view +def maxRedeem(owner: address) -> uint256: + ... + +@view +def previewRedeem(shares: uint256) -> uint256: + ... + +def redeem(shares: uint256, receiver: address, owner: address) -> uint256: + ... diff --git a/contracts/lending/Vault.vy b/contracts/lending/Vault.vy index 12eabf1e..2f00811a 100644 --- a/contracts/lending/Vault.vy +++ b/contracts/lending/Vault.vy @@ -1,6 +1,5 @@ # pragma version 0.4.3 # pragma optimize codesize -# pragma evm-version shanghai """ @title LlamaLend Vault @notice ERC4626+ Vault for lending using LLAMMA algorithm @@ -9,8 +8,7 @@ """ from contracts.interfaces import IERC20 -from ethereum.ercs import IERC4626 - +from contracts.interfaces import IERC4626 from contracts.interfaces import IAMM from contracts.interfaces import ILlamalendController as IController from contracts.interfaces import IFactory @@ -18,7 +16,7 @@ from contracts.interfaces import IFactory from contracts import constants as c implements: IERC20 -# implements: IERC4626 TODO fix this +implements: IERC4626 event SetMaxSupply: From e61c12d0eeeb49d9c5855e6a5cedd3a9d7766f0d Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 18 Sep 2025 16:48:47 +0200 Subject: [PATCH 284/413] chore: move borrow cap test to new folder layout --- tests/README.md | 5 +++++ tests/unitary/lending/conftest.py | 6 ++++++ .../lending/ll_controller}/test_set_borrow_cap.py | 0 3 files changed, 11 insertions(+) create mode 100644 tests/README.md create mode 100644 tests/unitary/lending/conftest.py rename tests/{lending => unitary/lending/ll_controller}/test_set_borrow_cap.py (100%) 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/unitary/lending/conftest.py b/tests/unitary/lending/conftest.py new file mode 100644 index 00000000..1541e357 --- /dev/null +++ b/tests/unitary/lending/conftest.py @@ -0,0 +1,6 @@ +import pytest + + +@pytest.fixture(scope="module") +def market_type(): + return "lending" diff --git a/tests/lending/test_set_borrow_cap.py b/tests/unitary/lending/ll_controller/test_set_borrow_cap.py similarity index 100% rename from tests/lending/test_set_borrow_cap.py rename to tests/unitary/lending/ll_controller/test_set_borrow_cap.py From 2b761d223c28ae347c0b91b4b85dfe327cd37c77 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 18 Sep 2025 16:49:19 +0200 Subject: [PATCH 285/413] style: more interface types --- contracts/zaps/CreateFromPool.vy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/zaps/CreateFromPool.vy b/contracts/zaps/CreateFromPool.vy index f5a09288..5e1a293b 100644 --- a/contracts/zaps/CreateFromPool.vy +++ b/contracts/zaps/CreateFromPool.vy @@ -18,8 +18,8 @@ POOL_PRICE_ORACLE_BLUEPRINT: public(immutable(address)) @deploy -def __init__(_factory: address, _pool_price_oracle_blueprint: address): - FACTORY = ILendingFactory(_factory) +def __init__(_factory: ILendingFactory, _pool_price_oracle_blueprint: address): + FACTORY = _factory POOL_PRICE_ORACLE_BLUEPRINT = _pool_price_oracle_blueprint From ae608f54b6a721fafe2703ef0f85e1f8c542ae68 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 18 Sep 2025 19:34:09 +0200 Subject: [PATCH 286/413] test: market_type is stricter --- tests/conftest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 34239751..a353111a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -181,7 +181,7 @@ def market( liquidation_discount=liquidation_discount, debt_ceiling=seed_liquidity, ) - else: + elif market_type == "lending": return proto.create_lending_market( borrowed_token=borrowed_token, collateral_token=collateral_token, @@ -196,6 +196,8 @@ def market( seed_amount=seed_liquidity, mpolicy_deployer=lending_monetary_policy, ) + else: + raise ValueError("Incorrect market type fixture") @pytest.fixture(scope="module") From e411f48d8d88cdf94c9696011a0aeb01b35938a2 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 18 Sep 2025 19:34:36 +0200 Subject: [PATCH 287/413] test: test internal borrow caps --- .../test_internal_update_total_debt.py | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 tests/unitary/controller/test_internal_update_total_debt.py diff --git a/tests/unitary/controller/test_internal_update_total_debt.py b/tests/unitary/controller/test_internal_update_total_debt.py new file mode 100644 index 00000000..c1a3c06a --- /dev/null +++ b/tests/unitary/controller/test_internal_update_total_debt.py @@ -0,0 +1,47 @@ +# You can access update_total_debt using + +import boa +import pytest +from textwrap import dedent + +from tests.utils.constants import WAD + + +BORROW_CAP = 10**9 + + +@pytest.fixture(scope="module", autouse=True) +def expose_internal(controller): + controller.inject_function( + dedent(""" + @external + def update_total_debt(d_debt: uint256, rate_mul: uint256, is_increase: bool) -> core.IController.Loan: + return core._update_total_debt(d_debt, rate_mul, is_increase) + """) + ) + + +def test_default_behavior(controller): + controller.eval("core._total_debt.initial_debt = 0") + controller.eval(f"core._total_debt.rate_mul = {WAD}") + controller.eval(f"core.borrow_cap = {BORROW_CAP}") + + controller.inject.update_total_debt(BORROW_CAP - 1, WAD, True) + current_debt = controller.eval("core._total_debt.initial_debt") + assert current_debt == BORROW_CAP - 1 + + controller.inject.update_total_debt(1, WAD, True) + current_debt = controller.eval("core._total_debt.initial_debt") + assert current_debt == BORROW_CAP + + +def test_exceeding_borrow_cap_reverts(controller): + controller.eval(f"core._total_debt.rate_mul = {WAD}") + controller.eval(f"core.borrow_cap = {BORROW_CAP}") + controller.eval(f"core._total_debt.initial_debt = {BORROW_CAP}") + + with boa.reverts("Borrow cap exceeded"): + controller.inject.update_total_debt(1, WAD, True) + + current_debt = controller.eval("core._total_debt.initial_debt") + assert current_debt == BORROW_CAP From f6e4f400f70849ee0795b37a80b07552ac261578 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 18 Sep 2025 19:34:58 +0200 Subject: [PATCH 288/413] ci: add unitary tests folder --- .github/workflows/test.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 73ae9213..214a0788 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -35,6 +35,7 @@ jobs: fail-fast: false matrix: folder: + - "tests/unitary" - "tests/flashloan" - "tests/lm_callback" - "tests/controller" From d021fcc86075972a5812468ecd36223afd088a87 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 18 Sep 2025 19:56:14 +0200 Subject: [PATCH 289/413] test: remove useless assert --- tests/unitary/controller/test_internal_update_total_debt.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/unitary/controller/test_internal_update_total_debt.py b/tests/unitary/controller/test_internal_update_total_debt.py index c1a3c06a..6fb3da1e 100644 --- a/tests/unitary/controller/test_internal_update_total_debt.py +++ b/tests/unitary/controller/test_internal_update_total_debt.py @@ -42,6 +42,3 @@ def test_exceeding_borrow_cap_reverts(controller): with boa.reverts("Borrow cap exceeded"): controller.inject.update_total_debt(1, WAD, True) - - current_debt = controller.eval("core._total_debt.initial_debt") - assert current_debt == BORROW_CAP From e444528fdc8accc379a82286fab2c21227784287 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 18 Sep 2025 19:56:45 +0200 Subject: [PATCH 290/413] test: test borrow caps on create loan --- tests/unitary/controller/test_create_loan.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tests/unitary/controller/test_create_loan.py diff --git a/tests/unitary/controller/test_create_loan.py b/tests/unitary/controller/test_create_loan.py new file mode 100644 index 00000000..d6af875b --- /dev/null +++ b/tests/unitary/controller/test_create_loan.py @@ -0,0 +1,19 @@ +import boa + +from tests.utils.constants import WAD + +BORROW_CAP = 10**9 +COLLATERAL = 10**21 +N_BANDS = 5 + + +def test_borrow_cap_reverts_when_exceeded(controller, collateral_token): + controller.eval(f"core.borrow_cap = {BORROW_CAP}") + controller.eval(f"core._total_debt.initial_debt = {BORROW_CAP}") + controller.eval(f"core._total_debt.rate_mul = {WAD}") + + boa.deal(collateral_token, boa.env.eoa, COLLATERAL) + collateral_token.approve(controller, 2**256 - 1) + + with boa.reverts("Borrow cap exceeded"): + controller.create_loan(COLLATERAL, 1, N_BANDS) From 033dd6bdefd2751a438b1114f8763c82b54cadc3 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 18 Sep 2025 20:22:10 +0200 Subject: [PATCH 291/413] test: add more tests for borrow caps --- tests/e2e/test_borrow_caps.py | 74 +++++++++++++++++++ .../test_internal_add_collateral_borrow.py | 51 +++++++++++++ .../lending/ll_controller/test_borrow_cap.py | 1 + 3 files changed, 126 insertions(+) create mode 100644 tests/e2e/test_borrow_caps.py create mode 100644 tests/unitary/controller/test_internal_add_collateral_borrow.py create mode 100644 tests/unitary/lending/ll_controller/test_borrow_cap.py diff --git a/tests/e2e/test_borrow_caps.py b/tests/e2e/test_borrow_caps.py new file mode 100644 index 00000000..cb36a7a0 --- /dev/null +++ b/tests/e2e/test_borrow_caps.py @@ -0,0 +1,74 @@ +import boa +import pytest + +from tests.utils.constants import MAX_UINT256 + +COLLATERAL = 10**21 +EXTRA_COLLATERAL = 10**21 +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 + max_debt = controller.max_borrowable(COLLATERAL, N_BANDS, 0, boa.env.eoa) + debt_cap = max(1, max_debt // 4) + controller.set_borrow_cap(debt_cap, sender=admin) + controller.create_loan(COLLATERAL, debt_cap, N_BANDS) + 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(1, max_total - current_debt) + controller.set_borrow_cap(current_debt + extra_debt, sender=admin) + controller.borrow_more(0, extra_debt) + 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 + controller.repay(MAX_UINT256) + 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/unitary/controller/test_internal_add_collateral_borrow.py b/tests/unitary/controller/test_internal_add_collateral_borrow.py new file mode 100644 index 00000000..6444c810 --- /dev/null +++ b/tests/unitary/controller/test_internal_add_collateral_borrow.py @@ -0,0 +1,51 @@ +import boa +import pytest +from textwrap import dedent + +from tests.utils.constants import MAX_UINT256, WAD + + +BORROW_CAP = 10**9 +COLLATERAL = 10**21 +N_BANDS = 5 + + +@pytest.fixture(scope="module", autouse=True) +def expose_internal(controller): + controller.inject_function( + dedent( + """ + @external + def add_collateral_borrow( + d_collateral: uint256, + d_debt: uint256, + _for: address, + remove_collateral: bool, + check_rounding: bool, + ): + core._add_collateral_borrow( + d_collateral, d_debt, _for, remove_collateral, check_rounding + ) + """ + ) + ) + + +def test_borrow_cap_reverts(controller, collateral_token): + controller.eval("core._total_debt.initial_debt = 0") + controller.eval(f"core._total_debt.rate_mul = {WAD}") + controller.eval(f"core.borrow_cap = {BORROW_CAP}") + + boa.deal(collateral_token, boa.env.eoa, COLLATERAL) + collateral_token.approve(controller, MAX_UINT256) + + debt = BORROW_CAP - 1 + + controller.create_loan(COLLATERAL, debt, N_BANDS) + + controller.eval(f"core.borrow_cap = {debt}") + controller.eval(f"core._total_debt.initial_debt = {debt}") + controller.eval(f"core._total_debt.rate_mul = {WAD}") + + with boa.reverts("Borrow cap exceeded"): + controller.inject.add_collateral_borrow(0, 1, boa.env.eoa, False, False) diff --git a/tests/unitary/lending/ll_controller/test_borrow_cap.py b/tests/unitary/lending/ll_controller/test_borrow_cap.py new file mode 100644 index 00000000..4cbb8406 --- /dev/null +++ b/tests/unitary/lending/ll_controller/test_borrow_cap.py @@ -0,0 +1 @@ +# TODO should be zero at deployment From bd192cd1faa69272fbb1cd513aca48bcf5cbcf84 Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 19 Sep 2025 12:06:14 +0200 Subject: [PATCH 292/413] test: default value for borrow caps --- .../lending/ll_controller/test_borrow_cap.py | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/tests/unitary/lending/ll_controller/test_borrow_cap.py b/tests/unitary/lending/ll_controller/test_borrow_cap.py index 4cbb8406..5bfb29d3 100644 --- a/tests/unitary/lending/ll_controller/test_borrow_cap.py +++ b/tests/unitary/lending/ll_controller/test_borrow_cap.py @@ -1 +1,36 @@ -# TODO should be zero at deployment +import pytest + + +@pytest.fixture(scope="module") +def lending_controller( + proto, + collateral_token, + amm_A, + amm_fee, + loan_discount, + liquidation_discount, + price_oracle, + min_borrow_rate, + max_borrow_rate, +): + market = 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, + ) + return market["controller"] + + +def test_borrow_cap_default_behavior(lending_controller): + """ + Checks that freshly deployed lending controllers have a borrow cap of zero. + """ + assert lending_controller.borrow_cap() == 0 From 6b4d2b504cb2bd2282cbf24e0bf1b06a4cf4f456 Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 19 Sep 2025 13:29:44 +0200 Subject: [PATCH 293/413] test: add testing helper --- tests/utils/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py index ae1ca403..63ec703f 100644 --- a/tests/utils/__init__.py +++ b/tests/utils/__init__.py @@ -1,5 +1,7 @@ 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. @@ -11,3 +13,7 @@ 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) From f176d9ab0732a8731a6280486ed31b39bd13f0b0 Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 19 Sep 2025 13:29:57 +0200 Subject: [PATCH 294/413] ci: run e2e tests --- .github/workflows/test.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 214a0788..40530bc9 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -36,6 +36,9 @@ jobs: matrix: folder: - "tests/unitary" + - "tests/e2e" + # - "tests/stateful" not ready yet + # Old tests to be migrated - "tests/flashloan" - "tests/lm_callback" - "tests/controller" From 0240f5cc2415e9e39ec54e9f25917709dbe36718 Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 19 Sep 2025 13:30:17 +0200 Subject: [PATCH 295/413] test: add resupply hack simulation --- tests/e2e/test_resupply_hack.py | 45 +++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 tests/e2e/test_resupply_hack.py 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 From e220cb57811dcd3f8b4cde0352931fac0e22a4f4 Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 19 Sep 2025 14:05:42 +0200 Subject: [PATCH 296/413] chore: less warnings --- contracts/Stableswap.vy | 92 +++++++++++++++++++++++++++++------------ 1 file changed, 65 insertions(+), 27 deletions(-) diff --git a/contracts/Stableswap.vy b/contracts/Stableswap.vy index e1340846..ca671f3b 100644 --- a/contracts/Stableswap.vy +++ b/contracts/Stableswap.vy @@ -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,12 +287,12 @@ 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 staticcall ERC1271(_owner).isValidSignature(digest, sig) == ERC1271_MAGIC_VAL else: @@ -301,7 +301,7 @@ def permit( 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 @@ -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,7 +493,7 @@ 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)) @@ -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 @@ -824,7 +835,13 @@ def exchange( 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 @@ -858,9 +875,14 @@ def remove_liquidity( 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 @@ -919,8 +941,14 @@ def remove_liquidity_imbalance( 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 @@ -1047,10 +1075,15 @@ def remove_liquidity_one_coin( 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) + 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(msg.sender, _burn_amount, dy[0], total_supply) + log RemoveLiquidityOne( + provider=msg.sender, + token_amount=_burn_amount, + coin_amount=dy[0], + token_supply=total_supply, + ) self.save_p_from_price(dy[2]) @@ -1077,7 +1110,12 @@ 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 @@ -1091,7 +1129,7 @@ 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 @@ -1116,7 +1154,7 @@ def commit_new_fee(_new_fee: uint256): 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 @@ -1128,7 +1166,7 @@ def apply_new_fee(): fee: uint256 = self.future_fee self.fee = fee self.admin_action_deadline = 0 - log ApplyNewFee(fee) + log ApplyNewFee(fee=fee) @external From ed44b82d3d98b899937785d7d8f21dccf5f5f18d Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 19 Sep 2025 15:45:23 +0200 Subject: [PATCH 297/413] feat: improve LendController structure --- contracts/interfaces/ILlamalendController.vyi | 4 + contracts/lending/LLController.vy | 75 ++++++++++++------- 2 files changed, 50 insertions(+), 29 deletions(-) diff --git a/contracts/interfaces/ILlamalendController.vyi b/contracts/interfaces/ILlamalendController.vyi index 651f0b9b..fe9697a1 100644 --- a/contracts/interfaces/ILlamalendController.vyi +++ b/contracts/interfaces/ILlamalendController.vyi @@ -73,6 +73,10 @@ event CollectFees: new_supply: uint256 +event SetAdminFee: + admin_fee: uint256 + + # Functions @view diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy index e7052501..5ed8a4e8 100644 --- a/contracts/lending/LLController.vy +++ b/contracts/lending/LLController.vy @@ -2,10 +2,10 @@ # pragma nonreentrancy on # pragma optimize codesize """ -@title LlamaLend Controller +@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 +@notice Main contract to interact with a Llamalend Lend Market. Each contract is specific to a single mint market. @custom:security security@curve.fi """ @@ -81,14 +81,23 @@ exports: ( VAULT: immutable(IVault) +# https://github.com/vyperlang/vyper/issues/4721 +@external +@view +def vault() -> IVault: + """ + @notice Address of the vault + """ + return VAULT + + # cumulative amount of assets ever lent lent: public(uint256) # cumulative amount of assets collected by admin collected: public(uint256) - -# Unlike mint markets admin fee here is can be less than 100% +# TODO Rename to admin percentage? admin_fee: public(uint256) @@ -104,7 +113,7 @@ def __init__( view_impl: address, ): """ - @notice Controller constructor deployed by the factory from blueprint + @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 @@ -124,13 +133,20 @@ def __init__( view_impl, ) + # Borrow cap is zero by default in lend markets. The admin has to raise it + # after deployment to allow borrowing. core.borrow_cap = 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") @@ -138,24 +154,24 @@ def version() -> String[10]: @view def borrow_cap() -> uint256: """ - @notice Current borrow cap + @notice Maximum amount of borrowed tokens that can be lent out at any time. """ return core.borrow_cap @external @view -def vault() -> IVault: +def borrowed_balance() -> uint256: """ - @notice Address of the vault + @notice Amount of borrowed token the controller currently holds. + @dev Used by the vault for its accounting logic. """ - return VAULT - - -@internal -@view -def _borrowed_balance() -> uint256: - # (VAULT.deposited() - VAULT.withdrawn()) - (self.lent - self.repaid) - self.collected + # TODO rename to `borrowed_token_balance` for clarity? + # Start from the vault’s net funding of borrowed tokens (deposited − withdrawn), + # subtract the portion we actually lent out that hasn’t been repaid yet (lent − repaid), + # and subtract what the admin already skimmed as fees; what’s left is the controller’s idle cash. + # (VAULT.deposited() - VAULT.withdrawn()) - (lent - repaid) - self.collected + # The terms are rearranged to avoid underflows in intermediate steps. return ( staticcall VAULT.deposited() + core.repaid @@ -165,12 +181,6 @@ def _borrowed_balance() -> uint256: ) -@external -@view -def borrowed_balance() -> uint256: - return self._borrowed_balance() - - @external def create_loan( collateral: uint256, @@ -181,7 +191,7 @@ def create_loan( calldata: Bytes[10**4] = b"", ): """ - @notice Create loan but pass borrowed to a callback first so that it can build leverage + @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 Borrowed asset debt to take @param N Number of bands to deposit into (to do autoliquidation-deliquidation), @@ -204,6 +214,14 @@ def borrow_more( callbacker: address = empty(address), calldata: Bytes[10**4] = 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 borrowed asset debt to take + @param _for Address to borrow for + @param callbacker Address of the callback contract + @param calldata Any data for callbacker + """ _debt: uint256 = core._borrow_more( collateral, debt, @@ -218,6 +236,9 @@ def borrow_more( @external @view def admin_fees() -> uint256: + """ + @notice Return the amount of borrowed tokens that have been collected as fees. + """ return core._admin_fees(self.admin_fee) @@ -228,12 +249,6 @@ def collect_fees() -> uint256: return fees -@internal -def _set_borrow_cap(_borrow_cap: uint256): - core.borrow_cap = _borrow_cap - log ILlamalendController.SetBorrowCap(borrow_cap=_borrow_cap) - - @external def set_borrow_cap(_borrow_cap: uint256): """ @@ -242,7 +257,8 @@ def set_borrow_cap(_borrow_cap: uint256): @param _borrow_cap New borrow cap in units of borrowed_token """ core._check_admin() - self._set_borrow_cap(_borrow_cap) + core.borrow_cap = _borrow_cap + log ILlamalendController.SetBorrowCap(borrow_cap=_borrow_cap) @external @@ -253,3 +269,4 @@ def set_admin_fee(_admin_fee: uint256): core._check_admin() assert _admin_fee <= core.WAD # dev: admin fee higher than 100% self.admin_fee = _admin_fee + log ILlamalendController.SetAdminFee(admin_fee=_admin_fee) From a3532efb0aec27600682e1c29c6620aa99817bd8 Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 19 Sep 2025 15:49:44 +0200 Subject: [PATCH 298/413] test: version getter for lend controller --- tests/unitary/lending/ll_controller/test_version.py | 5 +++++ tests/utils/constants.py | 1 + 2 files changed, 6 insertions(+) create mode 100644 tests/unitary/lending/ll_controller/test_version.py diff --git a/tests/unitary/lending/ll_controller/test_version.py b/tests/unitary/lending/ll_controller/test_version.py new file mode 100644 index 00000000..c0a09060 --- /dev/null +++ b/tests/unitary/lending/ll_controller/test_version.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/utils/constants.py b/tests/utils/constants.py index f07bd0d8..eb90af06 100644 --- a/tests/utils/constants.py +++ b/tests/utils/constants.py @@ -12,6 +12,7 @@ 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 From f7e9f187a7766165ecbfe68c92656163806d4c68 Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 19 Sep 2025 15:50:06 +0200 Subject: [PATCH 299/413] test: vault getter for lend controller --- tests/unitary/lending/ll_controller/test_vault.py | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 tests/unitary/lending/ll_controller/test_vault.py diff --git a/tests/unitary/lending/ll_controller/test_vault.py b/tests/unitary/lending/ll_controller/test_vault.py new file mode 100644 index 00000000..4bc37b5b --- /dev/null +++ b/tests/unitary/lending/ll_controller/test_vault.py @@ -0,0 +1,2 @@ +def test_default_behavior(controller, vault): + assert controller.vault() == vault.address From e1e480d7a171f76c2062b07b045c211bc5edcab5 Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 19 Sep 2025 15:50:31 +0200 Subject: [PATCH 300/413] test: strict is implicit --- tests/unitary/lending/ll_controller/test_set_borrow_cap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unitary/lending/ll_controller/test_set_borrow_cap.py b/tests/unitary/lending/ll_controller/test_set_borrow_cap.py index cc26415c..b0367e66 100644 --- a/tests/unitary/lending/ll_controller/test_set_borrow_cap.py +++ b/tests/unitary/lending/ll_controller/test_set_borrow_cap.py @@ -6,7 +6,7 @@ @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", _strict=True) + logs = filter_logs(controller, "SetBorrowCap") assert len(logs) == 1 and logs[-1].borrow_cap == new_cap assert controller.borrow_cap() == new_cap From 13e504b400af37638ee221d391e26e1db9b944ff Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 19 Sep 2025 15:51:22 +0200 Subject: [PATCH 301/413] test: set_admin_fee in lend controller --- .../ll_controller/test_set_admin_fee.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tests/unitary/lending/ll_controller/test_set_admin_fee.py diff --git a/tests/unitary/lending/ll_controller/test_set_admin_fee.py b/tests/unitary/lending/ll_controller/test_set_admin_fee.py new file mode 100644 index 00000000..d6917f80 --- /dev/null +++ b/tests/unitary/lending/ll_controller/test_set_admin_fee.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_fee(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_fee(WAD + 1, sender=admin) + + +def test_unauthorized(controller): + with boa.reverts("only admin"): + controller.set_admin_fee(1) From c4ccfb10754377c0b6460bd84e319ab63021b8e6 Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 19 Sep 2025 15:52:01 +0200 Subject: [PATCH 302/413] docs: minor consistency corrections --- contracts/Controller.vy | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index ff652b1f..0c03b0ae 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -2,10 +2,10 @@ # pragma nonreentrancy on # pragma optimize codesize """ -@title crvUSD Controller +@title Llamalend Mint Market Controller @author Curve.Fi @license Copyright (c) Curve.Fi, 2020-2025 - all rights reserved -@notice Main contract to interact with a Llamalend mint market. Each +@notice Main contract to interact with a Llamalend Mint Market. Each contract is specific to a single mint market. @custom:security security@curve.fi """ @@ -138,9 +138,11 @@ def __init__( _AMM: IAMM, view_impl: address, ): + # TODO add sanity check for zero addresses # In MintController the correct way to limit borrowing - # is through the debt ceiling. + # is through the debt ceiling. This is here to be used + # in LendController only. self.borrow_cap = max_value(uint256) FACTORY = IFactory(msg.sender) From 97e38349d62019c0ccda9c5c4dd39166d3d40bde Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 19 Sep 2025 15:55:40 +0200 Subject: [PATCH 303/413] test: lend ctor --- tests/unitary/lending/ll_controller/test_ctor.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 tests/unitary/lending/ll_controller/test_ctor.py diff --git a/tests/unitary/lending/ll_controller/test_ctor.py b/tests/unitary/lending/ll_controller/test_ctor.py new file mode 100644 index 00000000..b967928b --- /dev/null +++ b/tests/unitary/lending/ll_controller/test_ctor.py @@ -0,0 +1,9 @@ +from tests.utils.constants import MAX_UINT256 + + +def test_default_behavior(controller, vault, borrowed_token): + # No need to check vault address and borrow cap in here + # as their getter tests already do that. + + approved = borrowed_token.allowance(controller, vault) + assert approved == MAX_UINT256 From 180316c76ae5da51043618d3f1cae040b318ccab Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 19 Sep 2025 16:23:04 +0200 Subject: [PATCH 304/413] test: create loan lend behavior --- .../lending/ll_controller/test_create_loan.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/unitary/lending/ll_controller/test_create_loan.py diff --git a/tests/unitary/lending/ll_controller/test_create_loan.py b/tests/unitary/lending/ll_controller/test_create_loan.py new file mode 100644 index 00000000..27f0a58b --- /dev/null +++ b/tests/unitary/lending/ll_controller/test_create_loan.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 From de87338423a967be60d4e21e8fb460150359377d Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 19 Sep 2025 16:27:04 +0200 Subject: [PATCH 305/413] test: borrow more lend behavior --- .../lending/ll_controller/test_borrow_more.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tests/unitary/lending/ll_controller/test_borrow_more.py diff --git a/tests/unitary/lending/ll_controller/test_borrow_more.py b/tests/unitary/lending/ll_controller/test_borrow_more.py new file mode 100644 index 00000000..2933d49a --- /dev/null +++ b/tests/unitary/lending/ll_controller/test_borrow_more.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 From 51b8b995e608d87b37d80f44ee3c08b9f392bc12 Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 19 Sep 2025 16:35:11 +0200 Subject: [PATCH 306/413] test: collect fees lending --- .../ll_controller/test_collect_fees.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/unitary/lending/ll_controller/test_collect_fees.py diff --git a/tests/unitary/lending/ll_controller/test_collect_fees.py b/tests/unitary/lending/ll_controller/test_collect_fees.py new file mode 100644 index 00000000..64874160 --- /dev/null +++ b/tests/unitary/lending/ll_controller/test_collect_fees.py @@ -0,0 +1,57 @@ +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 +FEE_PCTS = [100, 75, 50, 25, 10] + + +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, +): + # We iterate over a handful of admin-fee percentages and re-run the same setup for + # each entry, checking two things: (1) `collect_fees()` returns exactly what ends + # up in `controller.collected()` and (2) the amount is a linear proportion of the + # 100% baseline. + def collect_for_pct(pct: int) -> int: + with boa.env.anchor(): + controller.set_admin_fee(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) + + boa.deal(borrowed_token, controller.address, 10**24) + amount = controller.collect_fees() + assert controller.collected() == amount + return amount + + base_amount = collect_for_pct(100) + assert base_amount > 0 + + results = {100: base_amount} + for pct in FEE_PCTS[1:]: + results[pct] = collect_for_pct(pct) + expected = base_amount * pct // 100 + assert results[pct] == expected + + for higher, lower in zip(FEE_PCTS, FEE_PCTS[1:]): + assert results[higher] > results[lower] From d35b559c7be745c214848a7f88c8f4c7bec3ed04 Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 19 Sep 2025 16:57:54 +0200 Subject: [PATCH 307/413] docs: add natspec --- contracts/lending/LLController.vy | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/lending/LLController.vy b/contracts/lending/LLController.vy index 5ed8a4e8..0b357cc7 100644 --- a/contracts/lending/LLController.vy +++ b/contracts/lending/LLController.vy @@ -244,6 +244,9 @@ def admin_fees() -> uint256: @external def collect_fees() -> uint256: + """ + @notice Collect the fees charged as interest that belong to the admin. + """ fees: uint256 = core._collect_fees(self.admin_fee) self.collected += fees return fees From 2ad3bc1961e2d65dfdc4f0bba4326a49d4aab5c4 Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 19 Sep 2025 16:58:02 +0200 Subject: [PATCH 308/413] test: admin fees preview --- .../lending/ll_controller/test_admin_fees.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 tests/unitary/lending/ll_controller/test_admin_fees.py diff --git a/tests/unitary/lending/ll_controller/test_admin_fees.py b/tests/unitary/lending/ll_controller/test_admin_fees.py new file mode 100644 index 00000000..a93aa6f7 --- /dev/null +++ b/tests/unitary/lending/ll_controller/test_admin_fees.py @@ -0,0 +1,46 @@ +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 +FEE_PCTS = [100, 75, 50, 25, 10] + + +def test_default_behavior_no_fees(controller): + assert controller.admin_fees() == 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_fee(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() + + base_amount = outstanding_for_pct(100) + assert base_amount > 0 + + results = {100: base_amount} + for pct in FEE_PCTS[1:]: + results[pct] = outstanding_for_pct(pct) + expected = base_amount * pct // 100 + assert results[pct] == expected + + for higher, lower in zip(FEE_PCTS, FEE_PCTS[1:]): + assert results[higher] > results[lower] From 04d2107bb506df432a26520716d4bdb3d3b92074 Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 19 Sep 2025 18:12:01 +0200 Subject: [PATCH 309/413] Create test_borrowed_balance.py --- .../ll_controller/test_borrowed_balance.py | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 tests/unitary/lending/ll_controller/test_borrowed_balance.py diff --git a/tests/unitary/lending/ll_controller/test_borrowed_balance.py b/tests/unitary/lending/ll_controller/test_borrowed_balance.py new file mode 100644 index 00000000..78adee0c --- /dev/null +++ b/tests/unitary/lending/ll_controller/test_borrowed_balance.py @@ -0,0 +1,153 @@ +import boa + +from tests.utils import max_approve +from tests.utils.constants import MIN_TICKS, WAD + +COLLATERAL = 10**21 +DEBT = 10**18 + + +def snapshot(controller, vault): + return { + "borrowed": controller.borrowed_balance(), + "lent": controller.lent(), + "repaid": controller.repaid(), + "collected": controller.collected(), + "deposited": vault.deposited(), + "withdrawn": vault.withdrawn(), + } + + +def expect_same(before, after, *fields): + for field in fields: + assert after[field] == before[field] + + +def test_default_behavior(controller, vault, seed_liquidity): + state = snapshot(controller, vault) + assert state["lent"] == 0 + assert state["collected"] == 0 + assert state["repaid"] == 0 + assert state["withdrawn"] == 0 + assert state["borrowed"] == state["deposited"] == seed_liquidity + + +def test_decreases_after_borrow(controller, vault, collateral_token): + before = snapshot(controller, vault) + + boa.deal(collateral_token, boa.env.eoa, COLLATERAL) + max_approve(collateral_token, controller.address, sender=boa.env.eoa) + + controller.create_loan(COLLATERAL, DEBT, MIN_TICKS) + + after = snapshot(controller, vault) + assert after["borrowed"] == before["borrowed"] - DEBT + assert after["lent"] == before["lent"] + DEBT + expect_same(before, after, "repaid", "collected", "deposited", "withdrawn") + + +def test_restores_after_repay(controller, vault, collateral_token, borrowed_token): + before = snapshot(controller, vault) + + boa.deal(collateral_token, boa.env.eoa, COLLATERAL) + max_approve(collateral_token, controller.address, sender=boa.env.eoa) + + controller.create_loan(COLLATERAL, DEBT, MIN_TICKS) + + max_approve(borrowed_token, controller.address, sender=boa.env.eoa) + controller.repay(DEBT) + + after = snapshot(controller, vault) + expect_same(before, after, "borrowed", "collected", "deposited", "withdrawn") + assert after["lent"] == before["lent"] + DEBT + assert after["repaid"] == before["repaid"] + DEBT + + +def test_decreases_after_withdraw( + controller, + vault, + collateral_token, + borrowed_token, +): + EXTRA_DEPOSIT = 5 * 10**18 + WITHDRAW_AMOUNT = EXTRA_DEPOSIT // 2 + + start = snapshot(controller, vault) + + boa.deal(borrowed_token, boa.env.eoa, EXTRA_DEPOSIT) + max_approve(borrowed_token, vault.address, sender=boa.env.eoa) + assert borrowed_token.balanceOf(boa.env.eoa) == EXTRA_DEPOSIT + vault.deposit(EXTRA_DEPOSIT) + max_approve(borrowed_token, controller.address, sender=boa.env.eoa) + + after_deposit = snapshot(controller, vault) + assert after_deposit["borrowed"] == start["borrowed"] + EXTRA_DEPOSIT + assert after_deposit["deposited"] == start["deposited"] + EXTRA_DEPOSIT + expect_same(start, after_deposit, "lent", "repaid", "collected", "withdrawn") + + boa.deal(collateral_token, boa.env.eoa, COLLATERAL) + max_approve(collateral_token, controller.address, sender=boa.env.eoa) + controller.create_loan(COLLATERAL, DEBT, MIN_TICKS) + + after_loan = snapshot(controller, vault) + assert after_loan["borrowed"] == after_deposit["borrowed"] - DEBT + assert after_loan["lent"] == after_deposit["lent"] + DEBT + expect_same( + after_deposit, after_loan, "repaid", "collected", "deposited", "withdrawn" + ) + + controller.repay(DEBT) + after_repay = snapshot(controller, vault) + expect_same( + after_deposit, after_repay, "borrowed", "collected", "deposited", "withdrawn" + ) + expect_same(after_loan, after_repay, "lent") + assert after_repay["repaid"] == after_deposit["repaid"] + DEBT + + withdrawn_before = vault.withdrawn() + vault.withdraw(WITHDRAW_AMOUNT) + + after_withdraw = snapshot(controller, vault) + assert after_withdraw["borrowed"] == after_repay["borrowed"] - WITHDRAW_AMOUNT + assert after_withdraw["withdrawn"] == withdrawn_before + WITHDRAW_AMOUNT + expect_same(after_repay, after_withdraw, "lent", "repaid", "collected", "deposited") + + +def test_collect_fees_reduces_balance( + admin, + controller, + amm, + collateral_token, + borrowed_token, + vault, +): + ADMIN_FEE = WAD // 2 + RATE = 10**11 + TIME_DELTA = 86400 + + before_fee = snapshot(controller, vault) + controller.set_admin_fee(ADMIN_FEE, sender=admin) + after_fee = snapshot(controller, vault) + assert controller.admin_fee() == ADMIN_FEE + assert after_fee == before_fee + + boa.deal(collateral_token, boa.env.eoa, COLLATERAL) + max_approve(collateral_token, controller.address, sender=boa.env.eoa) + controller.create_loan(COLLATERAL, DEBT, MIN_TICKS) + + before_collect = snapshot(controller, vault) + amm.eval(f"self.rate = {RATE}") + amm.eval("self.rate_time = block.timestamp") + boa.env.time_travel(TIME_DELTA) + + boa.deal(borrowed_token, controller.address, 10**24) + + amount = controller.collect_fees() + + assert amount > 0 + after_collect = snapshot(controller, vault) + assert after_collect["collected"] == before_collect["collected"] + amount + assert after_collect["borrowed"] == before_collect["borrowed"] - amount + expect_same( + before_collect, after_collect, "lent", "repaid", "deposited", "withdrawn" + ) From 16dacf6e7cfdfa5c3742a3c0248a051454b00138 Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 19 Sep 2025 18:28:38 +0200 Subject: [PATCH 310/413] chore: renaming LL to Lend --- .../{LLController.vy => LendController.vy} | 0 ...ControllerView.vy => LendControllerView.vy} | 0 tests/conftest.py | 2 +- tests/lm_callback/conftest.py | 4 ++-- tests/stableborrow/stabilize/conftest.py | 4 ++-- tests/stableborrow/test_bigfuzz.py | 4 ++-- tests/stableborrow/test_factory.py | 4 ++-- .../test_admin_fees_lc.py} | 0 .../test_borrow_cap_lc.py} | 0 .../test_borrow_more_lc.py} | 0 .../test_borrowed_balance_lc.py} | 0 .../test_collect_fees_lc.py} | 0 .../test_create_loan_lc.py} | 0 .../test_ctor_lc.py} | 0 .../test_set_admin_fee_lc.py} | 0 .../test_set_borrow_cap_lc.py} | 0 .../test_vault_lc.py} | 0 .../test_version_lc.py} | 0 tests/utils/deployers.py | 8 ++++---- tests/utils/protocols.py | 18 +++++++++--------- 20 files changed, 22 insertions(+), 22 deletions(-) rename contracts/lending/{LLController.vy => LendController.vy} (100%) rename contracts/lending/{LLControllerView.vy => LendControllerView.vy} (100%) rename tests/unitary/lending/{ll_controller/test_admin_fees.py => lend_controller/test_admin_fees_lc.py} (100%) rename tests/unitary/lending/{ll_controller/test_borrow_cap.py => lend_controller/test_borrow_cap_lc.py} (100%) rename tests/unitary/lending/{ll_controller/test_borrow_more.py => lend_controller/test_borrow_more_lc.py} (100%) rename tests/unitary/lending/{ll_controller/test_borrowed_balance.py => lend_controller/test_borrowed_balance_lc.py} (100%) rename tests/unitary/lending/{ll_controller/test_collect_fees.py => lend_controller/test_collect_fees_lc.py} (100%) rename tests/unitary/lending/{ll_controller/test_create_loan.py => lend_controller/test_create_loan_lc.py} (100%) rename tests/unitary/lending/{ll_controller/test_ctor.py => lend_controller/test_ctor_lc.py} (100%) rename tests/unitary/lending/{ll_controller/test_set_admin_fee.py => lend_controller/test_set_admin_fee_lc.py} (100%) rename tests/unitary/lending/{ll_controller/test_set_borrow_cap.py => lend_controller/test_set_borrow_cap_lc.py} (100%) rename tests/unitary/lending/{ll_controller/test_vault.py => lend_controller/test_vault_lc.py} (100%) rename tests/unitary/lending/{ll_controller/test_version.py => lend_controller/test_version_lc.py} (100%) diff --git a/contracts/lending/LLController.vy b/contracts/lending/LendController.vy similarity index 100% rename from contracts/lending/LLController.vy rename to contracts/lending/LendController.vy diff --git a/contracts/lending/LLControllerView.vy b/contracts/lending/LendControllerView.vy similarity index 100% rename from contracts/lending/LLControllerView.vy rename to contracts/lending/LendControllerView.vy diff --git a/tests/conftest.py b/tests/conftest.py index a353111a..3aa68d34 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -100,7 +100,7 @@ def amm_impl(proto): @pytest.fixture(scope="module") def controller_impl(proto): - return proto.blueprints.ll_controller + return proto.blueprints.lend_controller @pytest.fixture(scope="module") diff --git a/tests/lm_callback/conftest.py b/tests/lm_callback/conftest.py index 203b4ef9..fdca8501 100644 --- a/tests/lm_callback/conftest.py +++ b/tests/lm_callback/conftest.py @@ -12,7 +12,7 @@ CONSTANT_MONETARY_POLICY_DEPLOYER, LM_CALLBACK_DEPLOYER, BLOCK_COUNTER_DEPLOYER, - LL_CONTROLLER_DEPLOYER, + LEND_CONTROLLER_DEPLOYER, ) @@ -159,7 +159,7 @@ def market_amm(market, collateral_token, stablecoin, accounts): def market_controller( market, stablecoin, collateral_token, controller_factory, accounts ): - return LL_CONTROLLER_DEPLOYER.at(market.get_controller(collateral_token.address)) + return LEND_CONTROLLER_DEPLOYER.at(market.get_controller(collateral_token.address)) @pytest.fixture(scope="module") diff --git a/tests/stableborrow/stabilize/conftest.py b/tests/stableborrow/stabilize/conftest.py index 36946390..fbe84299 100644 --- a/tests/stableborrow/stabilize/conftest.py +++ b/tests/stableborrow/stabilize/conftest.py @@ -19,7 +19,7 @@ AGG_MONETARY_POLICY2_DEPLOYER, CHAINLINK_AGGREGATOR_MOCK_DEPLOYER, AMM_DEPLOYER, - LL_CONTROLLER_DEPLOYER, + LEND_CONTROLLER_DEPLOYER, ) from tests.utils.constants import ZERO_ADDRESS @@ -335,7 +335,7 @@ def market_controller_agg( controller_factory, accounts, ): - controller = LL_CONTROLLER_DEPLOYER.at( + controller = LEND_CONTROLLER_DEPLOYER.at( market_agg.get_controller(collateral_token.address) ) for acc in accounts: diff --git a/tests/stableborrow/test_bigfuzz.py b/tests/stableborrow/test_bigfuzz.py index 2b152a38..4eae21f8 100644 --- a/tests/stableborrow/test_bigfuzz.py +++ b/tests/stableborrow/test_bigfuzz.py @@ -11,7 +11,7 @@ invariant, ) -from tests.utils.deployers import AMM_DEPLOYER, LL_CONTROLLER_DEPLOYER +from tests.utils.deployers import AMM_DEPLOYER, LEND_CONTROLLER_DEPLOYER # Variables and methods to check # * A @@ -537,7 +537,7 @@ def test_big_fuzz( collateral_token = ERC20_MOCK_DEPLOYER.deploy(collateral_digits) market = get_market(collateral_token) market_amm = AMM_DEPLOYER.at(market.get_amm(collateral_token.address)) - market_controller = LL_CONTROLLER_DEPLOYER.at( + market_controller = LEND_CONTROLLER_DEPLOYER.at( market.get_controller(collateral_token.address) ) fake_leverage = get_fake_leverage(collateral_token, market_controller) diff --git a/tests/stableborrow/test_factory.py b/tests/stableborrow/test_factory.py index 638a0089..d3a51cd6 100644 --- a/tests/stableborrow/test_factory.py +++ b/tests/stableborrow/test_factory.py @@ -1,5 +1,5 @@ import boa -from tests.utils.deployers import AMM_DEPLOYER, LL_CONTROLLER_DEPLOYER +from tests.utils.deployers import AMM_DEPLOYER, LEND_CONTROLLER_DEPLOYER def test_stablecoin_admin(controller_factory, stablecoin, accounts): @@ -48,7 +48,7 @@ def test_add_market( == collateral_token.address.lower() ) - controller = LL_CONTROLLER_DEPLOYER.at( + controller = LEND_CONTROLLER_DEPLOYER.at( controller_factory.get_controller(collateral_token.address) ) amm = AMM_DEPLOYER.at(controller_factory.get_amm(collateral_token.address)) diff --git a/tests/unitary/lending/ll_controller/test_admin_fees.py b/tests/unitary/lending/lend_controller/test_admin_fees_lc.py similarity index 100% rename from tests/unitary/lending/ll_controller/test_admin_fees.py rename to tests/unitary/lending/lend_controller/test_admin_fees_lc.py diff --git a/tests/unitary/lending/ll_controller/test_borrow_cap.py b/tests/unitary/lending/lend_controller/test_borrow_cap_lc.py similarity index 100% rename from tests/unitary/lending/ll_controller/test_borrow_cap.py rename to tests/unitary/lending/lend_controller/test_borrow_cap_lc.py diff --git a/tests/unitary/lending/ll_controller/test_borrow_more.py b/tests/unitary/lending/lend_controller/test_borrow_more_lc.py similarity index 100% rename from tests/unitary/lending/ll_controller/test_borrow_more.py rename to tests/unitary/lending/lend_controller/test_borrow_more_lc.py diff --git a/tests/unitary/lending/ll_controller/test_borrowed_balance.py b/tests/unitary/lending/lend_controller/test_borrowed_balance_lc.py similarity index 100% rename from tests/unitary/lending/ll_controller/test_borrowed_balance.py rename to tests/unitary/lending/lend_controller/test_borrowed_balance_lc.py diff --git a/tests/unitary/lending/ll_controller/test_collect_fees.py b/tests/unitary/lending/lend_controller/test_collect_fees_lc.py similarity index 100% rename from tests/unitary/lending/ll_controller/test_collect_fees.py rename to tests/unitary/lending/lend_controller/test_collect_fees_lc.py diff --git a/tests/unitary/lending/ll_controller/test_create_loan.py b/tests/unitary/lending/lend_controller/test_create_loan_lc.py similarity index 100% rename from tests/unitary/lending/ll_controller/test_create_loan.py rename to tests/unitary/lending/lend_controller/test_create_loan_lc.py diff --git a/tests/unitary/lending/ll_controller/test_ctor.py b/tests/unitary/lending/lend_controller/test_ctor_lc.py similarity index 100% rename from tests/unitary/lending/ll_controller/test_ctor.py rename to tests/unitary/lending/lend_controller/test_ctor_lc.py diff --git a/tests/unitary/lending/ll_controller/test_set_admin_fee.py b/tests/unitary/lending/lend_controller/test_set_admin_fee_lc.py similarity index 100% rename from tests/unitary/lending/ll_controller/test_set_admin_fee.py rename to tests/unitary/lending/lend_controller/test_set_admin_fee_lc.py diff --git a/tests/unitary/lending/ll_controller/test_set_borrow_cap.py b/tests/unitary/lending/lend_controller/test_set_borrow_cap_lc.py similarity index 100% rename from tests/unitary/lending/ll_controller/test_set_borrow_cap.py rename to tests/unitary/lending/lend_controller/test_set_borrow_cap_lc.py diff --git a/tests/unitary/lending/ll_controller/test_vault.py b/tests/unitary/lending/lend_controller/test_vault_lc.py similarity index 100% rename from tests/unitary/lending/ll_controller/test_vault.py rename to tests/unitary/lending/lend_controller/test_vault_lc.py diff --git a/tests/unitary/lending/ll_controller/test_version.py b/tests/unitary/lending/lend_controller/test_version_lc.py similarity index 100% rename from tests/unitary/lending/ll_controller/test_version.py rename to tests/unitary/lending/lend_controller/test_version_lc.py diff --git a/tests/utils/deployers.py b/tests/utils/deployers.py index f3e0e862..0a9d9049 100644 --- a/tests/utils/deployers.py +++ b/tests/utils/deployers.py @@ -62,11 +62,11 @@ VAULT_DEPLOYER = boa.load_partial( LENDING_CONTRACT_PATH + "Vault.vy", compiler_args=compiler_args_codesize ) -LL_CONTROLLER_DEPLOYER = boa.load_partial( - LENDING_CONTRACT_PATH + "LLController.vy", compiler_args=compiler_args_codesize +LEND_CONTROLLER_DEPLOYER = boa.load_partial( + LENDING_CONTRACT_PATH + "LendController.vy", compiler_args=compiler_args_codesize ) -LL_CONTROLLER_VIEW_DEPLOYER = boa.load_partial( - LENDING_CONTRACT_PATH + "LLControllerView.vy", compiler_args=compiler_args_default +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 diff --git a/tests/utils/protocols.py b/tests/utils/protocols.py index 8cee116e..3d3aae9a 100644 --- a/tests/utils/protocols.py +++ b/tests/utils/protocols.py @@ -22,8 +22,8 @@ CONTROLLER_VIEW_DEPLOYER, # Lending contracts VAULT_DEPLOYER, - LL_CONTROLLER_DEPLOYER, - LL_CONTROLLER_VIEW_DEPLOYER, + LEND_CONTROLLER_DEPLOYER, + LEND_CONTROLLER_VIEW_DEPLOYER, LENDING_FACTORY_DEPLOYER, # Price oracles DUMMY_PRICE_ORACLE_DEPLOYER, @@ -46,8 +46,8 @@ def __init__(self, **deployers: VyperDeployer): amm: VyperBlueprint mint_controller: VyperBlueprint - ll_controller: VyperBlueprint - ll_controller_view: VyperBlueprint + lend_controller: VyperBlueprint + lend_controller_view: VyperBlueprint price_oracle: VyperBlueprint mpolicy: VyperBlueprint vault: VyperBlueprint @@ -94,8 +94,8 @@ def __init__(self, initial_price: int = 3000 * 10**18): self.blueprints = Blueprints( amm=AMM_DEPLOYER, mint_controller=self._mint_controller_deployer, - ll_controller=LL_CONTROLLER_DEPLOYER, - ll_controller_view=LL_CONTROLLER_VIEW_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, @@ -139,10 +139,10 @@ def __init_lend_markets(self): # Deploy lending factory self.lending_factory = LENDING_FACTORY_DEPLOYER.deploy( self.blueprints.amm.address, - self.blueprints.ll_controller.address, + self.blueprints.lend_controller.address, self.blueprints.vault.address, self.blueprints.price_oracle.address, - self.blueprints.ll_controller_view.address, + self.blueprints.lend_controller_view.address, self.admin, self.fee_receiver, ) @@ -259,7 +259,7 @@ def create_lending_market( ) vault = VAULT_DEPLOYER.at(result[0]) - controller = LL_CONTROLLER_DEPLOYER.at(result[1]) + controller = LEND_CONTROLLER_DEPLOYER.at(result[1]) amm = AMM_DEPLOYER.at(result[2]) # Seed lending markets by depositing borrowed token into the vault From 7d4618a37508a3f452ab61ebdf35170c35f369d1 Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 19 Sep 2025 18:52:38 +0200 Subject: [PATCH 311/413] chore: housekeeping --- .flake8 | 3 - .gitignore | 1 + ape-config.yaml | 33 --- ape-run.sh | 27 -- brownie-config.yaml | 5 - docs-requirements.txt | 1 - docs/Makefile | 20 -- docs/source/conf.py | 33 --- docs/source/index.rst | 20 -- docs/source/stablecoin.rst | 245 ------------------ pypytest | 4 - pytest.ini | 2 - {tests_forked => tests/forked}/__init__.py | 0 .../forked}/price_oracles/__init__.py | 0 .../forked}/price_oracles/conftest.py | 0 .../forked}/price_oracles/settings.py | 0 .../test_lp_oracle_compare_to_spot.py | 0 .../leverage}/__init__.py | 0 .../leverage}/test_v1/__init__.py | 0 .../leverage}/test_v1/conftest.py | 0 .../leverage}/test_v1/test_deleverage.py | 0 .../test_v1/test_deleverage_light.py | 0 .../leverage}/test_v1/test_leverage.py | 0 .../leverage}/test_v1/test_leverage_light.py | 0 .../leverage}/test_v1/utils.py | 0 .../leverage}/test_v2/__init__.py | 0 .../leverage}/test_v2/conftest.py | 0 .../leverage}/test_v2/constants.py | 0 .../leverage}/test_v2/settings.py | 0 .../leverage}/test_v2/tests/__init__.py | 0 .../leverage}/test_v2/tests/test_leverage.py | 0 .../leverage}/test_v2/utils.py | 0 .../stateful/test_lend_controller_stateful.py | 3 + 33 files changed, 4 insertions(+), 393 deletions(-) delete mode 100644 .flake8 delete mode 100644 ape-config.yaml delete mode 100755 ape-run.sh delete mode 100644 brownie-config.yaml delete mode 100644 docs-requirements.txt delete mode 100644 docs/Makefile delete mode 100644 docs/source/conf.py delete mode 100644 docs/source/index.rst delete mode 100644 docs/source/stablecoin.rst delete mode 100755 pypytest delete mode 100644 pytest.ini rename {tests_forked => tests/forked}/__init__.py (100%) rename {tests_forked => tests/forked}/price_oracles/__init__.py (100%) rename {tests_forked => tests/forked}/price_oracles/conftest.py (100%) rename {tests_forked => tests/forked}/price_oracles/settings.py (100%) rename {tests_forked => tests/forked}/price_oracles/test_lp_oracle_compare_to_spot.py (100%) rename {tests_leverage => tests/leverage}/__init__.py (100%) rename {tests_leverage => tests/leverage}/test_v1/__init__.py (100%) rename {tests_leverage => tests/leverage}/test_v1/conftest.py (100%) rename {tests_leverage => tests/leverage}/test_v1/test_deleverage.py (100%) rename {tests_leverage => tests/leverage}/test_v1/test_deleverage_light.py (100%) rename {tests_leverage => tests/leverage}/test_v1/test_leverage.py (100%) rename {tests_leverage => tests/leverage}/test_v1/test_leverage_light.py (100%) rename {tests_leverage => tests/leverage}/test_v1/utils.py (100%) rename {tests_leverage => tests/leverage}/test_v2/__init__.py (100%) rename {tests_leverage => tests/leverage}/test_v2/conftest.py (100%) rename {tests_leverage => tests/leverage}/test_v2/constants.py (100%) rename {tests_leverage => tests/leverage}/test_v2/settings.py (100%) rename {tests_leverage => tests/leverage}/test_v2/tests/__init__.py (100%) rename {tests_leverage => tests/leverage}/test_v2/tests/test_leverage.py (100%) rename {tests_leverage => tests/leverage}/test_v2/utils.py (100%) create mode 100644 tests/stateful/test_lend_controller_stateful.py 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/.gitignore b/.gitignore index 31fd7e45..294eb2a9 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ prof .prof .pytest_cache AGENTS.md +.ruff_cache 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/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 6ba18668..00000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,33 +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 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 ba806568..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/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/tests_forked/__init__.py b/tests/forked/__init__.py similarity index 100% rename from tests_forked/__init__.py rename to tests/forked/__init__.py diff --git a/tests_forked/price_oracles/__init__.py b/tests/forked/price_oracles/__init__.py similarity index 100% rename from tests_forked/price_oracles/__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 similarity index 100% rename from tests_forked/price_oracles/conftest.py rename to tests/forked/price_oracles/conftest.py 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 similarity index 100% rename from tests_forked/price_oracles/test_lp_oracle_compare_to_spot.py rename to tests/forked/price_oracles/test_lp_oracle_compare_to_spot.py diff --git a/tests_leverage/__init__.py b/tests/leverage/__init__.py similarity index 100% rename from tests_leverage/__init__.py rename to tests/leverage/__init__.py diff --git a/tests_leverage/test_v1/__init__.py b/tests/leverage/test_v1/__init__.py similarity index 100% rename from tests_leverage/test_v1/__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 100% rename from tests_leverage/test_v1/conftest.py rename to tests/leverage/test_v1/conftest.py diff --git a/tests_leverage/test_v1/test_deleverage.py b/tests/leverage/test_v1/test_deleverage.py similarity index 100% rename from tests_leverage/test_v1/test_deleverage.py rename to tests/leverage/test_v1/test_deleverage.py diff --git a/tests_leverage/test_v1/test_deleverage_light.py b/tests/leverage/test_v1/test_deleverage_light.py similarity index 100% rename from tests_leverage/test_v1/test_deleverage_light.py rename to tests/leverage/test_v1/test_deleverage_light.py diff --git a/tests_leverage/test_v1/test_leverage.py b/tests/leverage/test_v1/test_leverage.py similarity index 100% rename from tests_leverage/test_v1/test_leverage.py rename to tests/leverage/test_v1/test_leverage.py diff --git a/tests_leverage/test_v1/test_leverage_light.py b/tests/leverage/test_v1/test_leverage_light.py similarity index 100% rename from tests_leverage/test_v1/test_leverage_light.py rename to tests/leverage/test_v1/test_leverage_light.py diff --git a/tests_leverage/test_v1/utils.py b/tests/leverage/test_v1/utils.py similarity index 100% rename from tests_leverage/test_v1/utils.py rename to tests/leverage/test_v1/utils.py diff --git a/tests_leverage/test_v2/__init__.py b/tests/leverage/test_v2/__init__.py similarity index 100% rename from tests_leverage/test_v2/__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 100% rename from tests_leverage/test_v2/conftest.py rename to tests/leverage/test_v2/conftest.py 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/test_v2/tests/__init__.py b/tests/leverage/test_v2/tests/__init__.py similarity index 100% rename from tests_leverage/test_v2/tests/__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 similarity index 100% rename from tests_leverage/test_v2/tests/test_leverage.py rename to tests/leverage/test_v2/tests/test_leverage.py diff --git a/tests_leverage/test_v2/utils.py b/tests/leverage/test_v2/utils.py similarity index 100% rename from tests_leverage/test_v2/utils.py rename to tests/leverage/test_v2/utils.py diff --git a/tests/stateful/test_lend_controller_stateful.py b/tests/stateful/test_lend_controller_stateful.py new file mode 100644 index 00000000..8ebb8d99 --- /dev/null +++ b/tests/stateful/test_lend_controller_stateful.py @@ -0,0 +1,3 @@ +# Just noting down things I want to test +# 1. This test inherits from test_controller_stateful.py +# 2. It should add some invariants to track borrowed balance + lent + collected From 210d15c10422d164a95573d042adb1f9d4336589 Mon Sep 17 00:00:00 2001 From: Oleg <40476427+amfet42@users.noreply.github.com> Date: Tue, 23 Sep 2025 00:06:48 +0200 Subject: [PATCH 312/413] add partial repay zap callback --- .../interfaces/IPartialRepayZapCallback.vyi | 44 ++++ .../testing/zaps/PartialRepayZapTester.vy | 10 + contracts/zaps/PartialRepayZapCallback.vy | 203 ++++++++++++++++++ tests/utils/deployers.py | 3 + tests/zaps/partial_liquidation/conftest.py | 70 ++++++ .../test_partial_repay_zap.py | 57 ----- .../test_partial_repay_zap_callback.py | 91 ++++++++ 7 files changed, 421 insertions(+), 57 deletions(-) create mode 100644 contracts/interfaces/IPartialRepayZapCallback.vyi create mode 100644 contracts/testing/zaps/PartialRepayZapTester.vy create mode 100644 contracts/zaps/PartialRepayZapCallback.vy create mode 100644 tests/zaps/partial_liquidation/conftest.py create mode 100644 tests/zaps/partial_liquidation/test_partial_repay_zap_callback.py diff --git a/contracts/interfaces/IPartialRepayZapCallback.vyi b/contracts/interfaces/IPartialRepayZapCallback.vyi new file mode 100644 index 00000000..0a528667 --- /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/testing/zaps/PartialRepayZapTester.vy b/contracts/testing/zaps/PartialRepayZapTester.vy new file mode 100644 index 00000000..4f3aa1ae --- /dev/null +++ b/contracts/testing/zaps/PartialRepayZapTester.vy @@ -0,0 +1,10 @@ +# pragma version 0.4.3 + +from contracts.interfaces import IERC20 +import contracts.lib.token_lib 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/PartialRepayZapCallback.vy b/contracts/zaps/PartialRepayZapCallback.vy new file mode 100644 index 00000000..da450b06 --- /dev/null +++ b/contracts/zaps/PartialRepayZapCallback.vy @@ -0,0 +1,203 @@ +# 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 contracts.interfaces import IERC20 +from contracts.interfaces import IAMM +from contracts.interfaces import IController +from contracts import Controller as ctrl +import contracts.lib.token_lib 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) + + tkn.transfer_from(BORROWED, msg.sender, self, borrowed_from_sender) + + 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 * 5 - 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/tests/utils/deployers.py b/tests/utils/deployers.py index f3e0e862..bc126bb9 100644 --- a/tests/utils/deployers.py +++ b/tests/utils/deployers.py @@ -81,6 +81,9 @@ 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( 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 index 1d0c6e1f..f913bfd5 100644 --- a/tests/zaps/partial_liquidation/test_partial_repay_zap.py +++ b/tests/zaps/partial_liquidation/test_partial_repay_zap.py @@ -1,63 +1,6 @@ import boa import pytest -from tests.utils.deployers import PARTIAL_REPAY_ZAP_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 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) - # We put mostly USD into AMM, and its quantity remains constant while - # interest is accruing. Therefore, we will be at liquidation at some point - with boa.env.prank(user): - amm.exchange(0, 1, debt, 0) - health_1 = controller.health(user) - - assert health_0 <= health_1 # Earns fees on dynamic fee - - boa.env.time_travel(sleep_time) - - health_2 = controller.health(user) - # Still healthy but liquidation threshold satisfied - assert 0 < health_2 < controller.liquidation_discount() - - with boa.env.prank(admin): - # Stop charging fees to have enough coins to liquidate in existence a block before - monetary_policy.set_rate(0) - - return controller - - return f - @pytest.mark.parametrize("is_approved", [True, False]) def test_users_to_liquidate( 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..7a48e19d --- /dev/null +++ b/tests/zaps/partial_liquidation/test_partial_repay_zap_callback.py @@ -0,0 +1,91 @@ +import boa +import pytest + +from eth_utils import to_bytes + + +@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: + callbacker = str(partial_repay_zap_callback.address) + controller.approve(callbacker, 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) + callbacker = str(partial_repay_zap_callback.address) + controller.approve(callbacker, True, sender=user) + + initial_health = controller.health(user) + + boa.deal(borrowed_token, liquidator, 10**21) + 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 From 49f7db81da7eec1bbdf3334cfe44a6c45af58160 Mon Sep 17 00:00:00 2001 From: Oleg <40476427+amfet42@users.noreply.github.com> Date: Tue, 23 Sep 2025 00:14:17 +0200 Subject: [PATCH 313/413] fix linter --- contracts/interfaces/IPartialRepayZapCallback.vyi | 6 +++--- tests/utils/deployers.py | 3 ++- .../partial_liquidation/test_partial_repay_zap_callback.py | 4 +++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/contracts/interfaces/IPartialRepayZapCallback.vyi b/contracts/interfaces/IPartialRepayZapCallback.vyi index 0a528667..43384b5b 100644 --- a/contracts/interfaces/IPartialRepayZapCallback.vyi +++ b/contracts/interfaces/IPartialRepayZapCallback.vyi @@ -24,9 +24,9 @@ def users_to_liquidate(_controller: IController, _from: uint256, _limit: uint256 @external def liquidate_partial( - _controller: IController, - _user: address, _min_x: uint256, - _callbacker: address = empty(address), + _controller: IController, + _user: address, _min_x: uint256, + _callbacker: address = empty(address), _calldata: Bytes[10 ** 4 - 32 * 6 - 16] = b"", ): ... diff --git a/tests/utils/deployers.py b/tests/utils/deployers.py index 8a7107bf..d26e135d 100644 --- a/tests/utils/deployers.py +++ b/tests/utils/deployers.py @@ -82,7 +82,8 @@ 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 + ZAPS_CONTRACT_PATH + "PartialRepayZapCallback.vy", + compiler_args=compiler_args_default, ) # Monetary policies - all have no pragma diff --git a/tests/zaps/partial_liquidation/test_partial_repay_zap_callback.py b/tests/zaps/partial_liquidation/test_partial_repay_zap_callback.py index 7a48e19d..ed903382 100644 --- a/tests/zaps/partial_liquidation/test_partial_repay_zap_callback.py +++ b/tests/zaps/partial_liquidation/test_partial_repay_zap_callback.py @@ -79,7 +79,9 @@ def test_liquidate_partial_callback( 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) + 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 From 47bf52bcd8baff285a1ff97bdfac82f1c09c775e Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 23 Sep 2025 11:39:52 +0400 Subject: [PATCH 314/413] test: single test for lend controller constructor --- .../lend_controller/test_borrow_cap_lc.py | 36 ---------------- .../lending/lend_controller/test_ctor_lc.py | 43 +++++++++++++++++-- .../lending/lend_controller/test_vault_lc.py | 2 - 3 files changed, 39 insertions(+), 42 deletions(-) delete mode 100644 tests/unitary/lending/lend_controller/test_borrow_cap_lc.py delete mode 100644 tests/unitary/lending/lend_controller/test_vault_lc.py diff --git a/tests/unitary/lending/lend_controller/test_borrow_cap_lc.py b/tests/unitary/lending/lend_controller/test_borrow_cap_lc.py deleted file mode 100644 index 5bfb29d3..00000000 --- a/tests/unitary/lending/lend_controller/test_borrow_cap_lc.py +++ /dev/null @@ -1,36 +0,0 @@ -import pytest - - -@pytest.fixture(scope="module") -def lending_controller( - proto, - collateral_token, - amm_A, - amm_fee, - loan_discount, - liquidation_discount, - price_oracle, - min_borrow_rate, - max_borrow_rate, -): - market = 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, - ) - return market["controller"] - - -def test_borrow_cap_default_behavior(lending_controller): - """ - Checks that freshly deployed lending controllers have a borrow cap of zero. - """ - assert lending_controller.borrow_cap() == 0 diff --git a/tests/unitary/lending/lend_controller/test_ctor_lc.py b/tests/unitary/lending/lend_controller/test_ctor_lc.py index b967928b..ab294d06 100644 --- a/tests/unitary/lending/lend_controller/test_ctor_lc.py +++ b/tests/unitary/lending/lend_controller/test_ctor_lc.py @@ -1,9 +1,44 @@ +import pytest from tests.utils.constants import MAX_UINT256 -def test_default_behavior(controller, vault, borrowed_token): - # No need to check vault address and borrow cap in here - # as their getter tests already do that. +@pytest.fixture(scope="module") +def market( + proto, + collateral_token, + amm_A, + amm_fee, + loan_discount, + liquidation_discount, + price_oracle, + min_borrow_rate, + max_borrow_rate, +): + market = 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, + ) + return market["controller"] - approved = borrowed_token.allowance(controller, vault) + +def test_default_behavior(fresh_market, 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 = proto.crvUSD.allowance(controller, vault) assert approved == MAX_UINT256 diff --git a/tests/unitary/lending/lend_controller/test_vault_lc.py b/tests/unitary/lending/lend_controller/test_vault_lc.py deleted file mode 100644 index 4bc37b5b..00000000 --- a/tests/unitary/lending/lend_controller/test_vault_lc.py +++ /dev/null @@ -1,2 +0,0 @@ -def test_default_behavior(controller, vault): - assert controller.vault() == vault.address From f01a5cbbedcbf87500c54b2f3fa24ca8e50219e9 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 23 Sep 2025 12:55:36 +0400 Subject: [PATCH 315/413] test: refactor test_borrowed_balance_lc --- .../test_borrowed_balance_lc.py | 133 ++++++++---------- 1 file changed, 58 insertions(+), 75 deletions(-) diff --git a/tests/unitary/lending/lend_controller/test_borrowed_balance_lc.py b/tests/unitary/lending/lend_controller/test_borrowed_balance_lc.py index 78adee0c..bdb19400 100644 --- a/tests/unitary/lending/lend_controller/test_borrowed_balance_lc.py +++ b/tests/unitary/lending/lend_controller/test_borrowed_balance_lc.py @@ -4,6 +4,7 @@ from tests.utils.constants import MIN_TICKS, WAD COLLATERAL = 10**21 +DEPOSIT = 10**18 DEBT = 10**18 @@ -13,6 +14,7 @@ def snapshot(controller, vault): "lent": controller.lent(), "repaid": controller.repaid(), "collected": controller.collected(), + "processed": controller.processed(), "deposited": vault.deposited(), "withdrawn": vault.withdrawn(), } @@ -23,94 +25,80 @@ def expect_same(before, after, *fields): assert after[field] == before[field] -def test_default_behavior(controller, vault, seed_liquidity): - state = snapshot(controller, vault) - assert state["lent"] == 0 - assert state["collected"] == 0 - assert state["repaid"] == 0 - assert state["withdrawn"] == 0 - assert state["borrowed"] == state["deposited"] == seed_liquidity +def test_increases_after_deposit(controller, vault, borrowed_token): + boa.deal(borrowed_token, boa.env.eoa, DEPOSIT) + max_approve(borrowed_token, vault.address) - -def test_decreases_after_borrow(controller, vault, collateral_token): before = snapshot(controller, vault) + vault.deposit(DEPOSIT) + after = snapshot(controller, vault) - boa.deal(collateral_token, boa.env.eoa, COLLATERAL) - max_approve(collateral_token, controller.address, sender=boa.env.eoa) + assert after["borrowed"] == before["borrowed"] + DEPOSIT + assert after["deposited"] == before["deposited"] + DEBT + expect_same(before, after, "lent", "repaid", "collected", "withdrawn", "processed") - controller.create_loan(COLLATERAL, DEBT, MIN_TICKS) +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"] - DEBT - assert after["lent"] == before["lent"] + DEBT - expect_same(before, after, "repaid", "collected", "deposited", "withdrawn") + assert after["borrowed"] == before["borrowed"] - DEPOSIT + assert after["withdrawn"] == before["withdrawn"] + DEBT + expect_same(before, after, "lent", "repaid", "collected", "deposited", "processed") -def test_restores_after_repay(controller, vault, collateral_token, borrowed_token): - before = snapshot(controller, vault) +def test_decreases_after_create(controller, vault, collateral_token): boa.deal(collateral_token, boa.env.eoa, COLLATERAL) - max_approve(collateral_token, controller.address, sender=boa.env.eoa) + max_approve(collateral_token, controller.address) + before = snapshot(controller, vault) controller.create_loan(COLLATERAL, DEBT, MIN_TICKS) - - max_approve(borrowed_token, controller.address, sender=boa.env.eoa) - controller.repay(DEBT) - after = snapshot(controller, vault) - expect_same(before, after, "borrowed", "collected", "deposited", "withdrawn") + + assert after["borrowed"] == before["borrowed"] - DEBT assert after["lent"] == before["lent"] + DEBT - assert after["repaid"] == before["repaid"] + DEBT + assert after["processed"] == before["processed"] + DEBT + expect_same(before, after, "repaid", "collected", "deposited", "withdrawn") -def test_decreases_after_withdraw( - controller, - vault, - collateral_token, - borrowed_token, -): - EXTRA_DEPOSIT = 5 * 10**18 - WITHDRAW_AMOUNT = EXTRA_DEPOSIT // 2 +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) - start = snapshot(controller, vault) + before = snapshot(controller, vault) + controller.borrow_more(COLLATERAL, DEBT) + after = snapshot(controller, vault) - boa.deal(borrowed_token, boa.env.eoa, EXTRA_DEPOSIT) - max_approve(borrowed_token, vault.address, sender=boa.env.eoa) - assert borrowed_token.balanceOf(boa.env.eoa) == EXTRA_DEPOSIT - vault.deposit(EXTRA_DEPOSIT) - max_approve(borrowed_token, controller.address, sender=boa.env.eoa) + assert after["borrowed"] == before["borrowed"] - DEBT + assert after["lent"] == before["lent"] + DEBT + assert after["processed"] == before["processed"] + DEBT + expect_same(before, after, "repaid", "collected", "deposited", "withdrawn") - after_deposit = snapshot(controller, vault) - assert after_deposit["borrowed"] == start["borrowed"] + EXTRA_DEPOSIT - assert after_deposit["deposited"] == start["deposited"] + EXTRA_DEPOSIT - expect_same(start, after_deposit, "lent", "repaid", "collected", "withdrawn") +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, sender=boa.env.eoa) + max_approve(collateral_token, controller.address) + max_approve(borrowed_token, controller.address) controller.create_loan(COLLATERAL, DEBT, MIN_TICKS) - after_loan = snapshot(controller, vault) - assert after_loan["borrowed"] == after_deposit["borrowed"] - DEBT - assert after_loan["lent"] == after_deposit["lent"] + DEBT - expect_same( - after_deposit, after_loan, "repaid", "collected", "deposited", "withdrawn" - ) - + before = snapshot(controller, vault) controller.repay(DEBT) - after_repay = snapshot(controller, vault) - expect_same( - after_deposit, after_repay, "borrowed", "collected", "deposited", "withdrawn" - ) - expect_same(after_loan, after_repay, "lent") - assert after_repay["repaid"] == after_deposit["repaid"] + DEBT - - withdrawn_before = vault.withdrawn() - vault.withdraw(WITHDRAW_AMOUNT) + after = snapshot(controller, vault) - after_withdraw = snapshot(controller, vault) - assert after_withdraw["borrowed"] == after_repay["borrowed"] - WITHDRAW_AMOUNT - assert after_withdraw["withdrawn"] == withdrawn_before + WITHDRAW_AMOUNT - expect_same(after_repay, after_withdraw, "lent", "repaid", "collected", "deposited") + assert after["borrowed"] == before["borrowed"] + DEBT + assert after["repaid"] == before["repaid"] + DEBT + expect_same(before, after, "lent", "collected", "deposited", "withdrawn", "processed") def test_collect_fees_reduces_balance( @@ -125,29 +113,24 @@ def test_collect_fees_reduces_balance( RATE = 10**11 TIME_DELTA = 86400 - before_fee = snapshot(controller, vault) controller.set_admin_fee(ADMIN_FEE, sender=admin) - after_fee = snapshot(controller, vault) - assert controller.admin_fee() == ADMIN_FEE - assert after_fee == before_fee boa.deal(collateral_token, boa.env.eoa, COLLATERAL) - max_approve(collateral_token, controller.address, sender=boa.env.eoa) + max_approve(collateral_token, controller.address) controller.create_loan(COLLATERAL, DEBT, MIN_TICKS) - before_collect = snapshot(controller, vault) amm.eval(f"self.rate = {RATE}") amm.eval("self.rate_time = block.timestamp") boa.env.time_travel(TIME_DELTA) - boa.deal(borrowed_token, controller.address, 10**24) - + before = snapshot(controller, vault) amount = controller.collect_fees() + after = snapshot(controller, vault) assert amount > 0 - after_collect = snapshot(controller, vault) - assert after_collect["collected"] == before_collect["collected"] + amount - assert after_collect["borrowed"] == before_collect["borrowed"] - amount + assert after["collected"] == before["collected"] + amount + assert after["borrowed"] == before["borrowed"] - amount + assert after["processed"] == after["repaid"] + controller.total_debt() expect_same( - before_collect, after_collect, "lent", "repaid", "deposited", "withdrawn" + before, after, "lent", "repaid", "deposited", "withdrawn" ) From b40f9adf98768786256a7486662600e6f7661938 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 23 Sep 2025 15:41:43 +0400 Subject: [PATCH 316/413] test: refactor test_admin_fees_lc --- .../lending/lend_controller/test_admin_fees_lc.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/tests/unitary/lending/lend_controller/test_admin_fees_lc.py b/tests/unitary/lending/lend_controller/test_admin_fees_lc.py index a93aa6f7..e8b6d658 100644 --- a/tests/unitary/lending/lend_controller/test_admin_fees_lc.py +++ b/tests/unitary/lending/lend_controller/test_admin_fees_lc.py @@ -7,7 +7,6 @@ DEBT = 10**18 RATE = 10**11 TIME_DELTA = 86400 -FEE_PCTS = [100, 75, 50, 25, 10] def test_default_behavior_no_fees(controller): @@ -33,14 +32,5 @@ def outstanding_for_pct(pct: int) -> int: return controller.admin_fees() - base_amount = outstanding_for_pct(100) - assert base_amount > 0 - - results = {100: base_amount} - for pct in FEE_PCTS[1:]: - results[pct] = outstanding_for_pct(pct) - expected = base_amount * pct // 100 - assert results[pct] == expected - - for higher, lower in zip(FEE_PCTS, FEE_PCTS[1:]): - assert results[higher] > results[lower] + for pct in range(1, 101): + assert outstanding_for_pct(pct) == DEBT * TIME_DELTA * RATE // 10**18 * pct // 100 From a967e8aefffd1562f549c43158b9d9aa88a54684 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 23 Sep 2025 16:01:51 +0400 Subject: [PATCH 317/413] test: refactor test_collect_fees_lc --- .../lend_controller/test_collect_fees_lc.py | 47 ++++++++++++------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/tests/unitary/lending/lend_controller/test_collect_fees_lc.py b/tests/unitary/lending/lend_controller/test_collect_fees_lc.py index 64874160..e597a2a4 100644 --- a/tests/unitary/lending/lend_controller/test_collect_fees_lc.py +++ b/tests/unitary/lending/lend_controller/test_collect_fees_lc.py @@ -7,7 +7,6 @@ DEBT = 10**18 RATE = 10**11 TIME_DELTA = 86400 -FEE_PCTS = [100, 75, 50, 25, 10] def test_default_behavior_no_fees(controller): @@ -24,10 +23,6 @@ def test_collect_fees_accrues_interest( collateral_token, borrowed_token, ): - # We iterate over a handful of admin-fee percentages and re-run the same setup for - # each entry, checking two things: (1) `collect_fees()` returns exactly what ends - # up in `controller.collected()` and (2) the amount is a linear proportion of the - # 100% baseline. def collect_for_pct(pct: int) -> int: with boa.env.anchor(): controller.set_admin_fee(WAD * pct // 100, sender=admin) @@ -39,19 +34,39 @@ def collect_for_pct(pct: int) -> int: amm.eval("self.rate_time = block.timestamp") boa.env.time_travel(TIME_DELTA) - boa.deal(borrowed_token, controller.address, 10**24) + expected = controller.admin_fees() amount = controller.collect_fees() - assert controller.collected() == amount + assert controller.collected() == amount == expected + return amount - base_amount = collect_for_pct(100) - assert base_amount > 0 + 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_fee(WAD, sender=admin) + debt = controller.borrowed_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) - results = {100: base_amount} - for pct in FEE_PCTS[1:]: - results[pct] = collect_for_pct(pct) - expected = base_amount * pct // 100 - assert results[pct] == expected + amm.eval(f"self.rate = {RATE}") + amm.eval("self.rate_time = block.timestamp") + boa.env.time_travel(TIME_DELTA) - for higher, lower in zip(FEE_PCTS, FEE_PCTS[1:]): - assert results[higher] > results[lower] + 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 From 48c2179d66ca416820f37e6f536637ae900ebcd5 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 23 Sep 2025 18:30:59 +0400 Subject: [PATCH 318/413] test: vault test_initialize --- .../lending/vault/test_initialize_v.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 tests/unitary/lending/vault/test_initialize_v.py 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..fc466ed6 --- /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 From 167849657133ed6c0101eb9ab1a63a59f0d9652d Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 23 Sep 2025 18:31:29 +0400 Subject: [PATCH 319/413] fix: test_ctor_lc --- tests/unitary/lending/lend_controller/test_ctor_lc.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unitary/lending/lend_controller/test_ctor_lc.py b/tests/unitary/lending/lend_controller/test_ctor_lc.py index ab294d06..8d5daf6d 100644 --- a/tests/unitary/lending/lend_controller/test_ctor_lc.py +++ b/tests/unitary/lending/lend_controller/test_ctor_lc.py @@ -3,8 +3,9 @@ @pytest.fixture(scope="module") -def market( +def fresh_market( proto, + borrowed_token, collateral_token, amm_A, amm_fee, @@ -14,7 +15,7 @@ def market( min_borrow_rate, max_borrow_rate, ): - market = proto.create_lending_market( + return proto.create_lending_market( borrowed_token=proto.crvUSD, collateral_token=collateral_token, A=amm_A, @@ -27,10 +28,9 @@ def market( max_borrow_rate=max_borrow_rate, seed_amount=0, ) - return market["controller"] -def test_default_behavior(fresh_market, proto): +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. @@ -40,5 +40,5 @@ def test_default_behavior(fresh_market, proto): assert controller.vault() == vault.address assert controller.borrow_cap() == 0 - approved = proto.crvUSD.allowance(controller, vault) + approved = borrowed_token.allowance(controller, vault) assert approved == MAX_UINT256 From 5686d541baafb3f1d7d485787c91eb53d7ea122d Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 24 Sep 2025 15:39:51 +0400 Subject: [PATCH 320/413] test: test_set_max_supply_v --- .../lending/vault/test_set_max_supply_v.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/unitary/lending/vault/test_set_max_supply_v.py 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) From bc3f04d48207a4d6c3bdb12e2cfa899d66bc6661 Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 24 Sep 2025 16:07:04 +0400 Subject: [PATCH 321/413] test: vault borrow_apr and lend_apr --- tests/unitary/lending/vault/test_borrow_apr_v.py | 9 +++++++++ tests/unitary/lending/vault/test_lend_apr_v.py | 15 +++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 tests/unitary/lending/vault/test_borrow_apr_v.py create mode 100644 tests/unitary/lending/vault/test_lend_apr_v.py 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..4b98b967 --- /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.""" + test_rate = 10**9 + amm.eval(f"self.rate = {10**9}") + seconds_in_year = 365 * 86400 # 31,536,000 seconds + expected_apr = test_rate * seconds_in_year + + actual_apr = vault.borrow_apr() + assert actual_apr == expected_apr 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..6e4e743a --- /dev/null +++ b/tests/unitary/lending/vault/test_lend_apr_v.py @@ -0,0 +1,15 @@ +def test_lend_apr_calculation(vault, amm, controller): + """Test that lend_apr correctly calculates lending APR.""" + test_rate = 10**9 + borrowed_balance = controller.borrowed_balance() + assert borrowed_balance > 0 + debt = borrowed_balance // 2 + + amm.eval(f"self.rate = {test_rate}") + controller.eval(f"core._total_debt.initial_debt = {debt}") + + seconds_in_year = 365 * 86400 + expected_apr = test_rate * seconds_in_year * debt // (debt + borrowed_balance) + + actual_apr = vault.lend_apr() + assert actual_apr == expected_apr From 7dd5e7a9a5503c5d4a244ca1fb368bcf6c73f6c6 Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 24 Sep 2025 18:07:38 +0400 Subject: [PATCH 322/413] test: vault asset(), _total_assets(), totalAssets --- tests/unitary/lending/vault/test_asset_v.py | 3 +++ .../lending/vault/test_total_assets_v.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 tests/unitary/lending/vault/test_asset_v.py create mode 100644 tests/unitary/lending/vault/test_total_assets_v.py 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_total_assets_v.py b/tests/unitary/lending/vault/test_total_assets_v.py new file mode 100644 index 00000000..b30d54a5 --- /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.borrowed_balance() + debt_value = 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_value}") + controller.eval(f"core._total_debt.rate_mul = {_total_debt_rate_mul}") + + expected_total = borrowed_balance + debt_value * 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() From c6b2d9ca0664a79b50d892bc6131cb7846449348 Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 24 Sep 2025 19:27:03 +0400 Subject: [PATCH 323/413] test: vault convert_to_shares --- .../lending/vault/test_convert_to_shares_v.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 tests/unitary/lending/vault/test_convert_to_shares_v.py 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..07a2a33f --- /dev/null +++ b/tests/unitary/lending/vault/test_convert_to_shares_v.py @@ -0,0 +1,59 @@ +def test_convert_to_shares(vault, controller, amm): + """Test _convert_to_shares with is_floor=True (default).""" + # Set up some assets in the vault + borrowed_balance = controller.borrowed_balance() + debt_value = 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_value}") + controller.eval(f"core._total_debt.rate_mul = {_total_debt_rate_mul}") + + assets = 100 * 10**18 + 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.""" + # Set up some assets in the vault + borrowed_balance = controller.borrowed_balance() + debt_value = borrowed_balance // 2 + controller.eval(f"core._total_debt.initial_debt = {debt_value}") + + assets = 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 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 From f0a4cb1ae79027dfa3885ee275af16751189d67c Mon Sep 17 00:00:00 2001 From: Alberto Date: Sat, 4 Oct 2025 20:07:25 +0200 Subject: [PATCH 324/413] test: test exact amount is pulled --- .../test_partial_repay_zap_callback.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/zaps/partial_liquidation/test_partial_repay_zap_callback.py b/tests/zaps/partial_liquidation/test_partial_repay_zap_callback.py index ed903382..79d3ce56 100644 --- a/tests/zaps/partial_liquidation/test_partial_repay_zap_callback.py +++ b/tests/zaps/partial_liquidation/test_partial_repay_zap_callback.py @@ -3,6 +3,8 @@ 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( @@ -91,3 +93,29 @@ def test_liquidate_partial_callback( assert borrowed_token.balanceOf(partial_repay_zap_callback.address) == 0 assert collateral_token.balanceOf(partial_repay_zap_callback.address) == 0 + + +def test_liquidate_partial_uses_exact_amount( + borrowed_token, + controller_for_liquidation, + partial_repay_zap_callback, +): + controller = controller_for_liquidation( + sleep_time=int(30.7 * 86400), user=boa.env.eoa + ) + + controller.approve(partial_repay_zap_callback.address, True) + + position = partial_repay_zap_callback.users_to_liquidate(controller.address)[0] + borrowed_from_sender = position.dy + + # get money to liquidate: in real scenario this would be a separate address + boa.deal(borrowed_token, boa.env.eoa, 10**21) + + borrowed_token.approve(partial_repay_zap_callback.address, MAX_UINT256) + pre_balance = borrowed_token.balanceOf(boa.env.eoa) + partial_repay_zap_callback.liquidate_partial(controller.address, user, 0) + post_balance = borrowed_token.balanceOf(boa.env.eoa) + + spent = pre_balance - post_balance + assert spent == borrowed_from_sender # TODO this actually pulls double the amount From 5544d7c1babc854fe4dfa23d69ea30b09b14d92f Mon Sep 17 00:00:00 2001 From: Alberto Date: Sat, 4 Oct 2025 21:16:23 +0200 Subject: [PATCH 325/413] test: add test that needs to be fixed --- tests/e2e/test_donate_dos.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/e2e/test_donate_dos.py diff --git a/tests/e2e/test_donate_dos.py b/tests/e2e/test_donate_dos.py new file mode 100644 index 00000000..8f333f61 --- /dev/null +++ b/tests/e2e/test_donate_dos.py @@ -0,0 +1,34 @@ +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, admin +): + deposited = vault.deposited() + + 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) + + controller.create_loan(COLLATERAL, deposited + DONATION, N_BANDS) + + vault.totalAssets() From 2f746459f0da7ba4388df696b6dd490f43376564 Mon Sep 17 00:00:00 2001 From: Alberto Date: Sun, 5 Oct 2025 11:58:25 +0200 Subject: [PATCH 326/413] style: format --- .../lending/lend_controller/test_admin_fees_lc.py | 4 +++- .../lend_controller/test_borrowed_balance_lc.py | 8 ++++---- .../unitary/lending/vault/test_convert_to_shares_v.py | 10 +++++++--- tests/unitary/lending/vault/test_initialize_v.py | 6 +++--- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/tests/unitary/lending/lend_controller/test_admin_fees_lc.py b/tests/unitary/lending/lend_controller/test_admin_fees_lc.py index e8b6d658..26c1a81d 100644 --- a/tests/unitary/lending/lend_controller/test_admin_fees_lc.py +++ b/tests/unitary/lending/lend_controller/test_admin_fees_lc.py @@ -33,4 +33,6 @@ def outstanding_for_pct(pct: int) -> int: return controller.admin_fees() for pct in range(1, 101): - assert outstanding_for_pct(pct) == DEBT * TIME_DELTA * RATE // 10**18 * pct // 100 + assert ( + outstanding_for_pct(pct) == DEBT * TIME_DELTA * RATE // 10**18 * pct // 100 + ) diff --git a/tests/unitary/lending/lend_controller/test_borrowed_balance_lc.py b/tests/unitary/lending/lend_controller/test_borrowed_balance_lc.py index bdb19400..0b1ad85f 100644 --- a/tests/unitary/lending/lend_controller/test_borrowed_balance_lc.py +++ b/tests/unitary/lending/lend_controller/test_borrowed_balance_lc.py @@ -98,7 +98,9 @@ def test_increases_after_repay(controller, vault, collateral_token, borrowed_tok assert after["borrowed"] == before["borrowed"] + DEBT assert after["repaid"] == before["repaid"] + DEBT - expect_same(before, after, "lent", "collected", "deposited", "withdrawn", "processed") + expect_same( + before, after, "lent", "collected", "deposited", "withdrawn", "processed" + ) def test_collect_fees_reduces_balance( @@ -131,6 +133,4 @@ def test_collect_fees_reduces_balance( 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", "deposited", "withdrawn" - ) + expect_same(before, after, "lent", "repaid", "deposited", "withdrawn") diff --git a/tests/unitary/lending/vault/test_convert_to_shares_v.py b/tests/unitary/lending/vault/test_convert_to_shares_v.py index 07a2a33f..4b5aefde 100644 --- a/tests/unitary/lending/vault/test_convert_to_shares_v.py +++ b/tests/unitary/lending/vault/test_convert_to_shares_v.py @@ -41,7 +41,7 @@ def test_convert_to_shares_with_total_assets(vault, controller, borrowed_token): controller.eval(f"core._total_debt.initial_debt = {debt_value}") assets = 100 * 10**18 - total_assets = 500 * 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") @@ -51,9 +51,13 @@ def test_convert_to_shares_with_total_assets(vault, controller, borrowed_token): denominator = total_assets * precision + 1 expected_shares_floor = numerator // denominator - actual_shares_floor = vault.eval(f"self._convert_to_shares({assets}, True, {total_assets})") + 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})") + 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_initialize_v.py b/tests/unitary/lending/vault/test_initialize_v.py index fc466ed6..4580e267 100644 --- a/tests/unitary/lending/vault/test_initialize_v.py +++ b/tests/unitary/lending/vault/test_initialize_v.py @@ -40,7 +40,7 @@ def test_default_behavior(fresh_market, collateral_token, borrowed_token, proto) 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.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 From 56c381f04683d9143224da8fa8e22cbc9a6a0982 Mon Sep 17 00:00:00 2001 From: Alberto Date: Sun, 5 Oct 2025 14:12:15 +0200 Subject: [PATCH 327/413] chore: add pre-commit --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index a038f941..246c0d85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ dev = [ "pytest-forked>=1.6.0", "pytest-cov>=4.0.0", "pytest-profiling>=1.8.1", + "pre-commit==4.0.1" ] [tool.uv.sources] From 4e624fd78be82a5d07944935db76a56bc34171f2 Mon Sep 17 00:00:00 2001 From: Alberto Date: Sun, 5 Oct 2025 14:13:03 +0200 Subject: [PATCH 328/413] chore: add pre-commit to dev deps --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 246c0d85..86b1fc15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dev = [ "pytest-forked>=1.6.0", "pytest-cov>=4.0.0", "pytest-profiling>=1.8.1", - "pre-commit==4.0.1" + "pre-commit==4.3.0", ] [tool.uv.sources] From fa6c2710ac93bc0e75eee02d479709f3ed02c803 Mon Sep 17 00:00:00 2001 From: macket Date: Sun, 5 Oct 2025 18:03:07 +0400 Subject: [PATCH 329/413] feat: repay with position shrink --- contracts/Controller.vy | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 0c03b0ae..9a0f7e82 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -904,6 +904,7 @@ def repay( max_active_band: int256 = max_value(int256), callbacker: address = empty(address), calldata: Bytes[CALLDATA_MAX_SIZE] = b"", + shrink: bool = False ): """ @notice Repay debt (partially or fully) @@ -913,6 +914,7 @@ def repay( @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 calldata Any data for callbacker + @param shrink Whether shrink soft-liquidated part of the position or not """ debt: uint256 = 0 rate_mul: uint256 = 0 @@ -985,9 +987,12 @@ def repay( debt = unsafe_sub(debt, d_debt) 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, too short collateral part" + size = unsafe_sub(ns[1], active_band + 1) liquidation_discount: uint256 = self.liquidation_discounts[_for] - if ns[0] > active_band: + if ns[0] > active_band or shrink: # Not in soft-liquidation - can use callback and move bands new_collateral: uint256 = cb.collateral if callbacker == empty(address): From c8ea0550195b3c24eaf7ed36e55af261a97cecd0 Mon Sep 17 00:00:00 2001 From: macket Date: Sun, 5 Oct 2025 18:56:52 +0400 Subject: [PATCH 330/413] fix: transfer borrowed from AMM to Controller when shrink --- contracts/Controller.vy | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 9a0f7e82..718bb689 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -943,7 +943,6 @@ def repay( total_borrowed: uint256 = 0 if xy[0] > 0: - # Only allow full repayment when underwater for the sender to do assert approval tkn.transfer_from(BORROWED_TOKEN, AMM.address, self, xy[0]) total_borrowed += xy[0] @@ -1020,6 +1019,9 @@ def repay( # full = False to make this condition non-manipulatable (and also cheaper on gas) assert self._health(_for, debt, False, liquidation_discount) > 0 + if xy[0] > 0 and shrink: + assert approval + tkn.transfer_from(BORROWED_TOKEN, AMM.address, self, xy[0]) if cb.borrowed > 0: tkn.transfer_from(BORROWED_TOKEN, callbacker, self, cb.borrowed) if _d_debt > 0: From e2ed92efc5c396a10c994646bd93ddd69953f9f4 Mon Sep 17 00:00:00 2001 From: Alberto Date: Sun, 5 Oct 2025 20:09:02 +0200 Subject: [PATCH 331/413] fix: add shrink to interfaces --- contracts/interfaces/IController.vyi | 1 + contracts/interfaces/ILlamalendController.vyi | 1 + 2 files changed, 2 insertions(+) diff --git a/contracts/interfaces/IController.vyi b/contracts/interfaces/IController.vyi index 5d49a871..7253158f 100644 --- a/contracts/interfaces/IController.vyi +++ b/contracts/interfaces/IController.vyi @@ -161,6 +161,7 @@ def repay( max_active_band: int256 = max_value(int256), callbacker: address = empty(address), calldata: Bytes[10000] = b"", + shrink: bool = False, ): ... diff --git a/contracts/interfaces/ILlamalendController.vyi b/contracts/interfaces/ILlamalendController.vyi index fe9697a1..3fbbce9c 100644 --- a/contracts/interfaces/ILlamalendController.vyi +++ b/contracts/interfaces/ILlamalendController.vyi @@ -145,6 +145,7 @@ def repay( max_active_band: int256 = max_value(int256), callbacker: address = empty(address), calldata: Bytes[10000] = b"", + shrink: bool = False, ): ... From 3b1659d027109ec540cec68fc83e0a7f270df3f4 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 6 Oct 2025 12:01:19 +0400 Subject: [PATCH 332/413] test: add rate_mul to test_lend_apr_v --- tests/unitary/lending/vault/test_lend_apr_v.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/unitary/lending/vault/test_lend_apr_v.py b/tests/unitary/lending/vault/test_lend_apr_v.py index 6e4e743a..18bf67d1 100644 --- a/tests/unitary/lending/vault/test_lend_apr_v.py +++ b/tests/unitary/lending/vault/test_lend_apr_v.py @@ -1,15 +1,20 @@ def test_lend_apr_calculation(vault, amm, controller): """Test that lend_apr correctly calculates lending APR.""" - test_rate = 10**9 + rate = 10**9 borrowed_balance = controller.borrowed_balance() assert borrowed_balance > 0 - debt = borrowed_balance // 2 - amm.eval(f"self.rate = {test_rate}") + 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 - expected_apr = test_rate * seconds_in_year * debt // (debt + borrowed_balance) + 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 From 2bf139f0390098a19fffd81ddd7e573afe8a5e29 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 6 Oct 2025 12:03:44 +0400 Subject: [PATCH 333/413] refactor: vault tests --- tests/unitary/lending/vault/test_borrow_apr_v.py | 4 ++-- tests/unitary/lending/vault/test_convert_to_shares_v.py | 6 ------ tests/unitary/lending/vault/test_total_assets_v.py | 6 +++--- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/tests/unitary/lending/vault/test_borrow_apr_v.py b/tests/unitary/lending/vault/test_borrow_apr_v.py index 4b98b967..c97c8725 100644 --- a/tests/unitary/lending/vault/test_borrow_apr_v.py +++ b/tests/unitary/lending/vault/test_borrow_apr_v.py @@ -1,9 +1,9 @@ def test_borrow_apr_calculation(vault, amm): """Test that borrow_apr correctly calculates annualized rate from AMM rate.""" - test_rate = 10**9 + rate = 10**9 amm.eval(f"self.rate = {10**9}") seconds_in_year = 365 * 86400 # 31,536,000 seconds - expected_apr = test_rate * seconds_in_year + 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_shares_v.py b/tests/unitary/lending/vault/test_convert_to_shares_v.py index 4b5aefde..84d310d0 100644 --- a/tests/unitary/lending/vault/test_convert_to_shares_v.py +++ b/tests/unitary/lending/vault/test_convert_to_shares_v.py @@ -3,7 +3,6 @@ def test_convert_to_shares(vault, controller, amm): # Set up some assets in the vault borrowed_balance = controller.borrowed_balance() debt_value = 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}") @@ -35,11 +34,6 @@ def test_convert_to_shares(vault, controller, amm): def test_convert_to_shares_with_total_assets(vault, controller, borrowed_token): """Test _convert_to_shares with custom _total_assets parameter.""" - # Set up some assets in the vault - borrowed_balance = controller.borrowed_balance() - debt_value = borrowed_balance // 2 - controller.eval(f"core._total_debt.initial_debt = {debt_value}") - assets = 100 * 10**18 total_assets = 500 * 10 ** borrowed_token.decimals() total_supply = vault.totalSupply() diff --git a/tests/unitary/lending/vault/test_total_assets_v.py b/tests/unitary/lending/vault/test_total_assets_v.py index b30d54a5..9ae610ff 100644 --- a/tests/unitary/lending/vault/test_total_assets_v.py +++ b/tests/unitary/lending/vault/test_total_assets_v.py @@ -3,14 +3,14 @@ def test_total_assets_calculation(vault, controller, amm): # Set specific debt value borrowed_balance = controller.borrowed_balance() - debt_value = borrowed_balance // 2 + 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_value}") + 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_value * 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 From 1a38b5105601f821fce6de5f2ab6f01a839c7030 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 6 Oct 2025 12:13:44 +0400 Subject: [PATCH 334/413] test: test_convert_to_assets_v --- .../lending/vault/test_convert_to_assets_v.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/unitary/lending/vault/test_convert_to_assets_v.py 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..f1afef6c --- /dev/null +++ b/tests/unitary/lending/vault/test_convert_to_assets_v.py @@ -0,0 +1,57 @@ +def test_convert_to_assets(vault, controller, amm): + """Test _convert_to_assets with is_floor=True and False.""" + # Set up some assets in the vault + borrowed_balance = controller.borrowed_balance() + debt_value = 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_value}") + controller.eval(f"core._total_debt.rate_mul = {_total_debt_rate_mul}") + + 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 From 911e70bdcab77c1cbd351d3621076b6b3163c1dd Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 6 Oct 2025 12:19:11 +0400 Subject: [PATCH 335/413] test: test_price_per_share_v --- .../lending/vault/test_price_per_share_v.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 tests/unitary/lending/vault/test_price_per_share_v.py 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..f2a50173 --- /dev/null +++ b/tests/unitary/lending/vault/test_price_per_share_v.py @@ -0,0 +1,41 @@ +def test_price_per_share(vault, controller, amm): + """Test pricePerShare with is_floor=True and False.""" + # Set up some assets in the vault + borrowed_balance = controller.borrowed_balance() + debt_value = 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_value}") + controller.eval(f"core._total_debt.rate_mul = {_total_debt_rate_mul}") + + 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 From 2f414a027dade787f77794f8a04596bf97298c6f Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 6 Oct 2025 12:40:14 +0400 Subject: [PATCH 336/413] test: test_max_deposit_v --- .../lending/vault/test_max_deposit_v.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/unitary/lending/vault/test_max_deposit_v.py 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 From 97d644142f9842df6caae65cc53f6d5ca6cd9fd6 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 6 Oct 2025 13:07:43 +0400 Subject: [PATCH 337/413] test: test_preview_deposit_v --- .../unitary/lending/vault/test_preview_deposit_v.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 tests/unitary/lending/vault/test_preview_deposit_v.py 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..56f628bc --- /dev/null +++ b/tests/unitary/lending/vault/test_preview_deposit_v.py @@ -0,0 +1,13 @@ +def test_preview_deposit(vault, controller, amm): + """Test previewDeposit returns correct shares for given assets.""" + # Set up some assets in the vault + borrowed_balance = controller.borrowed_balance() + debt_value = 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_value}") + controller.eval(f"core._total_debt.rate_mul = {_total_debt_rate_mul}") + + assets = 100 * 10**18 + assert vault.previewDeposit(assets) == vault.eval(f"self._convert_to_shares({assets})") From 315e9dfe6e898d93e2af79f44346d9d0c2ff57fb Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 6 Oct 2025 17:47:29 +0400 Subject: [PATCH 338/413] test: test_deposit_v --- tests/unitary/lending/vault/test_deposit_v.py | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 tests/unitary/lending/vault/test_deposit_v.py 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..406ae888 --- /dev/null +++ b/tests/unitary/lending/vault/test_deposit_v.py @@ -0,0 +1,144 @@ +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_deposited = vault.deposited() + 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 ** 18 + + # 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.deposited() == initial_deposited + 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_deposited = vault.deposited() + 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 ** 18 + + # 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.deposited() == initial_deposited + 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 = 1000 # 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.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**18 # 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) From 27308e1b6467b17a9ac73d12c1cebf8482a9494f Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 6 Oct 2025 15:52:55 +0200 Subject: [PATCH 339/413] refactor: split repay --- contracts/Controller.vy | 242 ++++++++++++++++++++++++---------------- 1 file changed, 143 insertions(+), 99 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 718bb689..7e788899 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -897,6 +897,137 @@ def _remove_from_list(_for: address): self.n_loans = last_loan_ix +@internal +def _repay_full( + _for: address, + _d_debt: uint256, + _approval: bool, + _xy: uint256[2], + _cb: IController.CallbackData, + _callbacker: address, +): + xy: uint256[2] = _xy + if _callbacker == empty(address): + xy = extcall AMM.withdraw(_for, WAD) + + # ================= Recover borrowed tokens (xy[0]) ================= + total_borrowed: uint256 = 0 + if xy[0] > 0: # pull borrowed tokens in AMM (already soft liquidated) + assert _approval + tkn.transfer_from(BORROWED_TOKEN, AMM.address, self, xy[0]) + total_borrowed += xy[0] + if _cb.borrowed > 0: # pull borrowed tokens from callback + tkn.transfer_from(BORROWED_TOKEN, _callbacker, self, _cb.borrowed) + total_borrowed += _cb.borrowed + if total_borrowed < _d_debt: # pull remaining borrowed tokens from user + d_debt_effective: uint256 = unsafe_sub( + _d_debt, xy[0] + _cb.borrowed + ) + tkn.transfer_from(BORROWED_TOKEN, msg.sender, self, d_debt_effective) + total_borrowed += d_debt_effective + # TODO can this be else for better readability? (transfer skips amounts == 0) + if total_borrowed > _d_debt: # if borrowed tokens from AMM+callback > debt + tkn.transfer( + BORROWED_TOKEN, _for, unsafe_sub(total_borrowed, _d_debt) + ) + + + # ================= Recover collateral tokens (xy[1]) ================= + if _callbacker == empty(address): + # TODO can skip this check as tkn_transfer_from does it for us + if xy[1] > 0: + tkn.transfer_from(COLLATERAL_TOKEN, AMM.address, _for, xy[1]) + else: + if _cb.collateral > 0: + tkn.transfer_from(COLLATERAL_TOKEN, _callbacker, _for, _cb.collateral) + + self._remove_from_list(_for) + + 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=_d_debt + ) + + +@internal +def _repay_partial( + _for: address, + _debt: uint256, + _d_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() + assert active_band <= _max_active_band + + new_debt: uint256 = unsafe_sub(_debt, _d_debt) + 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] + + xy: uint256[2] = _xy + cb: IController.CallbackData = _cb + if ns[0] > active_band or _shrink: + new_collateral: uint256 = cb.collateral + if _callbacker == empty(address): + xy = extcall AMM.withdraw(_for, WAD) + new_collateral = xy[1] + 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 - cannot use callback or move bands but can avoid a bad liquidation + xy = staticcall AMM.get_sum_xy(_for) + assert _callbacker == empty(address) + + 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 + + if xy[0] > 0 and _shrink: + assert _approval + tkn.transfer_from(BORROWED_TOKEN, AMM.address, self, xy[0]) + if cb.borrowed > 0: + tkn.transfer_from(BORROWED_TOKEN, _callbacker, self, cb.borrowed) + if _wallet_d_debt > 0: + tkn.transfer_from(BORROWED_TOKEN, msg.sender, self, _wallet_d_debt) + + log IController.UserState( + user=_for, + collateral=xy[1], + debt=new_debt, + n1=ns[0], + n2=ns[1], + liquidation_discount=liquidation_discount, + ) + log IController.Repay( + user=_for, collateral_decrease=0, loan_decrease=_d_debt + ) + + return new_debt + + @external def repay( _d_debt: uint256, @@ -935,108 +1066,21 @@ def repay( d_debt: uint256 = min(min(_d_debt, debt) + xy[0] + cb.borrowed, debt) assert d_debt > 0 # dev: no coins to repay - # If we have more borrowed tokens than the debt - full repayment and closing the position if d_debt >= debt: + self._repay_full(_for, d_debt, approval, xy, cb, callbacker) debt = 0 - if callbacker == empty(address): - xy = extcall AMM.withdraw(_for, WAD) - - total_borrowed: uint256 = 0 - if xy[0] > 0: - assert approval - tkn.transfer_from(BORROWED_TOKEN, AMM.address, self, xy[0]) - total_borrowed += xy[0] - if cb.borrowed > 0: - tkn.transfer_from(BORROWED_TOKEN, callbacker, self, cb.borrowed) - total_borrowed += cb.borrowed - if total_borrowed < d_debt: - _d_debt_effective: uint256 = unsafe_sub( - d_debt, xy[0] + cb.borrowed - ) # <= _d_debt - tkn.transfer_from( - BORROWED_TOKEN, msg.sender, self, _d_debt_effective - ) - total_borrowed += _d_debt_effective - - if total_borrowed > d_debt: - tkn.transfer( - BORROWED_TOKEN, _for, unsafe_sub(total_borrowed, d_debt) - ) - # Transfer collateral to _for - if callbacker == empty(address): - if xy[1] > 0: - tkn.transfer_from(COLLATERAL_TOKEN, AMM.address, _for, xy[1]) - else: - if cb.collateral > 0: - tkn.transfer_from( - COLLATERAL_TOKEN, callbacker, _for, cb.collateral - ) - self._remove_from_list(_for) - 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=d_debt - ) - # Else - partial repayment else: - active_band: int256 = staticcall AMM.active_band_with_skip() - assert active_band <= max_active_band - - debt = unsafe_sub(debt, d_debt) - 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, too short collateral part" - size = unsafe_sub(ns[1], active_band + 1) - liquidation_discount: uint256 = self.liquidation_discounts[_for] - - if ns[0] > active_band or shrink: - # Not in soft-liquidation - can use callback and move bands - new_collateral: uint256 = cb.collateral - if callbacker == empty(address): - xy = extcall AMM.withdraw(_for, WAD) - new_collateral = xy[1] - ns[0] = self._calculate_debt_n1( - new_collateral, - 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 - cannot use callback or move bands but can avoid a bad liquidation - xy = staticcall AMM.get_sum_xy(_for) - assert callbacker == empty(address) - - 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, debt, False, liquidation_discount) > 0 - - if xy[0] > 0 and shrink: - assert approval - tkn.transfer_from(BORROWED_TOKEN, AMM.address, self, xy[0]) - if cb.borrowed > 0: - tkn.transfer_from(BORROWED_TOKEN, callbacker, self, cb.borrowed) - if _d_debt > 0: - tkn.transfer_from(BORROWED_TOKEN, msg.sender, self, _d_debt) - - log IController.UserState( - user=_for, - collateral=xy[1], - debt=debt, - n1=ns[0], - n2=ns[1], - liquidation_discount=liquidation_discount, - ) - log IController.Repay( - user=_for, collateral_decrease=0, loan_decrease=d_debt + debt = self._repay_partial( + _for, + debt, + d_debt, + _d_debt, + approval, + xy, + cb, + callbacker, + max_active_band, + shrink, ) self.loan[_for] = IController.Loan(initial_debt=debt, rate_mul=rate_mul) From 4a89cc48fc5ad38fe192e12cc6812195ea6b48fc Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 6 Oct 2025 15:54:06 +0200 Subject: [PATCH 340/413] test: test repay full --- .../controller/test_internal_repay_full.py | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 tests/unitary/controller/test_internal_repay_full.py 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..1213b5bc --- /dev/null +++ b/tests/unitary/controller/test_internal_repay_full.py @@ -0,0 +1,105 @@ +import boa +import pytest +from textwrap import dedent + +from tests.utils import filter_logs, 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 repay_full( + _for: address, + _d_debt: uint256, + _approval: bool, + xy_borrowed: uint256, + xy_collateral: uint256 + ): + xy: uint256[2] = [xy_borrowed, xy_collateral] + cb: core.IController.CallbackData = empty(core.IController.CallbackData) + core._repay_full(_for, _d_debt, _approval, xy, cb, empty(address)) + """ + ) + ) + + +@pytest.fixture(scope="module") +def snapshot(controller, amm, fake_leverage): + def fn(token, borrower: str): + return { + "controller": token.balanceOf(controller), + "borrower": token.balanceOf(borrower), + "amm": token.balanceOf(amm), + "callback": token.balanceOf(fake_leverage), + } + + return fn + + +def test_default_behavior_no_callback( + controller, borrowed_token, collateral_token, amm, snapshot +): + borrower = boa.env.eoa + + boa.deal(collateral_token, borrower, COLLATERAL) + max_approve(collateral_token, controller) + controller.create_loan(COLLATERAL, DEBT, N_BANDS) + + max_approve(borrowed_token, controller) + debt = controller.debt(borrower) + + xy = amm.get_sum_xy(borrower) + + borrowed_token_before = snapshot(borrowed_token, borrower) + collateral_token_before = snapshot(collateral_token, borrower) + + controller.inject.repay_full(borrower, debt, True, xy[0], xy[1]) + + repay_logs = filter_logs(controller, "Repay") + state_logs = filter_logs(controller, "UserState") + + borrowed_token_after = snapshot(borrowed_token, borrower) + collateral_token_after = snapshot(collateral_token, borrower) + + repaid_amount = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrower_decrease = ( + borrowed_token_after["borrower"] - borrowed_token_before["borrower"] + ) + collateral_released = ( + collateral_token_after["borrower"] - collateral_token_before["borrower"] + ) + collateral_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] + + assert len(state_logs) == 1 + assert state_logs[0].user == borrower + assert state_logs[0].debt == 0 + assert state_logs[0].collateral == 0 + + assert len(repay_logs) == 1 + assert repay_logs[0].user == borrower + assert repay_logs[0].loan_decrease == repaid_amount + assert repay_logs[0].collateral_decrease == collateral_released + + assert repaid_amount == debt + assert borrower_decrease == -debt + assert borrowed_token_after["amm"] == borrowed_token_before["amm"] + assert borrowed_token_after["callback"] == borrowed_token_before["callback"] + + assert collateral_released == -collateral_from_amm + assert collateral_token_after["callback"] == collateral_token_before["callback"] + assert collateral_token_after["controller"] == collateral_token_before["controller"] + + assert collateral_released == COLLATERAL + assert borrowed_token.balanceOf(controller) == borrowed_token_after["controller"] + assert ( + collateral_token.balanceOf(controller) == collateral_token_after["controller"] + ) From bd7a103de54a34b669005ee70b4cddbf8aa67b62 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 6 Oct 2025 15:54:21 +0200 Subject: [PATCH 341/413] test: add MAX_INT256 const --- tests/utils/constants.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/utils/constants.py b/tests/utils/constants.py index eb90af06..44f51323 100644 --- a/tests/utils/constants.py +++ b/tests/utils/constants.py @@ -5,8 +5,11 @@ LENDING_FACTORY_DEPLOYER, ) -ZERO_ADDRESS = boa.eval("empty(address)") -MAX_UINT256 = boa.eval("max_value(uint256)") +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 From c7b3bf1858c781d9c08c17a6d3103a0dd1e4abf4 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 6 Oct 2025 15:54:55 +0200 Subject: [PATCH 342/413] test: add fake leverage as a global fixure --- tests/conftest.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 3aa68d34..aa8c1320 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,7 @@ from tests.utils.deployers import ( ERC20_MOCK_DEPLOYER, CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER, + FAKE_LEVERAGE_DEPLOYER, ) from tests.utils.protocols import Llamalend @@ -67,6 +68,18 @@ def borrowed_token(stablecoin): 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 mint_monetary_policy(proto): return proto.mint_monetary_policy From 53201a9552a18e9f3251a7955f05487e134a5d7b Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 6 Oct 2025 18:13:32 +0400 Subject: [PATCH 343/413] test: test_max_mint_v --- .../unitary/lending/vault/test_max_mint_v.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 tests/unitary/lending/vault/test_max_mint_v.py 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 From 4b3e7222ea0c1ce78c09f0f8d1a1e821240efa85 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 6 Oct 2025 18:14:59 +0400 Subject: [PATCH 344/413] refactor: use borrowed_token.decimals() instead of 18 --- .../lending/vault/test_convert_to_shares_v.py | 6 ++--- tests/unitary/lending/vault/test_deposit_v.py | 24 +++++++++++++------ .../lending/vault/test_preview_deposit_v.py | 8 ++++--- .../lending/vault/test_price_per_share_v.py | 2 +- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/tests/unitary/lending/vault/test_convert_to_shares_v.py b/tests/unitary/lending/vault/test_convert_to_shares_v.py index 84d310d0..3a5d7551 100644 --- a/tests/unitary/lending/vault/test_convert_to_shares_v.py +++ b/tests/unitary/lending/vault/test_convert_to_shares_v.py @@ -1,4 +1,4 @@ -def test_convert_to_shares(vault, controller, amm): +def test_convert_to_shares(vault, controller, amm, borrowed_token): """Test _convert_to_shares with is_floor=True (default).""" # Set up some assets in the vault borrowed_balance = controller.borrowed_balance() @@ -9,7 +9,7 @@ def test_convert_to_shares(vault, controller, amm): controller.eval(f"core._total_debt.initial_debt = {debt_value}") controller.eval(f"core._total_debt.rate_mul = {_total_debt_rate_mul}") - assets = 100 * 10**18 + assets = 100 * 10 ** borrowed_token.decimals() total_assets = vault.totalAssets() total_supply = vault.totalSupply() precision = vault.eval("self.precision") @@ -34,7 +34,7 @@ def test_convert_to_shares(vault, controller, amm): def test_convert_to_shares_with_total_assets(vault, controller, borrowed_token): """Test _convert_to_shares with custom _total_assets parameter.""" - assets = 100 * 10**18 + assets = 100 * 10 ** borrowed_token.decimals() total_assets = 500 * 10 ** borrowed_token.decimals() total_supply = vault.totalSupply() precision = vault.eval("self.precision") diff --git a/tests/unitary/lending/vault/test_deposit_v.py b/tests/unitary/lending/vault/test_deposit_v.py index 406ae888..90eb63b3 100644 --- a/tests/unitary/lending/vault/test_deposit_v.py +++ b/tests/unitary/lending/vault/test_deposit_v.py @@ -21,7 +21,7 @@ def test_deposit_basic(vault, controller, amm, monetary_policy, borrowed_token): assert amm.eval("self.rate_time") == 0 - assets = 100 * 10 ** 18 + assets = 100 * 10 ** borrowed_token.decimals() # Give user tokens and approve vault boa.deal(borrowed_token, boa.env.eoa, assets) @@ -44,7 +44,10 @@ def test_deposit_basic(vault, controller, amm, monetary_policy, borrowed_token): assert vault.balanceOf(boa.env.eoa) == initial_sender_balance + shares assert vault.deposited() == initial_deposited + assets assert vault.totalSupply() == initial_total_supply + shares - assert borrowed_token.balanceOf(controller.address) == initial_controller_balance + assets + assert ( + borrowed_token.balanceOf(controller.address) + == initial_controller_balance + assets + ) assert borrowed_token.balanceOf(boa.env.eoa) == 0 # Check rate was saved @@ -73,7 +76,7 @@ def test_deposit_with_receiver(vault, controller, amm, monetary_policy, borrowed assert amm.eval("self.rate_time") == 0 - assets = 100 * 10 ** 18 + assets = 100 * 10 ** borrowed_token.decimals() # Give user tokens and approve vault boa.deal(borrowed_token, boa.env.eoa, assets) @@ -93,11 +96,18 @@ def test_deposit_with_receiver(vault, controller, amm, monetary_policy, borrowed 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.balanceOf(boa.env.eoa) == initial_sender_balance + ) # Sender balance unchanged + assert ( + vault.balanceOf(receiver) == initial_receiver_balance + shares + ) # Receiver gets shares assert vault.deposited() == initial_deposited + assets assert vault.totalSupply() == initial_total_supply + shares - assert borrowed_token.balanceOf(controller.address) == initial_controller_balance + assets + assert ( + borrowed_token.balanceOf(controller.address) + == initial_controller_balance + assets + ) assert borrowed_token.balanceOf(boa.env.eoa) == 0 # Check rate was saved @@ -131,7 +141,7 @@ def test_deposit_supply_limit_revert(vault, controller, amm, borrowed_token): assert vault.totalAssets() == 0 # Set a low max supply - max_supply = 100 * 10**18 # Just above current assets + max_supply = 100 * 10 ** borrowed_token.decimals() # Just above current assets vault.eval(f"self.maxSupply = {max_supply}") # Try to deposit more than allowed diff --git a/tests/unitary/lending/vault/test_preview_deposit_v.py b/tests/unitary/lending/vault/test_preview_deposit_v.py index 56f628bc..b2c6c01b 100644 --- a/tests/unitary/lending/vault/test_preview_deposit_v.py +++ b/tests/unitary/lending/vault/test_preview_deposit_v.py @@ -1,4 +1,4 @@ -def test_preview_deposit(vault, controller, amm): +def test_preview_deposit(vault, controller, amm, borrowed_token): """Test previewDeposit returns correct shares for given assets.""" # Set up some assets in the vault borrowed_balance = controller.borrowed_balance() @@ -9,5 +9,7 @@ def test_preview_deposit(vault, controller, amm): controller.eval(f"core._total_debt.initial_debt = {debt_value}") controller.eval(f"core._total_debt.rate_mul = {_total_debt_rate_mul}") - assets = 100 * 10**18 - assert vault.previewDeposit(assets) == vault.eval(f"self._convert_to_shares({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_price_per_share_v.py b/tests/unitary/lending/vault/test_price_per_share_v.py index f2a50173..911dc982 100644 --- a/tests/unitary/lending/vault/test_price_per_share_v.py +++ b/tests/unitary/lending/vault/test_price_per_share_v.py @@ -35,7 +35,7 @@ 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 From 472a38433be3d0bf907fd8438745bc2bad5d551d Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 6 Oct 2025 18:20:12 +0400 Subject: [PATCH 345/413] test: test_preview_mint_v --- .../unitary/lending/vault/test_preview_mint_v.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 tests/unitary/lending/vault/test_preview_mint_v.py 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..70b9e747 --- /dev/null +++ b/tests/unitary/lending/vault/test_preview_mint_v.py @@ -0,0 +1,15 @@ +def test_preview_mint(vault, controller, amm, borrowed_token): + """Test previewMint returns correct assets for given shares.""" + # Set up some assets in the vault + borrowed_balance = controller.borrowed_balance() + debt_value = 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_value}") + controller.eval(f"core._total_debt.rate_mul = {_total_debt_rate_mul}") + + shares = 100 * 10 ** borrowed_token.decimals() + assert vault.previewMint(shares) == vault.eval( + f"self._convert_to_assets({shares}, False)" + ) From f91d160e9595e5d368efdc7494dc938780313e84 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 6 Oct 2025 18:38:03 +0400 Subject: [PATCH 346/413] test: test_mint_v --- tests/unitary/lending/vault/test_mint_v.py | 152 +++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 tests/unitary/lending/vault/test_mint_v.py 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..2c54877d --- /dev/null +++ b/tests/unitary/lending/vault/test_mint_v.py @@ -0,0 +1,152 @@ +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_deposited = vault.deposited() + 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.deposited() == initial_deposited + 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_deposited = vault.deposited() + 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.deposited() == initial_deposited + 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) From adae8e6bfa4a088c8408d409368364ff63150be8 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 6 Oct 2025 18:38:38 +0400 Subject: [PATCH 347/413] test: refactor test_deposit_need_more_assets_revert --- tests/unitary/lending/vault/test_deposit_v.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unitary/lending/vault/test_deposit_v.py b/tests/unitary/lending/vault/test_deposit_v.py index 90eb63b3..f3e3d479 100644 --- a/tests/unitary/lending/vault/test_deposit_v.py +++ b/tests/unitary/lending/vault/test_deposit_v.py @@ -127,7 +127,7 @@ def test_deposit_need_more_assets_revert(vault, controller, amm, borrowed_token) assert vault.totalAssets() == 0 # Small deposit that would make total assets < MIN_ASSETS - assets = 1000 # Very small amount + assets = vault.eval("MIN_ASSETS") - 1 boa.deal(borrowed_token, boa.env.eoa, assets) borrowed_token.approve(vault, assets) From c7d471b63cd578a059a4b372ad50694f35c479a7 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 6 Oct 2025 17:56:47 +0200 Subject: [PATCH 348/413] perf: remove redundant checks --- contracts/Controller.vy | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 7e788899..0c33a1c8 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -678,10 +678,7 @@ def _create_loan( ) tkn.transfer_from(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) - if more_collateral > 0: - tkn.transfer_from( - COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral - ) + tkn.transfer_from(COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral) if callbacker == empty(address): tkn.transfer(BORROWED_TOKEN, _for, debt) @@ -869,10 +866,7 @@ def _borrow_more( ) tkn.transfer_from(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) - if more_collateral > 0: - tkn.transfer_from( - COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral - ) + tkn.transfer_from(COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral) if callbacker == empty(address): tkn.transfer(BORROWED_TOKEN, _for, debt) @@ -934,12 +928,9 @@ def _repay_full( # ================= Recover collateral tokens (xy[1]) ================= if _callbacker == empty(address): - # TODO can skip this check as tkn_transfer_from does it for us - if xy[1] > 0: - tkn.transfer_from(COLLATERAL_TOKEN, AMM.address, _for, xy[1]) + tkn.transfer_from(COLLATERAL_TOKEN, AMM.address, _for, xy[1]) else: - if _cb.collateral > 0: - tkn.transfer_from(COLLATERAL_TOKEN, _callbacker, _for, _cb.collateral) + tkn.transfer_from(COLLATERAL_TOKEN, _callbacker, _for, _cb.collateral) self._remove_from_list(_for) @@ -1005,13 +996,11 @@ def _repay_partial( # full = False to make this condition non-manipulatable (and also cheaper on gas) assert self._health(_for, new_debt, False, liquidation_discount) > 0 - if xy[0] > 0 and _shrink: + if _shrink: assert _approval tkn.transfer_from(BORROWED_TOKEN, AMM.address, self, xy[0]) - if cb.borrowed > 0: - tkn.transfer_from(BORROWED_TOKEN, _callbacker, self, cb.borrowed) - if _wallet_d_debt > 0: - tkn.transfer_from(BORROWED_TOKEN, msg.sender, self, _wallet_d_debt) + tkn.transfer_from(BORROWED_TOKEN, _callbacker, self, cb.borrowed) + tkn.transfer_from(BORROWED_TOKEN, msg.sender, self, _wallet_d_debt) log IController.UserState( user=_for, From 59a63c9c633288bb9fe95792131018652d139917 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 6 Oct 2025 18:48:04 +0200 Subject: [PATCH 349/413] test: repay --- contracts/Controller.vy | 2 +- contracts/testing/FakeLeverage.vy | 4 ++ tests/unitary/controller/test_repay.py | 95 ++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 tests/unitary/controller/test_repay.py diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 0c33a1c8..e60ccd3c 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -1045,7 +1045,7 @@ def repay( cb: IController.CallbackData = empty(IController.CallbackData) if callbacker != empty(address): - assert approval + 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( diff --git a/contracts/testing/FakeLeverage.vy b/contracts/testing/FakeLeverage.vy index fe6ea39c..631254ea 100644 --- a/contracts/testing/FakeLeverage.vy +++ b/contracts/testing/FakeLeverage.vy @@ -5,6 +5,8 @@ STABLECOIN: immutable(IERC20) COLLATERAL: immutable(IERC20) price: public(uint256) +callback_deposit_hits: public(uint256) +callback_repay_hits: public(uint256) @deploy @@ -29,6 +31,7 @@ def approve_all(): @external 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 @@ -38,6 +41,7 @@ def callback_deposit(user: address, stablecoins_no_use: uint256, collateral: uin @external 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 diff --git a/tests/unitary/controller/test_repay.py b/tests/unitary/controller/test_repay.py new file mode 100644 index 00000000..2dd56840 --- /dev/null +++ b/tests/unitary/controller/test_repay.py @@ -0,0 +1,95 @@ +import boa + +from tests.utils import max_approve +from tests.utils.constants import MAX_INT256, MAX_UINT256 + +COLLATERAL = 10**21 +DEBT = 10**18 +N_BANDS = 6 + + +def loan_state(controller, user): + return controller.eval( + f"(core.loan[{user}].initial_debt, core.loan[{user}].rate_mul)" + ) + + +def open_loan(controller, collateral_token, borrowed_token): + borrower = boa.env.eoa + boa.deal(collateral_token, borrower, COLLATERAL) + max_approve(collateral_token, controller) + controller.create_loan(COLLATERAL, DEBT, N_BANDS) + debt, _ = loan_state(controller, borrower) + assert debt == DEBT + max_approve(borrowed_token, controller) + return borrower + + +def test_default_behavior_full_repay(controller, borrowed_token, collateral_token): + borrower = open_loan(controller, collateral_token, borrowed_token) + repaid_before = controller.repaid() + + controller.repay(MAX_UINT256, borrower) + + debt, _ = loan_state(controller, borrower) + assert debt == 0 + assert controller.repaid() == repaid_before + DEBT + + +def test_default_behavior_partial_repay(controller, borrowed_token, collateral_token): + borrower = open_loan(controller, collateral_token, borrowed_token) + partial = DEBT // 2 + repaid_before = controller.repaid() + + controller.repay(partial, borrower) + + debt, _ = loan_state(controller, borrower) + assert debt == DEBT - partial + assert controller.repaid() == repaid_before + partial + + +def test_default_behavior_with_callback( + controller, borrowed_token, collateral_token, fake_leverage, amm +): + borrower = open_loan(controller, collateral_token, borrowed_token) + xy = amm.get_sum_xy(borrower) + + boa.deal(collateral_token, fake_leverage.address, xy[1]) + boa.deal(borrowed_token, fake_leverage.address, DEBT) + hits_before = fake_leverage.callback_repay_hits() + + controller.approve(fake_leverage.address, True) + controller.repay( + MAX_UINT256, + borrower, + MAX_INT256, + fake_leverage.address, + (0).to_bytes(32, "big"), + ) + + debt, _ = loan_state(controller, borrower) + assert debt == 0 + assert fake_leverage.callback_repay_hits() == hits_before + 1 + + +def test_callback_needs_approval( + controller, borrowed_token, collateral_token, fake_leverage, amm +): + borrower = open_loan(controller, collateral_token, borrowed_token) + xy = amm.get_sum_xy(borrower) + + boa.deal(collateral_token, fake_leverage.address, xy[1]) + boa.deal(borrowed_token, fake_leverage.address, DEBT) + + repayer = boa.env.generate_address("repayer") + boa.deal(borrowed_token, repayer, DEBT) + + with boa.reverts(dev="need approval for callback"): + controller.repay( + DEBT, + borrower, + MAX_INT256, + fake_leverage.address, + (0).to_bytes(32, "big"), + sender=repayer, + ) From f99b95c4f78821333bd1d7a9ee930107545af798 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 6 Oct 2025 19:09:13 +0200 Subject: [PATCH 350/413] test: remove from list --- .../test_internal_remove_from_list.py | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 tests/unitary/controller/test_internal_remove_from_list.py 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 From a27e44c325edf96b8f60fc6af43a8d2340bb2c1d Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 7 Oct 2025 12:37:50 +0400 Subject: [PATCH 351/413] test: test_max_withdraw_v --- .../lending/vault/test_max_withdraw_v.py | 89 +++++++++++++++++++ tests/unitary/lending/vault/test_mint_v.py | 8 +- 2 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 tests/unitary/lending/vault/test_max_withdraw_v.py 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..1f84cc20 --- /dev/null +++ b/tests/unitary/lending/vault/test_max_withdraw_v.py @@ -0,0 +1,89 @@ +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 + + +@pytest.fixture(scope="module") +def deposit(vault, controller, amm, borrowed_token): + def f(user=boa.env.eoa): + assets = 100 * 10 ** borrowed_token.decimals() + boa.deal(borrowed_token, user, assets) + with boa.env.prank(user): + borrowed_token.approve(vault, assets) + vault.deposit(assets) + + return f + + +def test_max_withdraw_user_balance_limited( + vault, controller, amm, borrowed_token, deposit +): + """Test maxWithdraw when user balance is the limiting factor.""" + assert controller.borrowed_balance() == 0 + deposit() + assert controller.borrowed_balance() > 0 + + # 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 +): + """Test maxWithdraw when controller liquidity is the limiting factor.""" + assert controller.borrowed_balance() == 0 + deposit() + + # Reduce controller balance to be less than user's position + controller_balance = controller.borrowed_balance() + assert controller_balance > 0 + # Increase lent to reduce borrowed_balance + controller.eval(f"self.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): + """Test maxWithdraw when user has no shares.""" + # Load borrowed tokens into vault + assert controller.borrowed_balance() == 0 + deposit(boa.env.generate_address()) + assert controller.borrowed_balance() > 0 + + # 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 +): + """Test maxWithdraw when controller has no liquidity.""" + assert controller.borrowed_balance() == 0 + deposit() + + # Set controller balance to 0 by setting lent = borrowed_balance + borrowed_balance = controller.borrowed_balance() + assert borrowed_balance > 0 + controller.eval(f"self.lent = {borrowed_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 index 2c54877d..30393988 100644 --- a/tests/unitary/lending/vault/test_mint_v.py +++ b/tests/unitary/lending/vault/test_mint_v.py @@ -96,8 +96,12 @@ def test_mint_with_receiver(vault, controller, amm, monetary_policy, borrowed_to 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.balanceOf(boa.env.eoa) == initial_sender_balance + ) # Sender balance unchanged + assert ( + vault.balanceOf(receiver) == initial_receiver_balance + shares + ) # Receiver gets shares assert vault.deposited() == initial_deposited + assets assert vault.totalSupply() == initial_total_supply + shares assert ( From 1815d4f2d5a1c022e2b4fcac7840f39ea27135e3 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 7 Oct 2025 13:00:36 +0400 Subject: [PATCH 352/413] refactor: make_debt and deposit_into_vault fixtures --- tests/unitary/lending/conftest.py | 27 ++++++++++++ .../lending/vault/test_convert_to_assets_v.py | 11 +---- .../lending/vault/test_convert_to_shares_v.py | 11 +---- .../lending/vault/test_max_withdraw_v.py | 41 +++++-------------- .../lending/vault/test_preview_deposit_v.py | 11 +---- .../lending/vault/test_preview_mint_v.py | 11 +---- .../lending/vault/test_price_per_share_v.py | 11 +---- 7 files changed, 43 insertions(+), 80 deletions(-) diff --git a/tests/unitary/lending/conftest.py b/tests/unitary/lending/conftest.py index 1541e357..ad833217 100644 --- a/tests/unitary/lending/conftest.py +++ b/tests/unitary/lending/conftest.py @@ -1,6 +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.borrowed_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.borrowed_balance() + with boa.env.prank(user): + borrowed_token.approve(vault, assets) + vault.deposit(assets) + assert controller.borrowed_balance() == initial_balance + assets + + return f diff --git a/tests/unitary/lending/vault/test_convert_to_assets_v.py b/tests/unitary/lending/vault/test_convert_to_assets_v.py index f1afef6c..7259f965 100644 --- a/tests/unitary/lending/vault/test_convert_to_assets_v.py +++ b/tests/unitary/lending/vault/test_convert_to_assets_v.py @@ -1,14 +1,5 @@ -def test_convert_to_assets(vault, controller, amm): +def test_convert_to_assets(vault, controller, amm, make_debt): """Test _convert_to_assets with is_floor=True and False.""" - # Set up some assets in the vault - borrowed_balance = controller.borrowed_balance() - debt_value = 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_value}") - controller.eval(f"core._total_debt.rate_mul = {_total_debt_rate_mul}") - shares = 100 * 10**18 total_assets = vault.totalAssets() total_supply = vault.totalSupply() diff --git a/tests/unitary/lending/vault/test_convert_to_shares_v.py b/tests/unitary/lending/vault/test_convert_to_shares_v.py index 3a5d7551..89380e63 100644 --- a/tests/unitary/lending/vault/test_convert_to_shares_v.py +++ b/tests/unitary/lending/vault/test_convert_to_shares_v.py @@ -1,14 +1,5 @@ -def test_convert_to_shares(vault, controller, amm, borrowed_token): +def test_convert_to_shares(vault, controller, amm, borrowed_token, make_debt): """Test _convert_to_shares with is_floor=True (default).""" - # Set up some assets in the vault - borrowed_balance = controller.borrowed_balance() - debt_value = 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_value}") - controller.eval(f"core._total_debt.rate_mul = {_total_debt_rate_mul}") - assets = 100 * 10 ** borrowed_token.decimals() total_assets = vault.totalAssets() total_supply = vault.totalSupply() diff --git a/tests/unitary/lending/vault/test_max_withdraw_v.py b/tests/unitary/lending/vault/test_max_withdraw_v.py index 1f84cc20..c7fc2d15 100644 --- a/tests/unitary/lending/vault/test_max_withdraw_v.py +++ b/tests/unitary/lending/vault/test_max_withdraw_v.py @@ -10,25 +10,11 @@ def seed_liquidity(): return 0 -@pytest.fixture(scope="module") -def deposit(vault, controller, amm, borrowed_token): - def f(user=boa.env.eoa): - assets = 100 * 10 ** borrowed_token.decimals() - boa.deal(borrowed_token, user, assets) - with boa.env.prank(user): - borrowed_token.approve(vault, assets) - vault.deposit(assets) - - return f - - def test_max_withdraw_user_balance_limited( - vault, controller, amm, borrowed_token, deposit + vault, controller, amm, borrowed_token, deposit_into_vault ): """Test maxWithdraw when user balance is the limiting factor.""" - assert controller.borrowed_balance() == 0 - deposit() - assert controller.borrowed_balance() > 0 + deposit_into_vault() # maxWithdraw should be limited by user's balance user_balance = vault.balanceOf(boa.env.eoa) @@ -38,15 +24,13 @@ def test_max_withdraw_user_balance_limited( def test_max_withdraw_controller_limited( - vault, controller, amm, borrowed_token, deposit + vault, controller, amm, borrowed_token, deposit_into_vault ): """Test maxWithdraw when controller liquidity is the limiting factor.""" - assert controller.borrowed_balance() == 0 - deposit() + deposit_into_vault() # Reduce controller balance to be less than user's position controller_balance = controller.borrowed_balance() - assert controller_balance > 0 # Increase lent to reduce borrowed_balance controller.eval(f"self.lent = {controller_balance // 2}") limited_balance = controller_balance // 2 @@ -56,12 +40,12 @@ def test_max_withdraw_controller_limited( assert actual_max == limited_balance -def test_max_withdraw_zero_balance(vault, controller, amm, borrowed_token, deposit): +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 - assert controller.borrowed_balance() == 0 - deposit(boa.env.generate_address()) - assert controller.borrowed_balance() > 0 + deposit_into_vault(boa.env.generate_address()) # User has no shares user_balance = vault.balanceOf(boa.env.eoa) @@ -73,16 +57,13 @@ def test_max_withdraw_zero_balance(vault, controller, amm, borrowed_token, depos def test_max_withdraw_zero_controller_balance( - vault, controller, amm, borrowed_token, deposit + vault, controller, amm, borrowed_token, deposit_into_vault ): """Test maxWithdraw when controller has no liquidity.""" - assert controller.borrowed_balance() == 0 - deposit() + deposit_into_vault() # Set controller balance to 0 by setting lent = borrowed_balance - borrowed_balance = controller.borrowed_balance() - assert borrowed_balance > 0 - controller.eval(f"self.lent = {borrowed_balance}") + controller.eval(f"self.lent = {controller.borrowed_balance()}") # maxWithdraw should return 0 (limited by controller balance) actual_max = vault.maxWithdraw(boa.env.eoa) diff --git a/tests/unitary/lending/vault/test_preview_deposit_v.py b/tests/unitary/lending/vault/test_preview_deposit_v.py index b2c6c01b..d87b33a1 100644 --- a/tests/unitary/lending/vault/test_preview_deposit_v.py +++ b/tests/unitary/lending/vault/test_preview_deposit_v.py @@ -1,14 +1,5 @@ -def test_preview_deposit(vault, controller, amm, borrowed_token): +def test_preview_deposit(vault, controller, amm, borrowed_token, make_debt): """Test previewDeposit returns correct shares for given assets.""" - # Set up some assets in the vault - borrowed_balance = controller.borrowed_balance() - debt_value = 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_value}") - controller.eval(f"core._total_debt.rate_mul = {_total_debt_rate_mul}") - 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 index 70b9e747..abe6252c 100644 --- a/tests/unitary/lending/vault/test_preview_mint_v.py +++ b/tests/unitary/lending/vault/test_preview_mint_v.py @@ -1,14 +1,5 @@ -def test_preview_mint(vault, controller, amm, borrowed_token): +def test_preview_mint(vault, controller, amm, borrowed_token, make_debt): """Test previewMint returns correct assets for given shares.""" - # Set up some assets in the vault - borrowed_balance = controller.borrowed_balance() - debt_value = 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_value}") - controller.eval(f"core._total_debt.rate_mul = {_total_debt_rate_mul}") - 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_price_per_share_v.py b/tests/unitary/lending/vault/test_price_per_share_v.py index 911dc982..4755ef73 100644 --- a/tests/unitary/lending/vault/test_price_per_share_v.py +++ b/tests/unitary/lending/vault/test_price_per_share_v.py @@ -1,14 +1,5 @@ -def test_price_per_share(vault, controller, amm): +def test_price_per_share(vault, controller, amm, make_debt): """Test pricePerShare with is_floor=True and False.""" - # Set up some assets in the vault - borrowed_balance = controller.borrowed_balance() - debt_value = 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_value}") - controller.eval(f"core._total_debt.rate_mul = {_total_debt_rate_mul}") - total_assets = vault.totalAssets() total_supply = vault.totalSupply() precision = vault.eval("self.precision") From f27e2c2c9c573a1e9f928dc5fa61444d98dc6715 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 7 Oct 2025 13:02:12 +0400 Subject: [PATCH 353/413] test: test_preview_withdraw_v --- .../lending/vault/test_preview_withdraw_v.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/unitary/lending/vault/test_preview_withdraw_v.py 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..9054ee7c --- /dev/null +++ b/tests/unitary/lending/vault/test_preview_withdraw_v.py @@ -0,0 +1,34 @@ +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.borrowed_balance() + assets = borrowed_balance + 1 # More than available + + # Should revert due to assert in previewWithdraw + with boa.reverts(): + vault.previewWithdraw(assets) From d88c07143e9666ac71356a9f5d106b9892680dfa Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 7 Oct 2025 14:46:24 +0400 Subject: [PATCH 354/413] test: test_withdraw_v --- .../lending/vault/test_preview_withdraw_v.py | 4 +- .../unitary/lending/vault/test_withdraw_v.py | 273 ++++++++++++++++++ 2 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 tests/unitary/lending/vault/test_withdraw_v.py diff --git a/tests/unitary/lending/vault/test_preview_withdraw_v.py b/tests/unitary/lending/vault/test_preview_withdraw_v.py index 9054ee7c..421362d6 100644 --- a/tests/unitary/lending/vault/test_preview_withdraw_v.py +++ b/tests/unitary/lending/vault/test_preview_withdraw_v.py @@ -20,7 +20,9 @@ def test_preview_withdraw(vault, controller, amm, borrowed_token, deposit_into_v ) -def test_preview_withdraw_assert_revert(vault, controller, amm, borrowed_token, deposit_into_vault): +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() 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..147c7737 --- /dev/null +++ b/tests/unitary/lending/vault/test_withdraw_v.py @@ -0,0 +1,273 @@ +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_withdrawn = vault.withdrawn() + 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.withdrawn() == initial_withdrawn + 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_withdrawn = vault.withdrawn() + 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.withdrawn() == initial_withdrawn + 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_withdrawn = vault.withdrawn() + 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.withdrawn() == initial_withdrawn + 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_withdrawn = vault.withdrawn() + 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.withdrawn() == initial_withdrawn + 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) From 234a9ce63391c61665e39b26f22dc4a92c4c2789 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 7 Oct 2025 15:12:03 +0400 Subject: [PATCH 355/413] chore: format vyper --- .../unitary/lending/vault/test_withdraw_v.py | 100 ++++++++++++++---- 1 file changed, 77 insertions(+), 23 deletions(-) diff --git a/tests/unitary/lending/vault/test_withdraw_v.py b/tests/unitary/lending/vault/test_withdraw_v.py index 147c7737..702f4d2d 100644 --- a/tests/unitary/lending/vault/test_withdraw_v.py +++ b/tests/unitary/lending/vault/test_withdraw_v.py @@ -2,7 +2,9 @@ from tests.utils import filter_logs -def test_withdraw_basic(vault, controller, amm, monetary_policy, borrowed_token, deposit_into_vault): +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) @@ -36,8 +38,14 @@ def test_withdraw_basic(vault, controller, amm, monetary_policy, borrowed_token, assert vault.balanceOf(boa.env.eoa) == initial_sender_balance - shares assert vault.totalSupply() == initial_total_supply - shares assert vault.withdrawn() == initial_withdrawn + 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 + 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 @@ -52,7 +60,9 @@ def test_withdraw_basic(vault, controller, amm, monetary_policy, borrowed_token, assert logs[0].shares == shares -def test_withdraw_with_receiver(vault, controller, amm, monetary_policy, borrowed_token, deposit_into_vault): +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() @@ -88,13 +98,25 @@ def test_withdraw_with_receiver(vault, controller, amm, monetary_policy, borrowe 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.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.withdrawn() == initial_withdrawn + 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 + 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 @@ -109,7 +131,9 @@ def test_withdraw_with_receiver(vault, controller, amm, monetary_policy, borrowe assert logs[0].shares == shares -def test_withdraw_with_owner(vault, controller, amm, monetary_policy, borrowed_token, deposit_into_vault): +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() @@ -150,12 +174,20 @@ def test_withdraw_with_owner(vault, controller, amm, monetary_policy, borrowed_t 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(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.withdrawn() == initial_withdrawn + 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 + 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 @@ -170,7 +202,9 @@ def test_withdraw_with_owner(vault, controller, amm, monetary_policy, borrowed_t assert logs[0].shares == shares -def test_withdraw_with_owner_and_receiver(vault, controller, amm, monetary_policy, borrowed_token, deposit_into_vault): +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() @@ -215,15 +249,29 @@ def test_withdraw_with_owner_and_receiver(vault, controller, amm, monetary_polic 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(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.balanceOf(receiver) == initial_receiver_balance + ) # Receiver gets no shares assert vault.totalSupply() == initial_total_supply - shares assert vault.withdrawn() == initial_withdrawn + 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 + 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 @@ -238,21 +286,27 @@ def test_withdraw_with_owner_and_receiver(vault, controller, amm, monetary_polic assert logs[0].shares == shares -def test_withdraw_need_more_assets_revert(vault, controller, amm, borrowed_token, deposit_into_vault): +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 + 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): +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() From de9214500ea19d038bd8bd4fdb117822748eb756 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 7 Oct 2025 15:13:00 +0400 Subject: [PATCH 356/413] test: maxRedeem, previewRedeem and redeem --- .../lending/vault/test_max_redeem_v.py | 64 +++++ .../lending/vault/test_preview_redeem_v.py | 47 +++ tests/unitary/lending/vault/test_redeem_v.py | 271 ++++++++++++++++++ 3 files changed, 382 insertions(+) create mode 100644 tests/unitary/lending/vault/test_max_redeem_v.py create mode 100644 tests/unitary/lending/vault/test_preview_redeem_v.py create mode 100644 tests/unitary/lending/vault/test_redeem_v.py 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..84aa8a3b --- /dev/null +++ b/tests/unitary/lending/vault/test_max_redeem_v.py @@ -0,0 +1,64 @@ +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.borrowed_balance() + # Increase lent to reduce borrowed_balance + controller.eval(f"self.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.borrowed_balance() + controller.eval(f"self.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_preview_redeem_v.py b/tests/unitary/lending/vault/test_preview_redeem_v.py new file mode 100644 index 00000000..bd935554 --- /dev/null +++ b/tests/unitary/lending/vault/test_preview_redeem_v.py @@ -0,0 +1,47 @@ +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.borrowed_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_redeem_v.py b/tests/unitary/lending/vault/test_redeem_v.py new file mode 100644 index 00000000..200130cd --- /dev/null +++ b/tests/unitary/lending/vault/test_redeem_v.py @@ -0,0 +1,271 @@ +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_withdrawn = vault.withdrawn() + 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.withdrawn() == initial_withdrawn + 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_withdrawn = vault.withdrawn() + 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.withdrawn() == initial_withdrawn + 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_withdrawn = vault.withdrawn() + 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.withdrawn() == initial_withdrawn + 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_withdrawn = vault.withdrawn() + 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.withdrawn() == initial_withdrawn + 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) From e547e3505db5dd076834abb6b983d227d34aa8e5 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 7 Oct 2025 15:18:01 +0400 Subject: [PATCH 357/413] test: vault admin --- tests/unitary/lending/vault/test_admin_v.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 tests/unitary/lending/vault/test_admin_v.py 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 From d22fa8624a712b71009a5fecf8dfba4c448733db Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 7 Oct 2025 13:47:50 +0200 Subject: [PATCH 358/413] perf: don't reassign var --- contracts/Controller.vy | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index e60ccd3c..28d44973 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -900,22 +900,21 @@ def _repay_full( _cb: IController.CallbackData, _callbacker: address, ): - xy: uint256[2] = _xy if _callbacker == empty(address): - xy = extcall AMM.withdraw(_for, WAD) + _xy = extcall AMM.withdraw(_for, WAD) # ================= Recover borrowed tokens (xy[0]) ================= total_borrowed: uint256 = 0 - if xy[0] > 0: # pull borrowed tokens in AMM (already soft liquidated) + if _xy[0] > 0: # pull borrowed tokens in AMM (already soft liquidated) assert _approval - tkn.transfer_from(BORROWED_TOKEN, AMM.address, self, xy[0]) - total_borrowed += xy[0] + tkn.transfer_from(BORROWED_TOKEN, AMM.address, self, _xy[0]) + total_borrowed += _xy[0] if _cb.borrowed > 0: # pull borrowed tokens from callback tkn.transfer_from(BORROWED_TOKEN, _callbacker, self, _cb.borrowed) total_borrowed += _cb.borrowed if total_borrowed < _d_debt: # pull remaining borrowed tokens from user d_debt_effective: uint256 = unsafe_sub( - _d_debt, xy[0] + _cb.borrowed + _d_debt, _xy[0] + _cb.borrowed ) tkn.transfer_from(BORROWED_TOKEN, msg.sender, self, d_debt_effective) total_borrowed += d_debt_effective @@ -928,7 +927,7 @@ def _repay_full( # ================= Recover collateral tokens (xy[1]) ================= if _callbacker == empty(address): - tkn.transfer_from(COLLATERAL_TOKEN, AMM.address, _for, xy[1]) + tkn.transfer_from(COLLATERAL_TOKEN, AMM.address, _for, _xy[1]) else: tkn.transfer_from(COLLATERAL_TOKEN, _callbacker, _for, _cb.collateral) @@ -938,7 +937,7 @@ def _repay_full( user=_for, collateral=0, debt=0, n1=0, n2=0, liquidation_discount=0 ) log IController.Repay( - user=_for, collateral_decrease=xy[1], loan_decrease=_d_debt + user=_for, collateral_decrease=_xy[1], loan_decrease=_d_debt ) @@ -967,13 +966,11 @@ def _repay_partial( size = unsafe_sub(ns[1], active_band + 1) liquidation_discount: uint256 = self.liquidation_discounts[_for] - xy: uint256[2] = _xy - cb: IController.CallbackData = _cb if ns[0] > active_band or _shrink: - new_collateral: uint256 = cb.collateral + new_collateral: uint256 = _cb.collateral if _callbacker == empty(address): - xy = extcall AMM.withdraw(_for, WAD) - new_collateral = xy[1] + _xy = extcall AMM.withdraw(_for, WAD) + new_collateral = _xy[1] ns[0] = self._calculate_debt_n1( new_collateral, new_debt, @@ -984,7 +981,7 @@ def _repay_partial( extcall AMM.deposit_range(_for, new_collateral, ns[0], ns[1]) else: # Underwater - cannot use callback or move bands but can avoid a bad liquidation - xy = staticcall AMM.get_sum_xy(_for) + _xy = staticcall AMM.get_sum_xy(_for) assert _callbacker == empty(address) if _approval: @@ -998,13 +995,13 @@ def _repay_partial( 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, 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) log IController.UserState( user=_for, - collateral=xy[1], + collateral=_xy[1], debt=new_debt, n1=ns[0], n2=ns[1], From e425aa19b675fbcf1ccb49b8c7d7fa24bb3a5d49 Mon Sep 17 00:00:00 2001 From: Oleg <40476427+amfet42@users.noreply.github.com> Date: Tue, 7 Oct 2025 16:07:02 +0200 Subject: [PATCH 359/413] fix zap callback and add tests --- contracts/zaps/PartialRepayZapCallback.vy | 2 - .../test_partial_repay_zap_callback.py | 49 ++++++++++++------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/contracts/zaps/PartialRepayZapCallback.vy b/contracts/zaps/PartialRepayZapCallback.vy index da450b06..23558ed1 100644 --- a/contracts/zaps/PartialRepayZapCallback.vy +++ b/contracts/zaps/PartialRepayZapCallback.vy @@ -122,8 +122,6 @@ def liquidate_partial( 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) - 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) diff --git a/tests/zaps/partial_liquidation/test_partial_repay_zap_callback.py b/tests/zaps/partial_liquidation/test_partial_repay_zap_callback.py index 79d3ce56..e42b4d8b 100644 --- a/tests/zaps/partial_liquidation/test_partial_repay_zap_callback.py +++ b/tests/zaps/partial_liquidation/test_partial_repay_zap_callback.py @@ -17,8 +17,7 @@ def test_users_to_liquidate_callback( controller = controller_for_liquidation(sleep_time=int(33 * 86400), user=user) if is_approved: - callbacker = str(partial_repay_zap_callback.address) - controller.approve(callbacker, True, sender=user) + controller.approve(partial_repay_zap_callback.address, True, sender=user) users_to_liquidate = partial_repay_zap_callback.users_to_liquidate( controller.address @@ -69,14 +68,11 @@ def test_liquidate_partial_callback( user = accounts[1] liquidator = accounts[2] controller = controller_for_liquidation(sleep_time=int(30.7 * 86400), user=user) - callbacker = str(partial_repay_zap_callback.address) - controller.approve(callbacker, True, sender=user) + controller.approve(partial_repay_zap_callback.address, True, sender=user) initial_health = controller.health(user) - boa.deal(borrowed_token, liquidator, 10**21) 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): @@ -95,27 +91,44 @@ def test_liquidate_partial_callback( 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, ): - controller = controller_for_liquidation( - sleep_time=int(30.7 * 86400), user=boa.env.eoa - ) - - controller.approve(partial_repay_zap_callback.address, True) + 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 - # get money to liquidate: in real scenario this would be a separate address - boa.deal(borrowed_token, boa.env.eoa, 10**21) + 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) - borrowed_token.approve(partial_repay_zap_callback.address, MAX_UINT256) - pre_balance = borrowed_token.balanceOf(boa.env.eoa) - partial_repay_zap_callback.liquidate_partial(controller.address, user, 0) - post_balance = borrowed_token.balanceOf(boa.env.eoa) + 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 # TODO this actually pulls double the amount + assert spent == borrowed_from_sender From 6464688782286d17a61ab934b796f9597cfd0b2b Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 7 Oct 2025 19:09:38 +0400 Subject: [PATCH 360/413] fix: full repay --- contracts/Controller.vy | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 28d44973..2e2c99b7 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -894,7 +894,7 @@ def _remove_from_list(_for: address): @internal def _repay_full( _for: address, - _d_debt: uint256, + _debt: uint256, _approval: bool, _xy: uint256[2], _cb: IController.CallbackData, @@ -904,25 +904,14 @@ def _repay_full( _xy = extcall AMM.withdraw(_for, WAD) # ================= Recover borrowed tokens (xy[0]) ================= - total_borrowed: uint256 = 0 - if _xy[0] > 0: # pull borrowed tokens in AMM (already soft liquidated) + _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]) - total_borrowed += _xy[0] - if _cb.borrowed > 0: # pull borrowed tokens from callback - tkn.transfer_from(BORROWED_TOKEN, _callbacker, self, _cb.borrowed) - total_borrowed += _cb.borrowed - if total_borrowed < _d_debt: # pull remaining borrowed tokens from user - d_debt_effective: uint256 = unsafe_sub( - _d_debt, _xy[0] + _cb.borrowed - ) - tkn.transfer_from(BORROWED_TOKEN, msg.sender, self, d_debt_effective) - total_borrowed += d_debt_effective - # TODO can this be else for better readability? (transfer skips amounts == 0) - if total_borrowed > _d_debt: # if borrowed tokens from AMM+callback > debt - tkn.transfer( - BORROWED_TOKEN, _for, unsafe_sub(total_borrowed, _d_debt) - ) + 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]) ================= @@ -1053,6 +1042,7 @@ def repay( assert d_debt > 0 # dev: no coins to repay if d_debt >= debt: + d_debt = debt self._repay_full(_for, d_debt, approval, xy, cb, callbacker) debt = 0 else: From 3e25848c25a8240632bc22e1c6282a01f396c321 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 7 Oct 2025 19:27:07 +0400 Subject: [PATCH 361/413] refactor: make repay more readable --- contracts/Controller.vy | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 2e2c99b7..a1d242bc 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -926,14 +926,14 @@ def _repay_full( user=_for, collateral=0, debt=0, n1=0, n2=0, liquidation_discount=0 ) log IController.Repay( - user=_for, collateral_decrease=_xy[1], loan_decrease=_d_debt + user=_for, collateral_decrease=_xy[1], loan_decrease=_debt ) @internal def _repay_partial( _for: address, - _debt: uint256, + _new_debt: uint256, _d_debt: uint256, _wallet_d_debt: uint256, _approval: bool, @@ -942,12 +942,11 @@ def _repay_partial( _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() assert active_band <= _max_active_band - new_debt: uint256 = unsafe_sub(_debt, _d_debt) 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: @@ -962,7 +961,7 @@ def _repay_partial( new_collateral = _xy[1] ns[0] = self._calculate_debt_n1( new_collateral, - new_debt, + _new_debt, convert(unsafe_add(size, 1), uint256), _for, ) @@ -980,7 +979,7 @@ def _repay_partial( 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 + assert self._health(_for, _new_debt, False, liquidation_discount) > 0 if _shrink: assert _approval @@ -991,7 +990,7 @@ def _repay_partial( log IController.UserState( user=_for, collateral=_xy[1], - debt=new_debt, + debt=_new_debt, n1=ns[0], n2=ns[1], liquidation_discount=liquidation_discount, @@ -1000,12 +999,10 @@ def _repay_partial( user=_for, collateral_decrease=0, loan_decrease=_d_debt ) - return new_debt - @external def repay( - _d_debt: uint256, + _wallet_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 = max_value(int256), callbacker: address = empty(address), @@ -1014,7 +1011,7 @@ def repay( ): """ @notice Repay debt (partially or fully) - @param _d_debt The amount of debt to repay from user's wallet. + @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) @@ -1038,19 +1035,20 @@ def repay( callbacker, CALLBACK_REPAY, _for, xy[0], xy[1], debt, calldata ) - d_debt: uint256 = min(min(_d_debt, debt) + xy[0] + cb.borrowed, debt) + 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: d_debt = debt - self._repay_full(_for, d_debt, approval, xy, cb, callbacker) debt = 0 + self._repay_full(_for, d_debt, approval, xy, cb, callbacker) else: - debt = self._repay_partial( + debt = unsafe_sub(debt, d_debt) + self._repay_partial( _for, debt, d_debt, - _d_debt, + _wallet_d_debt, approval, xy, cb, From 0311423e0bd57a82facfdea82ef79de3d2d22b67 Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 7 Oct 2025 17:54:48 +0200 Subject: [PATCH 362/413] ci: tests run regardless of linting --- .github/workflows/pre-commit.yaml | 27 +++++++++++++++++++++++++++ .github/workflows/test.yaml | 21 --------------------- 2 files changed, 27 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/pre-commit.yaml 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 index 40530bc9..d02676c1 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -7,30 +7,9 @@ env: HYPOTHESIS_PROFILE: no-shrink 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 - tests: name: ${{ matrix.folder }} runs-on: ubuntu-latest - needs: pre-commit strategy: fail-fast: false matrix: From 10c964638920e18132cfeee0cf676b8aebc50a47 Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 8 Oct 2025 09:34:29 +0400 Subject: [PATCH 363/413] chore: formatting --- .../lending/vault/test_max_redeem_v.py | 16 +++- .../lending/vault/test_preview_redeem_v.py | 10 +- tests/unitary/lending/vault/test_redeem_v.py | 96 ++++++++++++++----- 3 files changed, 93 insertions(+), 29 deletions(-) diff --git a/tests/unitary/lending/vault/test_max_redeem_v.py b/tests/unitary/lending/vault/test_max_redeem_v.py index 84aa8a3b..46cc2a19 100644 --- a/tests/unitary/lending/vault/test_max_redeem_v.py +++ b/tests/unitary/lending/vault/test_max_redeem_v.py @@ -10,7 +10,9 @@ def seed_liquidity(): return 0 -def test_max_redeem_user_balance_limited(vault, controller, amm, borrowed_token, deposit_into_vault): +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() @@ -21,7 +23,9 @@ def test_max_redeem_user_balance_limited(vault, controller, amm, borrowed_token, assert actual_max == expected_max -def test_max_redeem_controller_limited(vault, controller, amm, borrowed_token, deposit_into_vault): +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() @@ -37,7 +41,9 @@ def test_max_redeem_controller_limited(vault, controller, amm, borrowed_token, d assert actual_max == expected_max -def test_max_redeem_zero_balance(vault, controller, amm, borrowed_token, deposit_into_vault): +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()) @@ -51,7 +57,9 @@ def test_max_redeem_zero_balance(vault, controller, amm, borrowed_token, deposit assert actual_max == 0 -def test_max_redeem_zero_controller_balance(vault, controller, amm, borrowed_token, deposit_into_vault): +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() diff --git a/tests/unitary/lending/vault/test_preview_redeem_v.py b/tests/unitary/lending/vault/test_preview_redeem_v.py index bd935554..3e41089d 100644 --- a/tests/unitary/lending/vault/test_preview_redeem_v.py +++ b/tests/unitary/lending/vault/test_preview_redeem_v.py @@ -14,13 +14,17 @@ def test_preview_redeem(vault, controller, amm, borrowed_token, deposit_into_vau """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})") + 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): +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() diff --git a/tests/unitary/lending/vault/test_redeem_v.py b/tests/unitary/lending/vault/test_redeem_v.py index 200130cd..ad6bc755 100644 --- a/tests/unitary/lending/vault/test_redeem_v.py +++ b/tests/unitary/lending/vault/test_redeem_v.py @@ -2,7 +2,9 @@ from tests.utils import filter_logs -def test_redeem_basic(vault, controller, amm, monetary_policy, borrowed_token, deposit_into_vault): +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) @@ -36,8 +38,14 @@ def test_redeem_basic(vault, controller, amm, monetary_policy, borrowed_token, d assert vault.balanceOf(boa.env.eoa) == initial_sender_balance - shares_to_redeem assert vault.totalSupply() == initial_total_supply - shares_to_redeem assert vault.withdrawn() == initial_withdrawn + 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 + 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 @@ -52,7 +60,9 @@ def test_redeem_basic(vault, controller, amm, monetary_policy, borrowed_token, d assert logs[0].shares == shares_to_redeem -def test_redeem_with_receiver(vault, controller, amm, monetary_policy, borrowed_token, deposit_into_vault): +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() @@ -88,13 +98,25 @@ def test_redeem_with_receiver(vault, controller, amm, monetary_policy, borrowed_ 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.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.withdrawn() == initial_withdrawn + 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 + 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 @@ -109,7 +131,9 @@ def test_redeem_with_receiver(vault, controller, amm, monetary_policy, borrowed_ assert logs[0].shares == shares_to_redeem -def test_redeem_with_owner(vault, controller, amm, monetary_policy, borrowed_token, deposit_into_vault): +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() @@ -150,12 +174,20 @@ def test_redeem_with_owner(vault, controller, amm, monetary_policy, borrowed_tok 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(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.withdrawn() == initial_withdrawn + 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 + 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 @@ -170,7 +202,9 @@ def test_redeem_with_owner(vault, controller, amm, monetary_policy, borrowed_tok assert logs[0].shares == shares_to_redeem -def test_redeem_with_owner_and_receiver(vault, controller, amm, monetary_policy, borrowed_token, deposit_into_vault): +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() @@ -214,14 +248,26 @@ def test_redeem_with_owner_and_receiver(vault, controller, amm, monetary_policy, 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(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.balanceOf(receiver) == initial_receiver_balance + ) # Receiver gets no shares assert vault.totalSupply() == initial_total_supply - shares_to_redeem assert vault.withdrawn() == initial_withdrawn + 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 + 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 @@ -236,21 +282,27 @@ def test_redeem_with_owner_and_receiver(vault, controller, amm, monetary_policy, assert logs[0].shares == shares_to_redeem -def test_redeem_need_more_assets_revert(vault, controller, amm, borrowed_token, deposit_into_vault): +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) + 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): +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() From 16d5c1ba56b119589528dd38718c3695cec360c1 Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 8 Oct 2025 10:51:45 +0400 Subject: [PATCH 364/413] refactor: repay --- contracts/Controller.vy | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index a1d242bc..90bd308a 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -894,7 +894,7 @@ def _remove_from_list(_for: address): @internal def _repay_full( _for: address, - _debt: uint256, + _debt: uint256, # same as _d_debt in this case _approval: bool, _xy: uint256[2], _cb: IController.CallbackData, @@ -955,6 +955,7 @@ def _repay_partial( liquidation_discount: uint256 = self.liquidation_discounts[_for] if ns[0] > active_band or _shrink: + # Not underwater or shrink mode - can move bands new_collateral: uint256 = _cb.collateral if _callbacker == empty(address): _xy = extcall AMM.withdraw(_for, WAD) @@ -968,7 +969,8 @@ def _repay_partial( ns[1] = ns[0] + size extcall AMM.deposit_range(_for, new_collateral, ns[0], ns[1]) else: - # Underwater - cannot use callback or move bands but can avoid a bad liquidation + # Underwater without shrink - cannot use callback or move bands. + # But can avoid a bad liquidation just reducing debt amount. _xy = staticcall AMM.get_sum_xy(_for) assert _callbacker == empty(address) @@ -1037,13 +1039,11 @@ def repay( d_debt: uint256 = min(min(_wallet_d_debt, debt) + xy[0] + cb.borrowed, debt) assert d_debt > 0 # dev: no coins to repay + debt = unsafe_sub(debt, d_debt) - if d_debt >= debt: - d_debt = debt - debt = 0 + if debt == 0: self._repay_full(_for, d_debt, approval, xy, cb, callbacker) else: - debt = unsafe_sub(debt, d_debt) self._repay_partial( _for, debt, From 82ab845dc9284eb1ab52fb4f8aefab8f045afd4d Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 8 Oct 2025 15:09:55 +0400 Subject: [PATCH 365/413] feat: tokens_to_shrink --- contracts/Controller.vy | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 90bd308a..61950aea 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -1064,6 +1064,32 @@ def repay( self._save_rate() +@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) + + if ns[0] > active_band: + return 0 + + 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 + ) + + return unsafe_sub(max(new_debt, max_borrowable), max_borrowable) + + @internal @view def _health( From 221668abb18dc34fad61facc20ba0c807190ee31 Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 8 Oct 2025 15:11:44 +0400 Subject: [PATCH 366/413] refactor: vars names without underscore --- contracts/Controller.vy | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 61950aea..ba32b11c 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -904,14 +904,14 @@ def _repay_full( _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) + 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)) + 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]) ================= From fccc82534f56a5de1f7686ae5e4d43fa689286e6 Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 8 Oct 2025 15:31:26 +0400 Subject: [PATCH 367/413] fix: xy value in repay --- contracts/Controller.vy | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index ba32b11c..046d350b 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -971,7 +971,6 @@ def _repay_partial( else: # Underwater without shrink - cannot use callback or move bands. # But can avoid a bad liquidation just reducing debt amount. - _xy = staticcall AMM.get_sum_xy(_for) assert _callbacker == empty(address) if _approval: @@ -1026,7 +1025,7 @@ def repay( debt, rate_mul = self._debt(_for) self._check_loan_exists(debt) approval: bool = self._check_approval(_for) - xy: uint256[2] = empty(uint256[2]) + xy: uint256[2] = staticcall AMM.get_sum_xy(_for) cb: IController.CallbackData = empty(IController.CallbackData) if callbacker != empty(address): From 78edb3e23359f9aa50d753270b848866569da6e5 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 8 Oct 2025 14:11:25 +0200 Subject: [PATCH 368/413] Revert "fix: xy value in repay" This reverts commit fccc82534f56a5de1f7686ae5e4d43fa689286e6. --- contracts/Controller.vy | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 046d350b..ba32b11c 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -971,6 +971,7 @@ def _repay_partial( else: # Underwater without shrink - cannot use callback or move bands. # But can avoid a bad liquidation just reducing debt amount. + _xy = staticcall AMM.get_sum_xy(_for) assert _callbacker == empty(address) if _approval: @@ -1025,7 +1026,7 @@ def repay( debt, rate_mul = self._debt(_for) self._check_loan_exists(debt) approval: bool = self._check_approval(_for) - xy: uint256[2] = staticcall AMM.get_sum_xy(_for) + xy: uint256[2] = empty(uint256[2]) cb: IController.CallbackData = empty(IController.CallbackData) if callbacker != empty(address): From f2f1f1a401c735503c60e6ce5404c94ab1d735df Mon Sep 17 00:00:00 2001 From: Oleg <40476427+amfet42@users.noreply.github.com> Date: Wed, 8 Oct 2025 15:00:28 +0200 Subject: [PATCH 369/413] fix typo for callback --- contracts/zaps/PartialRepayZapCallback.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/zaps/PartialRepayZapCallback.vy b/contracts/zaps/PartialRepayZapCallback.vy index 23558ed1..1251db2f 100644 --- a/contracts/zaps/PartialRepayZapCallback.vy +++ b/contracts/zaps/PartialRepayZapCallback.vy @@ -148,7 +148,7 @@ def liquidate_partial( def execute_callback( callbacker: address, callback_sig: bytes4, - calldata: Bytes[CALLDATA_MAX_SIZE - 32 * 5 - 16], + calldata: Bytes[CALLDATA_MAX_SIZE - 32 * 6 - 16], ): response: Bytes[64] = raw_call( callbacker, From 561b0f8d70ede9fb86d5af7d317c3c9e7f74a24f Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 9 Oct 2025 13:27:36 +0400 Subject: [PATCH 370/413] fix: repay d_debt --- contracts/Controller.vy | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index ba32b11c..cddac9f7 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -933,8 +933,7 @@ def _repay_full( @internal def _repay_partial( _for: address, - _new_debt: uint256, - _d_debt: uint256, + _debt: uint256, _wallet_d_debt: uint256, _approval: bool, _xy: uint256[2], @@ -942,7 +941,7 @@ def _repay_partial( _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() assert active_band <= _max_active_band @@ -953,6 +952,8 @@ def _repay_partial( 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 @@ -960,9 +961,10 @@ def _repay_partial( 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, + new_debt, convert(unsafe_add(size, 1), uint256), _for, ) @@ -971,7 +973,6 @@ def _repay_partial( else: # Underwater without shrink - cannot use callback or move bands. # But can avoid a bad liquidation just reducing debt amount. - _xy = staticcall AMM.get_sum_xy(_for) assert _callbacker == empty(address) if _approval: @@ -981,7 +982,7 @@ def _repay_partial( 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 + assert self._health(_for, new_debt, False, liquidation_discount) > 0 if _shrink: assert _approval @@ -989,18 +990,22 @@ def _repay_partial( tkn.transfer_from(BORROWED_TOKEN, _callbacker, self, _cb.borrowed) tkn.transfer_from(BORROWED_TOKEN, msg.sender, self, _wallet_d_debt) + d_debt: uint256 = _debt - new_debt + log IController.UserState( user=_for, collateral=_xy[1], - debt=_new_debt, + debt=new_debt, n1=ns[0], n2=ns[1], liquidation_discount=liquidation_discount, ) log IController.Repay( - user=_for, collateral_decrease=0, loan_decrease=_d_debt + user=_for, collateral_decrease=0, loan_decrease=d_debt ) + return d_debt + @external def repay( @@ -1026,7 +1031,7 @@ def repay( debt, rate_mul = self._debt(_for) self._check_loan_exists(debt) approval: bool = self._check_approval(_for) - xy: uint256[2] = empty(uint256[2]) + xy: uint256[2] = staticcall AMM.get_sum_xy(_for) cb: IController.CallbackData = empty(IController.CallbackData) if callbacker != empty(address): @@ -1039,15 +1044,13 @@ def repay( d_debt: uint256 = min(min(_wallet_d_debt, debt) + xy[0] + cb.borrowed, debt) assert d_debt > 0 # dev: no coins to repay - debt = unsafe_sub(debt, d_debt) - if debt == 0: + if d_debt == debt: self._repay_full(_for, d_debt, approval, xy, cb, callbacker) else: - self._repay_partial( + d_debt = self._repay_partial( _for, debt, - d_debt, _wallet_d_debt, approval, xy, @@ -1056,6 +1059,7 @@ def repay( 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) @@ -1115,9 +1119,7 @@ def _health( ) if full: - ns0: int256 = (staticcall AMM.read_user_tick_numbers(user))[ - 0 - ] # ns[1] > ns[0] + 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) From 45428eeae1239a62878ea0033403f247569e4aed Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 9 Oct 2025 13:29:39 +0400 Subject: [PATCH 371/413] chore: fix interface for tokens_to_shrink --- contracts/interfaces/IController.vyi | 6 ++++++ contracts/lending/LendController.vy | 1 + 2 files changed, 7 insertions(+) diff --git a/contracts/interfaces/IController.vyi b/contracts/interfaces/IController.vyi index 7253158f..d6998447 100644 --- a/contracts/interfaces/IController.vyi +++ b/contracts/interfaces/IController.vyi @@ -166,6 +166,12 @@ def repay( ... +@view +@external +def tokens_to_shrink(user: address) -> uint256: + ... + + @external def approve(_spender: address, _allow: bool): ... diff --git a/contracts/lending/LendController.vy b/contracts/lending/LendController.vy index 0b357cc7..4e001cb5 100644 --- a/contracts/lending/LendController.vy +++ b/contracts/lending/LendController.vy @@ -57,6 +57,7 @@ exports: ( core.monetary_policy, core.n_loans, core.tokens_to_liquidate, + core.tokens_to_shrink, core.total_debt, core.factory, core.processed, From 0d4c0330a619ba2316192222e6e2471626ab8c9c Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 9 Oct 2025 15:23:51 +0400 Subject: [PATCH 372/413] fix: max N = 4 after shrink --- contracts/Controller.vy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index cddac9f7..97c5898e 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -949,7 +949,7 @@ def _repay_partial( 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" + 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) @@ -1082,7 +1082,7 @@ def tokens_to_shrink(user: address) -> uint256: if ns[0] > active_band: return 0 - assert ns[1] > active_band + MIN_TICKS, "Can't shrink" + 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] From 0159e9609ec0f4b5c1e3c61b82b8a674bd94db81 Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 9 Oct 2025 15:24:25 +0400 Subject: [PATCH 373/413] test: test_tokens_to_shrink_fuzz --- tests/fuzz/test_tokens_to_shrink_fuzz.py | 92 ++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 tests/fuzz/test_tokens_to_shrink_fuzz.py 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..81a5f9e9 --- /dev/null +++ b/tests/fuzz/test_tokens_to_shrink_fuzz.py @@ -0,0 +1,92 @@ +import boa +import pytest +from hypothesis import given, settings, reproduce_failure +from hypothesis import strategies as st +from tests.utils.constants import MIN_TICKS, WAD, ZERO_ADDRESS + + +@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) From b616207f1fbe3e9e557b4995d4c64335f41cbab6 Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 9 Oct 2025 15:24:41 +0400 Subject: [PATCH 374/413] test: A=100 for tests --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index aa8c1320..36eded9c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -138,7 +138,7 @@ def market_type(request): @pytest.fixture(scope="module") def amm_A(): - return 1000 + return 100 @pytest.fixture(scope="module") From 9d0ae2d276a12cc7c3163583de0539f35a7de2c5 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 13 Oct 2025 18:33:39 +0400 Subject: [PATCH 375/413] fix: CS-CRVUSD-089 --- contracts/Controller.vy | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 97c5898e..f020515e 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -984,12 +984,16 @@ def _repay_partial( # 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, self, _cb.collateral) + d_debt: uint256 = _debt - new_debt log IController.UserState( From 0b28c718faee095929e03a80048c9d36217a2f4c Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 13 Oct 2025 18:36:43 +0400 Subject: [PATCH 376/413] fix: CS-CRVUSD-090 --- contracts/Controller.vy | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index f020515e..df7c1410 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -961,7 +961,8 @@ def _repay_partial( if _callbacker == empty(address): _xy = extcall AMM.withdraw(_for, WAD) new_collateral = _xy[1] - new_debt -=_xy[0] + new_debt -=_xy[0] + ns[0] = self._calculate_debt_n1( new_collateral, new_debt, From 8f4aefb70f14b6de9b04a9ede98471647f09a055 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 13 Oct 2025 18:57:14 +0400 Subject: [PATCH 377/413] fix: CS-CRVUSD-091 --- contracts/Controller.vy | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index df7c1410..eba9b197 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -944,6 +944,8 @@ def _repay_partial( ) -> uint256: # slippage-like check to prevent dos on repay (grief attack) active_band: int256 = staticcall AMM.active_band_with_skip() + if _callbacker != empty(address): + active_band = _cb.active_band assert active_band <= _max_active_band ns: int256[2] = staticcall AMM.read_user_tick_numbers(_for) From 6410d5bec9641dad3006f60d125500f609d9c838 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 14 Oct 2025 12:21:11 +0400 Subject: [PATCH 378/413] fix: CS-CRVUSD-096 --- contracts/Controller.vy | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index eba9b197..f234f3f1 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -944,8 +944,10 @@ def _repay_partial( ) -> 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) @@ -1001,14 +1003,16 @@ def _repay_partial( log IController.UserState( user=_for, - collateral=_xy[1], + collateral=new_collateral, debt=new_debt, n1=ns[0], n2=ns[1], liquidation_discount=liquidation_discount, ) log IController.Repay( - user=_for, collateral_decrease=0, loan_decrease=d_debt + user=_for, + collateral_decrease=unsafe_sub(max(_xy[1], new_collateral), new_collateral), + loan_decrease=d_debt, ) return d_debt From e7af5fae4a3b337e2dbd3f24346d79270e3ab7b2 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 14 Oct 2025 15:56:00 +0400 Subject: [PATCH 379/413] fix: CS-CRVUSD-098 --- contracts/lending/LendControllerView.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/lending/LendControllerView.vy b/contracts/lending/LendControllerView.vy index b7a7af55..0d32c51b 100644 --- a/contracts/lending/LendControllerView.vy +++ b/contracts/lending/LendControllerView.vy @@ -96,7 +96,7 @@ def max_borrowable( # 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._borrowed_balance() + current_debt, cap) + cap = min(self._borrowed_balance(), cap) + current_debt return core._max_borrowable( collateral, From f80dce2d43a08184646d9cc7aee6f507ea1f86e1 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 14 Oct 2025 18:23:00 +0400 Subject: [PATCH 380/413] fix: CS-CRVUSD-099 --- contracts/Controller.vy | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index f234f3f1..23f20d3b 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -1487,7 +1487,6 @@ def _collect_fees(admin_fee: uint256) -> uint256: # Borrowing-based fees rate_mul: uint256 = staticcall AMM.get_rate_mul() loan: IController.Loan = self._update_total_debt(0, rate_mul, False) - self._save_rate() # Cumulative amount which would have been repaid if all the debt was repaid now to_be_repaid: uint256 = loan.initial_debt + self.repaid @@ -1499,9 +1498,11 @@ def _collect_fees(admin_fee: uint256) -> uint256: fees: uint256 = unsafe_sub(to_be_repaid, processed) * admin_fee // WAD tkn.transfer(BORROWED_TOKEN, _to, fees) log IController.CollectFees(amount=fees, new_supply=loan.initial_debt) + self._save_rate() return fees else: log IController.CollectFees(amount=0, new_supply=loan.initial_debt) + self._save_rate() return 0 From 299b7215b9c021d93efd55547bc09deb0e44a071 Mon Sep 17 00:00:00 2001 From: macket Date: Tue, 14 Oct 2025 18:24:53 +0400 Subject: [PATCH 381/413] fix: CS-CRVUSD-100 --- contracts/Controller.vy | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 23f20d3b..43e0f8fd 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -663,7 +663,6 @@ def _create_loan( extcall AMM.deposit_range(_for, total_collateral, n1, n2) self.processed += debt - self._save_rate() log IController.UserState( user=_for, @@ -682,6 +681,8 @@ def _create_loan( if callbacker == empty(address): tkn.transfer(BORROWED_TOKEN, _for, debt) + self._save_rate() + return debt From 59db5d99ee65f0d3e2498ef434a0e5910c1c0d4f Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 15 Oct 2025 11:58:25 +0400 Subject: [PATCH 382/413] fix: new_collateral bug in _repay_partial --- contracts/Controller.vy | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 43e0f8fd..de49b829 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -948,7 +948,7 @@ def _repay_partial( new_collateral: uint256 = _xy[1] if _callbacker != empty(address): active_band = _cb.active_band - new_collateral = cb.collateral + new_collateral = _cb.collateral assert active_band <= _max_active_band ns: int256[2] = staticcall AMM.read_user_tick_numbers(_for) @@ -962,7 +962,6 @@ def _repay_partial( if ns[0] > active_band or _shrink: # Not underwater or shrink mode - can move bands - new_collateral: uint256 = _cb.collateral if _callbacker == empty(address): _xy = extcall AMM.withdraw(_for, WAD) new_collateral = _xy[1] From adfb509ca38a77198739f0b78cc13e44581f60d0 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 15 Oct 2025 18:44:57 +0200 Subject: [PATCH 383/413] refactor(Vault): merge deposited and withdrawn --- contracts/interfaces/IVault.vyi | 6 +----- contracts/lending/LendController.vy | 7 +++---- contracts/lending/Vault.vy | 11 +++++----- tests/e2e/test_donate_dos.py | 4 ++-- .../test_borrowed_balance_lc.py | 21 ++++++++----------- tests/unitary/lending/vault/test_deposit_v.py | 8 +++---- tests/unitary/lending/vault/test_mint_v.py | 8 +++---- tests/unitary/lending/vault/test_redeem_v.py | 16 +++++++------- .../unitary/lending/vault/test_withdraw_v.py | 16 +++++++------- 9 files changed, 44 insertions(+), 53 deletions(-) diff --git a/contracts/interfaces/IVault.vyi b/contracts/interfaces/IVault.vyi index 93645749..177a1e81 100644 --- a/contracts/interfaces/IVault.vyi +++ b/contracts/interfaces/IVault.vyi @@ -11,11 +11,7 @@ def borrowed_token() -> IERC20: ... @view -def deposited() -> uint256: - ... - -@view -def withdrawn() -> uint256: +def asset_balance() -> uint256: ... @view diff --git a/contracts/lending/LendController.vy b/contracts/lending/LendController.vy index 4e001cb5..7f8d3744 100644 --- a/contracts/lending/LendController.vy +++ b/contracts/lending/LendController.vy @@ -168,15 +168,14 @@ def borrowed_balance() -> uint256: @dev Used by the vault for its accounting logic. """ # TODO rename to `borrowed_token_balance` for clarity? - # Start from the vault’s net funding of borrowed tokens (deposited − withdrawn), + # Start from the vault’s erc20 balance (ignoring any tokens sent directly to the vault), # subtract the portion we actually lent out that hasn’t been repaid yet (lent − repaid), # and subtract what the admin already skimmed as fees; what’s left is the controller’s idle cash. - # (VAULT.deposited() - VAULT.withdrawn()) - (lent - repaid) - self.collected + # VAULT.asset_balance() - (lent - repaid) - self.collected # The terms are rearranged to avoid underflows in intermediate steps. return ( - staticcall VAULT.deposited() + staticcall VAULT.asset_balance() + core.repaid - - staticcall VAULT.withdrawn() - self.lent - self.collected ) diff --git a/contracts/lending/Vault.vy b/contracts/lending/Vault.vy index 2f00811a..53cf8f59 100644 --- a/contracts/lending/Vault.vy +++ b/contracts/lending/Vault.vy @@ -38,8 +38,7 @@ factory: public(IFactory) maxSupply: public(uint256) -deposited: public(uint256) # cumulative amount of assets ever deposited -withdrawn: public(uint256) # cumulative amount of assets ever withdrawn +asset_balance: public(uint256) # ERC20 publics @@ -268,7 +267,7 @@ def deposit(assets: uint256, receiver: address = msg.sender) -> uint256: assert total_assets + assets <= self.maxSupply, "Supply limit" to_mint: uint256 = self._convert_to_shares(assets, True, total_assets) assert extcall self.borrowed_token.transferFrom(msg.sender, controller.address, assets, default_return_value=True) - self.deposited += assets + self.asset_balance += assets self._mint(receiver, to_mint) extcall controller.save_rate() log IERC4626.Deposit(sender=msg.sender, owner=receiver, assets=assets, shares=to_mint) @@ -313,7 +312,7 @@ def mint(shares: uint256, receiver: address = msg.sender) -> uint256: assert total_assets + assets >= MIN_ASSETS, "Need more assets" assert total_assets + assets <= self.maxSupply, "Supply limit" assert extcall self.borrowed_token.transferFrom(msg.sender, controller.address, assets, default_return_value=True) - self.deposited += assets + self.asset_balance += assets self._mint(receiver, shares) extcall controller.save_rate() log IERC4626.Deposit(sender=msg.sender, owner=receiver, assets=assets, shares=shares) @@ -363,7 +362,7 @@ def withdraw(assets: uint256, receiver: address = msg.sender, owner: address = m controller: IController = self.controller self._burn(owner, shares) assert extcall self.borrowed_token.transferFrom(controller.address, receiver, assets, default_return_value=True) - self.withdrawn += assets + self.asset_balance -= assets extcall controller.save_rate() log IERC4626.Withdraw(sender=msg.sender, receiver=receiver, owner=owner, assets=assets, shares=shares) return shares @@ -423,7 +422,7 @@ def redeem(shares: uint256, receiver: address = msg.sender, owner: address = msg self._burn(owner, shares) controller: IController = self.controller assert extcall self.borrowed_token.transferFrom(controller.address, receiver, assets_to_redeem, default_return_value=True) - self.withdrawn += assets_to_redeem + self.asset_balance -= assets_to_redeem extcall controller.save_rate() log IERC4626.Withdraw(sender=msg.sender, receiver=receiver, owner=owner, assets=assets_to_redeem, shares=shares) return assets_to_redeem diff --git a/tests/e2e/test_donate_dos.py b/tests/e2e/test_donate_dos.py index 8f333f61..7d65b9f9 100644 --- a/tests/e2e/test_donate_dos.py +++ b/tests/e2e/test_donate_dos.py @@ -17,7 +17,7 @@ def borrow_cap(): def test_reverts_when_lent_exceeds_deposited_under_donation( controller, vault, borrowed_token, collateral_token, admin ): - deposited = vault.deposited() + initial_balance = vault.asset_balance() COLLATERAL = 10**30 N_BANDS = 5 @@ -29,6 +29,6 @@ def test_reverts_when_lent_exceeds_deposited_under_donation( boa.deal(borrowed_token, boa.env.eoa, DONATION) borrowed_token.transfer(controller, DONATION) - controller.create_loan(COLLATERAL, deposited + DONATION, N_BANDS) + controller.create_loan(COLLATERAL, initial_balance + DONATION, N_BANDS) vault.totalAssets() diff --git a/tests/unitary/lending/lend_controller/test_borrowed_balance_lc.py b/tests/unitary/lending/lend_controller/test_borrowed_balance_lc.py index 0b1ad85f..379f1fb5 100644 --- a/tests/unitary/lending/lend_controller/test_borrowed_balance_lc.py +++ b/tests/unitary/lending/lend_controller/test_borrowed_balance_lc.py @@ -15,8 +15,7 @@ def snapshot(controller, vault): "repaid": controller.repaid(), "collected": controller.collected(), "processed": controller.processed(), - "deposited": vault.deposited(), - "withdrawn": vault.withdrawn(), + "asset_balance": vault.asset_balance(), } @@ -34,8 +33,8 @@ def test_increases_after_deposit(controller, vault, borrowed_token): after = snapshot(controller, vault) assert after["borrowed"] == before["borrowed"] + DEPOSIT - assert after["deposited"] == before["deposited"] + DEBT - expect_same(before, after, "lent", "repaid", "collected", "withdrawn", "processed") + assert after["asset_balance"] == before["asset_balance"] + DEBT + expect_same(before, after, "lent", "repaid", "collected", "processed") def test_decreases_after_withdraw( @@ -53,8 +52,8 @@ def test_decreases_after_withdraw( after = snapshot(controller, vault) assert after["borrowed"] == before["borrowed"] - DEPOSIT - assert after["withdrawn"] == before["withdrawn"] + DEBT - expect_same(before, after, "lent", "repaid", "collected", "deposited", "processed") + 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): @@ -68,7 +67,7 @@ def test_decreases_after_create(controller, vault, collateral_token): assert after["borrowed"] == before["borrowed"] - DEBT assert after["lent"] == before["lent"] + DEBT assert after["processed"] == before["processed"] + DEBT - expect_same(before, after, "repaid", "collected", "deposited", "withdrawn") + expect_same(before, after, "repaid", "collected", "asset_balance") def test_decreases_after_borrow_more(controller, vault, collateral_token): @@ -83,7 +82,7 @@ def test_decreases_after_borrow_more(controller, vault, collateral_token): assert after["borrowed"] == before["borrowed"] - DEBT assert after["lent"] == before["lent"] + DEBT assert after["processed"] == before["processed"] + DEBT - expect_same(before, after, "repaid", "collected", "deposited", "withdrawn") + expect_same(before, after, "repaid", "collected", "asset_balance") def test_increases_after_repay(controller, vault, collateral_token, borrowed_token): @@ -98,9 +97,7 @@ def test_increases_after_repay(controller, vault, collateral_token, borrowed_tok assert after["borrowed"] == before["borrowed"] + DEBT assert after["repaid"] == before["repaid"] + DEBT - expect_same( - before, after, "lent", "collected", "deposited", "withdrawn", "processed" - ) + expect_same(before, after, "lent", "collected", "asset_balance", "processed") def test_collect_fees_reduces_balance( @@ -133,4 +130,4 @@ def test_collect_fees_reduces_balance( 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", "deposited", "withdrawn") + expect_same(before, after, "lent", "repaid", "asset_balance") diff --git a/tests/unitary/lending/vault/test_deposit_v.py b/tests/unitary/lending/vault/test_deposit_v.py index f3e3d479..b67cebb0 100644 --- a/tests/unitary/lending/vault/test_deposit_v.py +++ b/tests/unitary/lending/vault/test_deposit_v.py @@ -14,7 +14,7 @@ def seed_liquidity(): 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_deposited = vault.deposited() + initial_balance = vault.asset_balance() initial_total_supply = vault.totalSupply() initial_controller_balance = borrowed_token.balanceOf(controller.address) initial_amm_rate = amm.rate() @@ -42,7 +42,7 @@ def test_deposit_basic(vault, controller, amm, monetary_policy, borrowed_token): # Check balances assert vault.balanceOf(boa.env.eoa) == initial_sender_balance + shares - assert vault.deposited() == initial_deposited + assets + assert vault.asset_balance() == initial_balance + assets assert vault.totalSupply() == initial_total_supply + shares assert ( borrowed_token.balanceOf(controller.address) @@ -69,7 +69,7 @@ def test_deposit_with_receiver(vault, controller, amm, monetary_policy, borrowed initial_sender_balance = vault.balanceOf(boa.env.eoa) initial_receiver_balance = vault.balanceOf(receiver) - initial_deposited = vault.deposited() + initial_balance = vault.asset_balance() initial_total_supply = vault.totalSupply() initial_controller_balance = borrowed_token.balanceOf(controller.address) initial_amm_rate = amm.rate() @@ -102,7 +102,7 @@ def test_deposit_with_receiver(vault, controller, amm, monetary_policy, borrowed assert ( vault.balanceOf(receiver) == initial_receiver_balance + shares ) # Receiver gets shares - assert vault.deposited() == initial_deposited + assets + assert vault.asset_balance() == initial_balance + assets assert vault.totalSupply() == initial_total_supply + shares assert ( borrowed_token.balanceOf(controller.address) diff --git a/tests/unitary/lending/vault/test_mint_v.py b/tests/unitary/lending/vault/test_mint_v.py index 30393988..02c42853 100644 --- a/tests/unitary/lending/vault/test_mint_v.py +++ b/tests/unitary/lending/vault/test_mint_v.py @@ -14,7 +14,7 @@ def seed_liquidity(): 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_deposited = vault.deposited() + initial_balance = vault.asset_balance() initial_total_supply = vault.totalSupply() initial_controller_balance = borrowed_token.balanceOf(controller.address) initial_amm_rate = amm.rate() @@ -42,7 +42,7 @@ def test_mint_basic(vault, controller, amm, monetary_policy, borrowed_token): # Check balances assert vault.balanceOf(boa.env.eoa) == initial_sender_balance + shares - assert vault.deposited() == initial_deposited + assets + assert vault.asset_balance() == initial_balance + assets assert vault.totalSupply() == initial_total_supply + shares assert ( borrowed_token.balanceOf(controller.address) @@ -69,7 +69,7 @@ def test_mint_with_receiver(vault, controller, amm, monetary_policy, borrowed_to initial_sender_balance = vault.balanceOf(boa.env.eoa) initial_receiver_balance = vault.balanceOf(receiver) - initial_deposited = vault.deposited() + initial_balance = vault.asset_balance() initial_total_supply = vault.totalSupply() initial_controller_balance = borrowed_token.balanceOf(controller.address) initial_amm_rate = amm.rate() @@ -102,7 +102,7 @@ def test_mint_with_receiver(vault, controller, amm, monetary_policy, borrowed_to assert ( vault.balanceOf(receiver) == initial_receiver_balance + shares ) # Receiver gets shares - assert vault.deposited() == initial_deposited + assets + assert vault.asset_balance() == initial_balance + assets assert vault.totalSupply() == initial_total_supply + shares assert ( borrowed_token.balanceOf(controller.address) diff --git a/tests/unitary/lending/vault/test_redeem_v.py b/tests/unitary/lending/vault/test_redeem_v.py index ad6bc755..c68de063 100644 --- a/tests/unitary/lending/vault/test_redeem_v.py +++ b/tests/unitary/lending/vault/test_redeem_v.py @@ -11,7 +11,7 @@ def test_redeem_basic( initial_sender_balance = vault.balanceOf(boa.env.eoa) initial_total_supply = vault.totalSupply() - initial_withdrawn = vault.withdrawn() + initial_balance = vault.asset_balance() initial_controller_balance = borrowed_token.balanceOf(controller.address) initial_sender_token_balance = borrowed_token.balanceOf(boa.env.eoa) @@ -37,7 +37,7 @@ def test_redeem_basic( # 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.withdrawn() == initial_withdrawn + assets_redeemed + assert vault.asset_balance() == initial_balance - assets_redeemed assert ( borrowed_token.balanceOf(controller) == initial_controller_balance - assets_redeemed @@ -73,7 +73,7 @@ def test_redeem_with_receiver( initial_sender_balance = vault.balanceOf(boa.env.eoa) initial_receiver_balance = vault.balanceOf(receiver) initial_total_supply = vault.totalSupply() - initial_withdrawn = vault.withdrawn() + initial_balance = vault.asset_balance() 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) @@ -105,7 +105,7 @@ def test_redeem_with_receiver( vault.balanceOf(receiver) == initial_receiver_balance ) # Receiver gets no shares assert vault.totalSupply() == initial_total_supply - shares_to_redeem - assert vault.withdrawn() == initial_withdrawn + assets_redeemed + assert vault.asset_balance() == initial_balance - assets_redeemed assert ( borrowed_token.balanceOf(controller) == initial_controller_balance - assets_redeemed @@ -146,7 +146,7 @@ def test_redeem_with_owner( initial_owner_balance = vault.balanceOf(owner) initial_caller_balance = vault.balanceOf(caller) initial_total_supply = vault.totalSupply() - initial_withdrawn = vault.withdrawn() + initial_balance = vault.asset_balance() initial_controller_balance = borrowed_token.balanceOf(controller.address) initial_caller_token_balance = borrowed_token.balanceOf(caller) @@ -179,7 +179,7 @@ def test_redeem_with_owner( ) # 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.withdrawn() == initial_withdrawn + assets_redeemed + assert vault.asset_balance() == initial_balance + assets_redeemed assert ( borrowed_token.balanceOf(controller) == initial_controller_balance - assets_redeemed @@ -219,7 +219,7 @@ def test_redeem_with_owner_and_receiver( initial_caller_balance = vault.balanceOf(caller) initial_receiver_balance = vault.balanceOf(receiver) initial_total_supply = vault.totalSupply() - initial_withdrawn = vault.withdrawn() + initial_balance = vault.asset_balance() initial_controller_balance = borrowed_token.balanceOf(controller.address) initial_caller_token_balance = borrowed_token.balanceOf(caller) initial_receiver_token_balance = borrowed_token.balanceOf(receiver) @@ -256,7 +256,7 @@ def test_redeem_with_owner_and_receiver( vault.balanceOf(receiver) == initial_receiver_balance ) # Receiver gets no shares assert vault.totalSupply() == initial_total_supply - shares_to_redeem - assert vault.withdrawn() == initial_withdrawn + assets_redeemed + assert vault.asset_balance() == initial_balance - assets_redeemed assert ( borrowed_token.balanceOf(controller) == initial_controller_balance - assets_redeemed diff --git a/tests/unitary/lending/vault/test_withdraw_v.py b/tests/unitary/lending/vault/test_withdraw_v.py index 702f4d2d..ae1dadd0 100644 --- a/tests/unitary/lending/vault/test_withdraw_v.py +++ b/tests/unitary/lending/vault/test_withdraw_v.py @@ -11,7 +11,7 @@ def test_withdraw_basic( initial_sender_balance = vault.balanceOf(boa.env.eoa) initial_total_supply = vault.totalSupply() - initial_withdrawn = vault.withdrawn() + initial_balance = vault.asset_balance() initial_controller_balance = borrowed_token.balanceOf(controller.address) initial_sender_token_balance = borrowed_token.balanceOf(boa.env.eoa) @@ -37,7 +37,7 @@ def test_withdraw_basic( # Check balances assert vault.balanceOf(boa.env.eoa) == initial_sender_balance - shares assert vault.totalSupply() == initial_total_supply - shares - assert vault.withdrawn() == initial_withdrawn + assets_to_withdraw + assert vault.asset_balance() == initial_balance - assets_to_withdraw assert ( borrowed_token.balanceOf(controller) == initial_controller_balance - assets_to_withdraw @@ -73,7 +73,7 @@ def test_withdraw_with_receiver( initial_sender_balance = vault.balanceOf(boa.env.eoa) initial_receiver_balance = vault.balanceOf(receiver) initial_total_supply = vault.totalSupply() - initial_withdrawn = vault.withdrawn() + initial_balance = vault.asset_balance() 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) @@ -105,7 +105,7 @@ def test_withdraw_with_receiver( vault.balanceOf(receiver) == initial_receiver_balance ) # Receiver gets no shares assert vault.totalSupply() == initial_total_supply - shares - assert vault.withdrawn() == initial_withdrawn + assets_to_withdraw + assert vault.asset_balance() == initial_balance - assets_to_withdraw assert ( borrowed_token.balanceOf(controller) == initial_controller_balance - assets_to_withdraw @@ -146,7 +146,7 @@ def test_withdraw_with_owner( initial_owner_balance = vault.balanceOf(owner) initial_caller_balance = vault.balanceOf(caller) initial_total_supply = vault.totalSupply() - initial_withdrawn = vault.withdrawn() + initial_balance = vault.asset_balance() initial_controller_balance = borrowed_token.balanceOf(controller.address) initial_caller_token_balance = borrowed_token.balanceOf(caller) @@ -179,7 +179,7 @@ def test_withdraw_with_owner( ) # Owner's shares burned assert vault.balanceOf(caller) == initial_caller_balance # Caller gets no shares assert vault.totalSupply() == initial_total_supply - shares - assert vault.withdrawn() == initial_withdrawn + assets_to_withdraw + assert vault.asset_balance() == initial_balance - assets_to_withdraw assert ( borrowed_token.balanceOf(controller) == initial_controller_balance - assets_to_withdraw @@ -219,7 +219,7 @@ def test_withdraw_with_owner_and_receiver( initial_caller_balance = vault.balanceOf(caller) initial_receiver_balance = vault.balanceOf(receiver) initial_total_supply = vault.totalSupply() - initial_withdrawn = vault.withdrawn() + initial_balance = vault.asset_balance() initial_controller_balance = borrowed_token.balanceOf(controller.address) initial_owner_token_balance = borrowed_token.balanceOf(owner) initial_caller_token_balance = borrowed_token.balanceOf(caller) @@ -257,7 +257,7 @@ def test_withdraw_with_owner_and_receiver( vault.balanceOf(receiver) == initial_receiver_balance ) # Receiver gets no shares assert vault.totalSupply() == initial_total_supply - shares - assert vault.withdrawn() == initial_withdrawn + assets_to_withdraw + assert vault.asset_balance() == initial_balance - assets_to_withdraw assert ( borrowed_token.balanceOf(controller) == initial_controller_balance - assets_to_withdraw From a8c04a5bbec4f1c237f64d38423138e285c5c5f0 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 15 Oct 2025 18:51:38 +0200 Subject: [PATCH 384/413] style: format --- tests/fuzz/test_tokens_to_shrink_fuzz.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/fuzz/test_tokens_to_shrink_fuzz.py b/tests/fuzz/test_tokens_to_shrink_fuzz.py index 81a5f9e9..7db6a5b0 100644 --- a/tests/fuzz/test_tokens_to_shrink_fuzz.py +++ b/tests/fuzz/test_tokens_to_shrink_fuzz.py @@ -1,6 +1,6 @@ import boa import pytest -from hypothesis import given, settings, reproduce_failure +from hypothesis import given, settings from hypothesis import strategies as st from tests.utils.constants import MIN_TICKS, WAD, ZERO_ADDRESS @@ -74,12 +74,18 @@ def test_tokens_to_shrink( 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) + 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 + 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 @@ -88,5 +94,7 @@ def test_tokens_to_shrink( _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[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) From ebae79c89e45feff77895af9569a1fcc5dad950d Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 15 Oct 2025 18:58:49 +0200 Subject: [PATCH 385/413] test: fix incorrect balance accounting --- tests/unitary/lending/vault/test_redeem_v.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unitary/lending/vault/test_redeem_v.py b/tests/unitary/lending/vault/test_redeem_v.py index c68de063..ea165e6b 100644 --- a/tests/unitary/lending/vault/test_redeem_v.py +++ b/tests/unitary/lending/vault/test_redeem_v.py @@ -179,7 +179,7 @@ def test_redeem_with_owner( ) # 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.asset_balance() == initial_balance + assets_redeemed + assert vault.asset_balance() == initial_balance - assets_redeemed assert ( borrowed_token.balanceOf(controller) == initial_controller_balance - assets_redeemed From 39cd67da11e148c65a208810f78d817ff50fbf27 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 15 Oct 2025 19:17:15 +0200 Subject: [PATCH 386/413] chore: reorg files --- tests/{ => fuzz}/stateful/test_controller_stateful.py | 0 tests/{ => fuzz}/stateful/test_lend_controller_stateful.py | 0 tests/fuzz/strategies.py | 0 tests/fuzz/test_tokens_to_shrink_fuzz.py | 1 + 4 files changed, 1 insertion(+) rename tests/{ => fuzz}/stateful/test_controller_stateful.py (100%) rename tests/{ => fuzz}/stateful/test_lend_controller_stateful.py (100%) create mode 100644 tests/fuzz/strategies.py diff --git a/tests/stateful/test_controller_stateful.py b/tests/fuzz/stateful/test_controller_stateful.py similarity index 100% rename from tests/stateful/test_controller_stateful.py rename to tests/fuzz/stateful/test_controller_stateful.py diff --git a/tests/stateful/test_lend_controller_stateful.py b/tests/fuzz/stateful/test_lend_controller_stateful.py similarity index 100% rename from tests/stateful/test_lend_controller_stateful.py rename to tests/fuzz/stateful/test_lend_controller_stateful.py diff --git a/tests/fuzz/strategies.py b/tests/fuzz/strategies.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/fuzz/test_tokens_to_shrink_fuzz.py b/tests/fuzz/test_tokens_to_shrink_fuzz.py index 7db6a5b0..9cf74056 100644 --- a/tests/fuzz/test_tokens_to_shrink_fuzz.py +++ b/tests/fuzz/test_tokens_to_shrink_fuzz.py @@ -5,6 +5,7 @@ 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. From eb2ac62a55e89e6c03c7a126bcae95794c4d1ca2 Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 16 Oct 2025 21:11:42 +0400 Subject: [PATCH 387/413] test: update test_default_behavior_no_callback --- .../controller/test_internal_repay_full.py | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/tests/unitary/controller/test_internal_repay_full.py b/tests/unitary/controller/test_internal_repay_full.py index 1213b5bc..8a7b1b7d 100644 --- a/tests/unitary/controller/test_internal_repay_full.py +++ b/tests/unitary/controller/test_internal_repay_full.py @@ -68,38 +68,42 @@ def test_default_behavior_no_callback( borrowed_token_after = snapshot(borrowed_token, borrower) collateral_token_after = snapshot(collateral_token, borrower) - repaid_amount = ( + borrowed_to_controller = ( borrowed_token_after["controller"] - borrowed_token_before["controller"] ) - borrower_decrease = ( + borrowed_from_borrower = ( borrowed_token_after["borrower"] - borrowed_token_before["borrower"] ) - collateral_released = ( + collateral_to_borrower = ( collateral_token_after["borrower"] - collateral_token_before["borrower"] ) collateral_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] + # 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 + assert len(state_logs) == 1 assert state_logs[0].user == borrower - assert state_logs[0].debt == 0 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 == repaid_amount - assert repay_logs[0].collateral_decrease == collateral_released + assert repay_logs[0].loan_decrease == borrowed_to_controller + assert repay_logs[0].collateral_decrease == collateral_to_borrower - assert repaid_amount == debt - assert borrower_decrease == -debt + assert borrowed_to_controller == debt + assert borrowed_from_borrower == -debt assert borrowed_token_after["amm"] == borrowed_token_before["amm"] assert borrowed_token_after["callback"] == borrowed_token_before["callback"] - assert collateral_released == -collateral_from_amm + 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"] - - assert collateral_released == COLLATERAL - assert borrowed_token.balanceOf(controller) == borrowed_token_after["controller"] - assert ( - collateral_token.balanceOf(controller) == collateral_token_after["controller"] - ) From 53f779146e8c56b486b14e9b6042c01e195b3925 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 16 Oct 2025 21:48:44 +0200 Subject: [PATCH 388/413] test: improve stateful testing --- tests/fuzz/__init__.py | 0 tests/fuzz/stateful/__init__.py | 0 .../fuzz/stateful/test_controller_stateful.py | 104 +------------- .../stateful/test_lend_controller_stateful.py | 87 ++++++++++- tests/fuzz/strategies.py | 136 ++++++++++++++++++ tests/utils/constants.py | 3 + 6 files changed, 230 insertions(+), 100 deletions(-) create mode 100644 tests/fuzz/__init__.py create mode 100644 tests/fuzz/stateful/__init__.py diff --git a/tests/fuzz/__init__.py b/tests/fuzz/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/fuzz/stateful/__init__.py b/tests/fuzz/stateful/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/fuzz/stateful/test_controller_stateful.py b/tests/fuzz/stateful/test_controller_stateful.py index b6573df8..42ba5d55 100644 --- a/tests/fuzz/stateful/test_controller_stateful.py +++ b/tests/fuzz/stateful/test_controller_stateful.py @@ -1,9 +1,8 @@ from decimal import Decimal -from hypothesis import note, assume +from hypothesis import event, note, assume from hypothesis.strategies import ( composite, integers, - builds, decimals, SearchStrategy, data, @@ -19,53 +18,12 @@ 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_TICKS, MAX_UINT256, WAD, - MIN_A, - MAX_A, - MIN_FEE, - MAX_FEE, - MAX_LOAN_DISCOUNT, - MIN_LIQUIDATION_DISCOUNT, - MIN_TICKS, ) -from tests.utils.protocols import Llamalend - - -# Debt ceiling has no explicit on-chain limit; choose a realistic test bound -DEBT_CEILING_MAX = 10**8 * 10**18 - -# ---------------- simple parameter strategies ---------------- -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 def token_amounts( @@ -169,66 +127,14 @@ def loan_increments_for_borrow_more( # ---------------- mint market via Protocol ---------------- -@composite -def mint_markets( - draw, - As=As, - amm_fees=amm_fees, - discounts=discounts(), - debt_ceilings=debt_ceilings, - initial_prices=prices, -): - """Creates a Protocol and a mint market with fuzzed parameters. - Custom strategies can be passed to override defaults. - Returns a dict with proto, controller, amm, collateral_token, and params. - """ - _A = draw(As) - _fee = draw(amm_fees) - _loan_discount, _liq_discount = draw(discounts) - _dc = draw(debt_ceilings) - _price = draw(initial_prices) - - # Deploy protocol with initial oracle price - proto = Protocol(initial_price=_price) - - # Fresh collateral token (via strategy built from decimals if not provided) - _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 # ------------ controller interaction params ------------- -# TODO eventually fix this in SC -ticks = integers(min_value=MIN_TICKS + 1, max_value=MAX_TICKS) - - -# ===================== -# Stateful skeleton (to be expanded) -# ===================== - class ControllerStateful(RuleBasedStateMachine): @initialize(market=mint_markets()) - def initialize_protocol(self, market): + def _initialize(self, market): # Unpack market artifacts self.controller = market["controller"] self.amm = market["amm"] @@ -348,6 +254,10 @@ def remove_collateral_rule(self, data): 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( diff --git a/tests/fuzz/stateful/test_lend_controller_stateful.py b/tests/fuzz/stateful/test_lend_controller_stateful.py index 8ebb8d99..68e3d1a2 100644 --- a/tests/fuzz/stateful/test_lend_controller_stateful.py +++ b/tests/fuzz/stateful/test_lend_controller_stateful.py @@ -1,3 +1,84 @@ -# Just noting down things I want to test -# 1. This test inherits from test_controller_stateful.py -# 2. It should add some invariants to track borrowed balance + lent + collected +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 index e69de29b..ca44e50f 100644 --- a/tests/fuzz/strategies.py +++ 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/utils/constants.py b/tests/utils/constants.py index 44f51323..ff8781be 100644 --- a/tests/utils/constants.py +++ b/tests/utils/constants.py @@ -3,6 +3,7 @@ CONSTANTS_DEPLOYER, CONTROLLER_DEPLOYER, LENDING_FACTORY_DEPLOYER, + VAULT_DEPLOYER, ) from typing import Final @@ -27,3 +28,5 @@ 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 From 9bff58795f62a2c291e7911da45c5fbe6a4361c0 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 20 Oct 2025 14:20:45 +0200 Subject: [PATCH 389/413] refactor: reduce code duplication --- contracts/AMM.vy | 45 ++++++++++----------------------------------- 1 file changed, 10 insertions(+), 35 deletions(-) diff --git a/contracts/AMM.vy b/contracts/AMM.vy index 307ff306..54612144 100644 --- a/contracts/AMM.vy +++ b/contracts/AMM.vy @@ -39,21 +39,23 @@ implements: IAMM from contracts.interfaces import IPriceOracle from contracts.interfaces import ILMGauge - from contracts.interfaces import IERC20 +from snekmate.utils import math + from contracts import constants as c from contracts.lib import token_lib as tkn # TODO common constants -MAX_TICKS: constant(int256) = 50 +# 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 -# https://github.com/vyperlang/vyper/issues/4723 -DEAD_SHARES: constant(uint256) = c.DEAD_SHARES BORROWED_TOKEN: immutable(IERC20) # x @@ -351,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 From f5dbc688ed8b1f5ffad340ce7154b6d49210ea8b Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 20 Oct 2025 14:21:02 +0200 Subject: [PATCH 390/413] chore: remove deprecation warnings --- contracts/AMM.vy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/AMM.vy b/contracts/AMM.vy index 54612144..128128d2 100644 --- a/contracts/AMM.vy +++ b/contracts/AMM.vy @@ -543,7 +543,7 @@ def _read_user_ticks(user: address, ns: int256[2]) -> DynArray[uint256, MAX_TICK ticks.append(tick & (2**128 - 1)) if len(ticks) == size: break - ticks.append(shift(tick, -128)) + ticks.append(tick >> 128) return ticks @@ -610,7 +610,7 @@ def save_user_shares(user: address, user_shares: DynArray[uint256, MAX_TICKS_UIN 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 From 9c388cbf93485301d9ccb1823cf27abaf146be10 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 20 Oct 2025 14:26:58 +0200 Subject: [PATCH 391/413] perf: remove redundant check leftover from when the vault would be initialized using `create_copy_of` --- contracts/lending/Vault.vy | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/lending/Vault.vy b/contracts/lending/Vault.vy index 53cf8f59..c2ce3a33 100644 --- a/contracts/lending/Vault.vy +++ b/contracts/lending/Vault.vy @@ -73,8 +73,6 @@ def initialize( @param borrowed_token Token which is being borrowed @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 From d16203a62fb51ff5fcfed78ba7d4a2dc1c190d42 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 20 Oct 2025 15:04:31 +0200 Subject: [PATCH 392/413] chore: remove redundant branching --- contracts/lending/Vault.vy | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/lending/Vault.vy b/contracts/lending/Vault.vy index c2ce3a33..d085e4d9 100644 --- a/contracts/lending/Vault.vy +++ b/contracts/lending/Vault.vy @@ -122,8 +122,7 @@ def lend_apr() -> uint256: debt: uint256 = staticcall self.controller.total_debt() if debt == 0: return 0 - else: - return staticcall self.amm.rate() * (365 * 86400) * debt // self._total_assets() + return staticcall self.amm.rate() * (365 * 86400) * debt // self._total_assets() @external From fdfaeaa4aeb605dc7223492b5da2503bbf101adb Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 20 Oct 2025 15:33:40 +0200 Subject: [PATCH 393/413] chore: remove redundant branch --- contracts/Controller.vy | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index de49b829..5fed6b80 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -1500,10 +1500,9 @@ def _collect_fees(admin_fee: uint256) -> uint256: log IController.CollectFees(amount=fees, new_supply=loan.initial_debt) self._save_rate() return fees - else: - log IController.CollectFees(amount=0, new_supply=loan.initial_debt) - self._save_rate() - return 0 + log IController.CollectFees(amount=0, new_supply=loan.initial_debt) + self._save_rate() + return 0 @external From b59ec7c0691dc57021c7e4318148ba726620812e Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 20 Oct 2025 18:12:16 +0400 Subject: [PATCH 394/413] test: complete test_internal_repay_full --- .../controller/test_internal_repay_full.py | 703 +++++++++++++++++- 1 file changed, 680 insertions(+), 23 deletions(-) diff --git a/tests/unitary/controller/test_internal_repay_full.py b/tests/unitary/controller/test_internal_repay_full.py index 8a7b1b7d..44f2393e 100644 --- a/tests/unitary/controller/test_internal_repay_full.py +++ b/tests/unitary/controller/test_internal_repay_full.py @@ -3,9 +3,9 @@ from textwrap import dedent from tests.utils import filter_logs, max_approve +from tests.utils.constants import ZERO_ADDRESS -COLLATERAL = 10**21 -DEBT = 10**18 +COLLATERAL = 10**17 N_BANDS = 6 @@ -19,12 +19,11 @@ def repay_full( _for: address, _d_debt: uint256, _approval: bool, - xy_borrowed: uint256, - xy_collateral: uint256 + _xy: uint256[2], + _cb: core.IController.CallbackData, + _callbacker: address ): - xy: uint256[2] = [xy_borrowed, xy_collateral] - cb: core.IController.CallbackData = empty(core.IController.CallbackData) - core._repay_full(_for, _d_debt, _approval, xy, cb, empty(address)) + core._repay_full(_for, _d_debt, _approval, _xy, _cb, _callbacker) """ ) ) @@ -32,10 +31,11 @@ def repay_full( @pytest.fixture(scope="module") def snapshot(controller, amm, fake_leverage): - def fn(token, borrower: str): + 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), } @@ -43,37 +43,55 @@ def fn(token, borrower: str): return fn -def test_default_behavior_no_callback( - controller, borrowed_token, collateral_token, amm, snapshot +@pytest.mark.parametrize("different_payer", [True, False]) +def test_repay_full_from_wallet( + controller, borrowed_token, collateral_token, amm, snapshot, different_payer ): - borrower = boa.env.eoa + """ + 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() boa.deal(collateral_token, borrower, COLLATERAL) max_approve(collateral_token, controller) - controller.create_loan(COLLATERAL, DEBT, N_BANDS) + controller.create_loan(COLLATERAL, 10**18, N_BANDS) - max_approve(borrowed_token, controller) debt = controller.debt(borrower) + xy_before = amm.get_sum_xy(borrower) + assert xy_before[0] == 0 and xy_before[1] > 0 - xy = amm.get_sum_xy(borrower) + if different_payer: + boa.deal(borrowed_token, payer, debt) - borrowed_token_before = snapshot(borrowed_token, borrower) - collateral_token_before = snapshot(collateral_token, borrower) + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) - controller.inject.repay_full(borrower, debt, True, xy[0], xy[1]) + 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 + ) repay_logs = filter_logs(controller, "Repay") state_logs = filter_logs(controller, "UserState") - borrowed_token_after = snapshot(borrowed_token, borrower) - collateral_token_after = snapshot(collateral_token, borrower) + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) borrowed_to_controller = ( borrowed_token_after["controller"] - borrowed_token_before["controller"] ) - borrowed_from_borrower = ( - borrowed_token_after["borrower"] - borrowed_token_before["borrower"] - ) + borrowed_from_payer = borrowed_token_after["payer"] - borrowed_token_before["payer"] collateral_to_borrower = ( collateral_token_after["borrower"] - collateral_token_before["borrower"] ) @@ -99,7 +117,7 @@ def test_default_behavior_no_callback( assert repay_logs[0].collateral_decrease == collateral_to_borrower assert borrowed_to_controller == debt - assert borrowed_from_borrower == -debt + assert borrowed_from_payer == -debt assert borrowed_token_after["amm"] == borrowed_token_before["amm"] assert borrowed_token_after["callback"] == borrowed_token_before["callback"] @@ -107,3 +125,642 @@ def test_default_behavior_no_callback( 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_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() + + 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 the position to SL + 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) + + debt = controller.debt(borrower) + xy_before = amm.get_sum_xy(borrower) + assert xy_before[0] > debt and xy_before[1] > 0 + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + 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 + ) + + repay_logs = filter_logs(controller, "Repay") + state_logs = filter_logs(controller, "UserState") + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + 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"] + + # 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 + + 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] + + 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_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() + + 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 the position to SL + 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) + + debt = controller.debt(borrower) + xy_before = amm.get_sum_xy(borrower) + assert 0 < xy_before[0] < debt and xy_before[1] > 0 + + if different_payer: + boa.deal(borrowed_token, payer, debt) + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + 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 + ) + + repay_logs = filter_logs(controller, "Repay") + state_logs = filter_logs(controller, "UserState") + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + 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"] + + # 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 + + 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] + + 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_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() + + boa.deal(collateral_token, borrower, COLLATERAL) + max_approve(collateral_token, controller) + controller.create_loan(COLLATERAL, 10**18, N_BANDS) + + debt = controller.debt(borrower) + xy_before = amm.get_sum_xy(borrower) + assert xy_before[0] == 0 and xy_before[1] > 0 + + # 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) + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + 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 + ) + + repay_logs = filter_logs(controller, "Repay") + state_logs = filter_logs(controller, "UserState") + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + 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"] + ) + + # 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 + + 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 + + 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_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() + + boa.deal(collateral_token, borrower, COLLATERAL) + max_approve(collateral_token, controller) + controller.create_loan(COLLATERAL, 10**18, N_BANDS) + + debt = controller.debt(borrower) + xy_before = amm.get_sum_xy(borrower) + assert xy_before[0] == 0 and xy_before[1] > 0 + + if different_payer: + boa.deal(borrowed_token, payer, 1) + + # 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) + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + 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 + ) + + repay_logs = filter_logs(controller, "Repay") + state_logs = filter_logs(controller, "UserState") + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + 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"] + ) + + # 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 + + 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 + + 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_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() + + 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 the position to SL + 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) + + debt = controller.debt(borrower) + xy_before = amm.get_sum_xy(borrower) + assert 1 < xy_before[0] < debt and xy_before[1] > 0 + + # 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) + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + 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.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 + ) + + repay_logs = filter_logs(controller, "Repay") + state_logs = filter_logs(controller, "UserState") + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + 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"] + ) + + # 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 + + 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] + + 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_y0_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() + + 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 the position to SL + 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) + + debt = controller.debt(borrower) + xy_before = amm.get_sum_xy(borrower) + assert 1 < xy_before[0] < debt and xy_before[1] > 0 + + if different_payer: + boa.deal(borrowed_token, payer, 1) + + # 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) + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + 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.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 + ) + + repay_logs = filter_logs(controller, "Repay") + state_logs = filter_logs(controller, "UserState") + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + 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"] + ) + + # 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 + + 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] + + 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"] From a1bf13c8eb32df4664ea28370b4e5b167f0c81a1 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 20 Oct 2025 17:30:38 +0200 Subject: [PATCH 395/413] chore: save for later --- pyproject.toml | 1 + tests/crosshair/deposited_withdrawn.py | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 tests/crosshair/deposited_withdrawn.py diff --git a/pyproject.toml b/pyproject.toml index 86b1fc15..00ae9d7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ dev = [ "pytest-cov>=4.0.0", "pytest-profiling>=1.8.1", "pre-commit==4.3.0", + "z3-solver>=4.12.0", ] [tool.uv.sources] 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()) From d07aaeaea8c9662c8200eb10923228c37ba6bb56 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 20 Oct 2025 17:42:19 +0200 Subject: [PATCH 396/413] refactor: simplify admin fee claim * remove logic duplication between `_admin_fees` and `_collect_fees`. * make `_collect_fees` branchless * simplify naming and improve docs --- contracts/Controller.vy | 53 ++++++++++++++++------------- contracts/lending/LendController.vy | 2 +- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 5fed6b80..a7d3e682 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -1452,7 +1452,7 @@ def admin_fees() -> uint256: """ # In mint controller, 100% (WAD) fees are # collected as admin fees. - return self._admin_fees(WAD) + return self._admin_fees(WAD)[0] @external @@ -1467,42 +1467,47 @@ def collect_fees() -> uint256: @internal @view -def _admin_fees(admin_fee: uint256) -> uint256: +def _admin_fees(admin_fee: uint256) -> (uint256, uint256): """ @notice Calculate the amount of fees obtained from the interest + @return (admin_fees, unprocessed) respectively: + - amount of admin fees the admin can claim + - amount of unprocessed interest that will be marked as processed on claim """ processed: uint256 = self.processed - return unsafe_sub( - max(self._get_total_debt() + self.repaid, processed), processed - ) * admin_fee // WAD + + # Cumulative amount which would have been repaid if all the debt was repaid now + total_repaid_projection: uint256 = self.repaid + self._get_total_debt() + + if total_repaid_projection <= processed: + return 0, 0 + + # (total_repaid - processed) is the total interest accrued that + # has not been processed yet (admin fees have not been taken from it) + unprocessed: uint256 = unsafe_sub(total_repaid_projection, processed) + + return unprocessed * admin_fee // WAD, unprocessed @internal -def _collect_fees(admin_fee: uint256) -> uint256: - if admin_fee == 0: +def _collect_fees(_admin_fee_percentage: uint256) -> uint256: + if _admin_fee_percentage == 0: return 0 - _to: address = staticcall FACTORY.fee_receiver() - - # Borrowing-based fees rate_mul: uint256 = staticcall AMM.get_rate_mul() loan: IController.Loan = self._update_total_debt(0, rate_mul, False) - # Cumulative amount which would have been repaid if all the debt was repaid now - to_be_repaid: uint256 = loan.initial_debt + self.repaid - # Cumulative amount which was processed (admin fees have been taken from) - processed: uint256 = self.processed - # Difference between to_be_repaid and processed amount is exactly due to interest charged - if to_be_repaid > processed: - self.processed = to_be_repaid - fees: uint256 = unsafe_sub(to_be_repaid, processed) * admin_fee // WAD - tkn.transfer(BORROWED_TOKEN, _to, fees) - log IController.CollectFees(amount=fees, new_supply=loan.initial_debt) - self._save_rate() - return fees - log IController.CollectFees(amount=0, new_supply=loan.initial_debt) + fees: uint256 = 0 + unprocessed: uint256 = 0 + fees, unprocessed = self._admin_fees(_admin_fee_percentage) + self.processed += unprocessed + + to: address = staticcall FACTORY.fee_receiver() + tkn.transfer(BORROWED_TOKEN, to, fees) self._save_rate() - return 0 + + log IController.CollectFees(amount=fees, new_supply=loan.initial_debt) + return fees @external diff --git a/contracts/lending/LendController.vy b/contracts/lending/LendController.vy index 7f8d3744..50a0283c 100644 --- a/contracts/lending/LendController.vy +++ b/contracts/lending/LendController.vy @@ -239,7 +239,7 @@ def admin_fees() -> uint256: """ @notice Return the amount of borrowed tokens that have been collected as fees. """ - return core._admin_fees(self.admin_fee) + return core._admin_fees(self.admin_fee)[0] @external From 17121f6af90e03e585d86b9e90a158e02748b7c7 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 20 Oct 2025 18:14:47 +0200 Subject: [PATCH 397/413] refactor: use token_lib in vault --- contracts/lending/Vault.vy | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/contracts/lending/Vault.vy b/contracts/lending/Vault.vy index d085e4d9..62e2d066 100644 --- a/contracts/lending/Vault.vy +++ b/contracts/lending/Vault.vy @@ -14,11 +14,13 @@ from contracts.interfaces import ILlamalendController as IController from contracts.interfaces import IFactory from contracts import constants as c +from contracts.lib import token_lib as tkn implements: IERC20 implements: IERC4626 +# TODO move to own interface event SetMaxSupply: max_supply: uint256 @@ -29,6 +31,7 @@ event SetMaxSupply: DEAD_SHARES: constant(uint256) = c.DEAD_SHARES MIN_ASSETS: constant(uint256) = 10000 +# TODO turn into immutables borrowed_token: public(IERC20) collateral_token: public(IERC20) @@ -263,7 +266,7 @@ def deposit(assets: uint256, receiver: address = msg.sender) -> uint256: 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 extcall 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.asset_balance += assets self._mint(receiver, to_mint) extcall controller.save_rate() @@ -308,7 +311,7 @@ def mint(shares: uint256, receiver: address = msg.sender) -> uint256: 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 extcall 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.asset_balance += assets self._mint(receiver, shares) extcall controller.save_rate() @@ -358,7 +361,7 @@ def withdraw(assets: uint256, receiver: address = msg.sender, owner: address = m controller: IController = self.controller self._burn(owner, shares) - assert extcall self.borrowed_token.transferFrom(controller.address, receiver, assets, default_return_value=True) + tkn.transfer_from(self.borrowed_token, controller.address, receiver, assets) self.asset_balance -= assets extcall controller.save_rate() log IERC4626.Withdraw(sender=msg.sender, receiver=receiver, owner=owner, assets=assets, shares=shares) @@ -418,7 +421,7 @@ def redeem(shares: uint256, receiver: address = msg.sender, owner: address = msg raise "Need more assets" self._burn(owner, shares) controller: IController = self.controller - assert extcall self.borrowed_token.transferFrom(controller.address, receiver, assets_to_redeem, default_return_value=True) + tkn.transfer_from(self.borrowed_token, controller.address, receiver, assets_to_redeem) self.asset_balance -= assets_to_redeem extcall controller.save_rate() log IERC4626.Withdraw(sender=msg.sender, receiver=receiver, owner=owner, assets=assets_to_redeem, shares=shares) From 7451d089637640d3b106f0be590af7964eeb4129 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 20 Oct 2025 18:23:07 +0200 Subject: [PATCH 398/413] Revert "perf: remove redundant check" This reverts commit 9c388cbf93485301d9ccb1823cf27abaf146be10. --- contracts/lending/Vault.vy | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/lending/Vault.vy b/contracts/lending/Vault.vy index 62e2d066..be37d486 100644 --- a/contracts/lending/Vault.vy +++ b/contracts/lending/Vault.vy @@ -76,6 +76,8 @@ def initialize( @param borrowed_token Token which is being borrowed @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 From 6510aef19507d22434d527bde422f649e42d143d Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 21 Oct 2025 17:52:14 +0200 Subject: [PATCH 399/413] refactor(Controller): simplify admin fee logic --- contracts/Controller.vy | 173 +++++++----------- contracts/interfaces/IController.vyi | 11 +- contracts/interfaces/ILlamalendController.vyi | 40 +--- contracts/lending/LendController.vy | 118 +++--------- contracts/lib/math_lib.vy | 6 + 5 files changed, 103 insertions(+), 245 deletions(-) create mode 100644 contracts/lib/math_lib.vy diff --git a/contracts/Controller.vy b/contracts/Controller.vy index a7d3e682..c8388676 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -24,6 +24,7 @@ implements: IController implements: IView from contracts.lib import token_lib as tkn +from contracts.lib import math_lib as crv_math from snekmate.utils import math @@ -119,13 +120,21 @@ loan_ix: public(HashMap[address, uint256]) # Number of nonzero loans n_loans: public(uint256) + +# cumulative amount of assets ever lent +lent: uint256 # cumulative amount of assets ever repaid (including admin fees) -repaid: public(uint256) -# cumulative amount of assets admin fees have been taken from -processed: public(uint256) +repaid: uint256 + +# Admin fees yet to be collected. Goes to zero when collected. +admin_fees: public(uint256) +admin_percentage: public(uint256) + +# DANGER DO NOT RELY ON MSG.SENDER IN VIRTUAL METHODS +interface VirtualMethods: + def _on_debt_increased(debt: uint256): nonpayable -# unused for mint controller as it overlaps with debt ceiling -borrow_cap: uint256 +VIRTUAL: immutable(VirtualMethods) @deploy @@ -138,12 +147,12 @@ def __init__( _AMM: IAMM, view_impl: address, ): - # TODO add sanity check for zero addresses + VIRTUAL = VirtualMethods(self) + # 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.borrow_cap = max_value(uint256) FACTORY = IFactory(msg.sender) AMM = _AMM @@ -171,6 +180,7 @@ def __init__( 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) @@ -209,11 +219,12 @@ def _set_view(view_impl: address): log IController.SetView(view=view) +# TODO add back minted and redeemed @view @external def minted() -> uint256: - return self.processed + return self.lent @view @@ -251,12 +262,16 @@ def _update_total_debt( @notice Update total debt of this controller """ loan: IController.Loan = self._total_debt - loan.initial_debt = loan.initial_debt * rate_mul // loan.rate_mul + 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 - assert loan.initial_debt <= self.borrow_cap, "Borrow cap exceeded" + extcall VIRTUAL._on_debt_increased(loan.initial_debt) else: - loan.initial_debt = unsafe_sub(max(loan.initial_debt, d_debt), d_debt) + loan.initial_debt = crv_math.sub_or_zero(loan.initial_debt, d_debt) loan.rate_mul = rate_mul self._total_debt = loan @@ -611,15 +626,26 @@ def execute_callback( return data -@internal -def _create_loan( +# TODO use underscore for args everywhere +@external +def create_loan( collateral: uint256, debt: uint256, N: uint256, - _for: address, + _for: address = msg.sender, callbacker: address = empty(address), calldata: Bytes[CALLDATA_MAX_SIZE] = b"", -) -> uint256: +): + """ + @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 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 @@ -662,8 +688,6 @@ def _create_loan( extcall AMM.deposit_range(_for, total_collateral, n1, n2) - self.processed += debt - log IController.UserState( user=_for, collateral=total_collateral, @@ -683,29 +707,7 @@ def _create_loan( self._save_rate() - return debt - - -@external -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 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 - """ - self._create_loan(collateral, debt, N, _for, callbacker, calldata) + self.lent += debt @internal @@ -827,25 +829,8 @@ def borrow_more( @param callbacker Address of the callback contract @param calldata Any data for callbacker """ - _debt: uint256 = self._borrow_more( - collateral, - debt, - _for, - callbacker, - calldata, - ) - - -@internal -def _borrow_more( - collateral: uint256, - debt: uint256, - _for: address = msg.sender, - callbacker: address = empty(address), - calldata: Bytes[CALLDATA_MAX_SIZE] = b"", -) -> uint256: if debt == 0: - return 0 + return assert self._check_approval(_for) more_collateral: uint256 = 0 @@ -871,11 +856,9 @@ def _borrow_more( if callbacker == empty(address): tkn.transfer(BORROWED_TOKEN, _for, debt) - self.processed += debt + self.lent += debt self._save_rate() - return debt - @internal def _remove_from_list(_for: address): @@ -1074,8 +1057,8 @@ def repay( 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.repaid += d_debt self._save_rate() @@ -1303,6 +1286,7 @@ def liquidate( self._update_total_debt(debt, rate_mul, False) + self.repaid += debt self._save_rate() @@ -1444,70 +1428,37 @@ def set_callback(cb: ILMGauge): log IController.SetLMCallback(callback=cb) -@external -@view -def admin_fees() -> uint256: - """ - @notice Calculate the amount of fees obtained from the interest - """ - # In mint controller, 100% (WAD) fees are - # collected as admin fees. - return self._admin_fees(WAD)[0] - - @external def collect_fees() -> uint256: """ @notice Collect the fees charged as interest. """ - # In mint controller, 100% (WAD) fees are - # collected as admin fees. - return self._collect_fees(WAD) + return self._collect_fees() @internal -@view -def _admin_fees(admin_fee: uint256) -> (uint256, uint256): - """ - @notice Calculate the amount of fees obtained from the interest - @return (admin_fees, unprocessed) respectively: - - amount of admin fees the admin can claim - - amount of unprocessed interest that will be marked as processed on claim - """ - processed: uint256 = self.processed - - # Cumulative amount which would have been repaid if all the debt was repaid now - total_repaid_projection: uint256 = self.repaid + self._get_total_debt() - - if total_repaid_projection <= processed: - return 0, 0 - - # (total_repaid - processed) is the total interest accrued that - # has not been processed yet (admin fees have not been taken from it) - unprocessed: uint256 = unsafe_sub(total_repaid_projection, processed) - - return unprocessed * admin_fee // WAD, unprocessed - - -@internal -def _collect_fees(_admin_fee_percentage: uint256) -> uint256: - if _admin_fee_percentage == 0: - return 0 - +def _collect_fees() -> uint256: rate_mul: uint256 = staticcall AMM.get_rate_mul() loan: IController.Loan = self._update_total_debt(0, rate_mul, False) - fees: uint256 = 0 - unprocessed: uint256 = 0 - fees, unprocessed = self._admin_fees(_admin_fee_percentage) - self.processed += unprocessed + pending_admin_fees: uint256 = self.admin_fees + # TODO figure out rev share (probably add a setter in factory) to: address = staticcall FACTORY.fee_receiver() - tkn.transfer(BORROWED_TOKEN, to, fees) + tkn.transfer(BORROWED_TOKEN, to, pending_admin_fees) + + self.admin_fees = 0 + self._save_rate() - log IController.CollectFees(amount=fees, new_supply=loan.initial_debt) - return fees + log IController.CollectFees(amount=pending_admin_fees, new_supply=loan.initial_debt) + return pending_admin_fees + + +@external +@reentrant # TODO check if needed +def _on_debt_increased(debt: uint256): + pass @external diff --git a/contracts/interfaces/IController.vyi b/contracts/interfaces/IController.vyi index d6998447..5d95bca0 100644 --- a/contracts/interfaces/IController.vyi +++ b/contracts/interfaces/IController.vyi @@ -354,20 +354,15 @@ def n_loans() -> uint256: @view @external -def repaid() -> uint256: +def version() -> String[1]: ... -@view @external -def processed() -> uint256: +def set_view(view_impl: address): ... -@view -@external -def version() -> String[1]: - ... @external -def set_view(view_impl: address): +def _on_debt_increased(debt: uint256): ... diff --git a/contracts/interfaces/ILlamalendController.vyi b/contracts/interfaces/ILlamalendController.vyi index 3fbbce9c..f351e9eb 100644 --- a/contracts/interfaces/ILlamalendController.vyi +++ b/contracts/interfaces/ILlamalendController.vyi @@ -73,8 +73,8 @@ event CollectFees: new_supply: uint256 -event SetAdminFee: - admin_fee: uint256 +event SetAdminPercentage: + admin_percentage: uint256 # Functions @@ -85,18 +85,6 @@ def monetary_policy() -> IMonetaryPolicy: ... -@view -@external -def minted() -> uint256: - ... - - -@view -@external -def redeemed() -> uint256: - ... - - @view @external def max_borrowable(collateral: uint256, N: uint256, current_debt: uint256, user: address) -> uint256: @@ -329,20 +317,6 @@ def loan_ix(arg0: address) -> uint256: def n_loans() -> uint256: ... - -@view -@external -def repaid() -> uint256: - ... - - -@view -@external -def processed() -> uint256: - ... - - - # EXTRA THINGS FOR LLAMALEND CONTROLLER event SetBorrowCap: @@ -361,13 +335,7 @@ def set_borrow_cap(_borrow_cap: uint256): @external -def set_admin_fee(admin_fee: uint256): - ... - - -@view -@external -def lent() -> uint256: +def set_admin_percentage(admin_percentage: uint256): ... @@ -379,7 +347,7 @@ def collected() -> uint256: @view @external -def admin_fee() -> uint256: +def admin_percentage() -> uint256: ... diff --git a/contracts/lending/LendController.vy b/contracts/lending/LendController.vy index 50a0283c..02d7a32c 100644 --- a/contracts/lending/LendController.vy +++ b/contracts/lending/LendController.vy @@ -27,9 +27,12 @@ from contracts import Controller as core initializes: core from contracts.lib import token_lib as tkn +from contracts.lib import math_lib as crv_math exports: ( # Loan management + core.create_loan, + core.borrow_more, core.add_collateral, core.approve, core.remove_collateral, @@ -60,8 +63,8 @@ exports: ( core.tokens_to_shrink, core.total_debt, core.factory, - core.processed, - core.repaid, + core.admin_fees, + core.admin_percentage, # Setters core.set_view, core.set_amm_fee, @@ -92,15 +95,9 @@ def vault() -> IVault: return VAULT - -# cumulative amount of assets ever lent -lent: public(uint256) # cumulative amount of assets collected by admin collected: public(uint256) - -# TODO Rename to admin percentage? -admin_fee: public(uint256) - +borrow_cap: public(uint256) @deploy def __init__( @@ -136,7 +133,7 @@ def __init__( # Borrow cap is zero by default in lend markets. The admin has to raise it # after deployment to allow borrowing. - core.borrow_cap = 0 + core.admin_percentage = 0 # Pre-approve the vault to transfer borrowed tokens out of the controller tkn.max_approve(core.BORROWED_TOKEN, VAULT.address) @@ -151,15 +148,6 @@ def version() -> String[10]: return concat(core.version, "-lend") -@external -@view -def borrow_cap() -> uint256: - """ - @notice Maximum amount of borrowed tokens that can be lent out at any time. - """ - return core.borrow_cap - - @external @view def borrowed_balance() -> uint256: @@ -173,73 +161,13 @@ def borrowed_balance() -> uint256: # and subtract what the admin already skimmed as fees; what’s left is the controller’s idle cash. # VAULT.asset_balance() - (lent - repaid) - self.collected # The terms are rearranged to avoid underflows in intermediate steps. - return ( - staticcall VAULT.asset_balance() + # TODO handle asset_balance() underflow + balance: uint256 = (staticcall VAULT.asset_balance() + core.repaid - - self.lent + - core.lent - self.collected ) - - -@external -def create_loan( - collateral: uint256, - debt: uint256, - N: uint256, - _for: address = msg.sender, - callbacker: address = empty(address), - calldata: Bytes[10**4] = 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 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 - """ - _debt: uint256 = core._create_loan( - collateral, debt, N, _for, callbacker, calldata - ) - self.lent += _debt - - -@external -def borrow_more( - collateral: uint256, - debt: uint256, - _for: address = msg.sender, - callbacker: address = empty(address), - calldata: Bytes[10**4] = 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 borrowed asset debt to take - @param _for Address to borrow for - @param callbacker Address of the callback contract - @param calldata Any data for callbacker - """ - _debt: uint256 = core._borrow_more( - collateral, - debt, - _for, - callbacker, - calldata, - ) - - self.lent += _debt - - -@external -@view -def admin_fees() -> uint256: - """ - @notice Return the amount of borrowed tokens that have been collected as fees. - """ - return core._admin_fees(self.admin_fee)[0] + return crv_math.sub_or_zero(balance, core.admin_fees) @external @@ -247,7 +175,7 @@ def collect_fees() -> uint256: """ @notice Collect the fees charged as interest that belong to the admin. """ - fees: uint256 = core._collect_fees(self.admin_fee) + fees: uint256 = core._collect_fees() self.collected += fees return fees @@ -260,16 +188,26 @@ def set_borrow_cap(_borrow_cap: uint256): @param _borrow_cap New borrow cap in units of borrowed_token """ core._check_admin() - core.borrow_cap = _borrow_cap + self.borrow_cap = _borrow_cap log ILlamalendController.SetBorrowCap(borrow_cap=_borrow_cap) @external -def set_admin_fee(_admin_fee: uint256): +def set_admin_percentage(_admin_percentage: uint256): """ - @param _admin_fee The percentage of interest that goes to the admin, scaled by 1e18 + @param _admin_percentage The percentage of interest that goes to the admin, scaled by 1e18 """ core._check_admin() - assert _admin_fee <= core.WAD # dev: admin fee higher than 100% - self.admin_fee = _admin_fee - log ILlamalendController.SetAdminFee(admin_fee=_admin_fee) + 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(debt: uint256): + """ + @notice Hook called when debt is increased + """ + assert msg.sender == self # dev: virtual method protection + assert debt <= self.borrow_cap, "Borrow cap exceeded" diff --git a/contracts/lib/math_lib.vy b/contracts/lib/math_lib.vy new file mode 100644 index 00000000..31a7af3f --- /dev/null +++ b/contracts/lib/math_lib.vy @@ -0,0 +1,6 @@ +# TODO use this util everywhere: ctrl+f unsafe_sub(max( +@pure +@internal +def sub_or_zero(a: uint256, b: uint256) -> uint256: + """Subtraction that floors at zero.""" + return unsafe_sub(max(a, b), b) From 7966f179ca2108a6763e5494712acc3fdddf1793 Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 22 Oct 2025 14:32:00 +0400 Subject: [PATCH 400/413] test: _repay_partial --- .../controller/test_internal_repay_partial.py | 1196 +++++++++++++++++ 1 file changed, 1196 insertions(+) create mode 100644 tests/unitary/controller/test_internal_repay_partial.py 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..8020d5f4 --- /dev/null +++ b/tests/unitary/controller/test_internal_repay_partial.py @@ -0,0 +1,1196 @@ +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_controller = ( + collateral_token_after["controller"] - collateral_token_before["controller"] + ) + 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_controller == callback_collateral + assert collateral_from_callback == -callback_collateral + assert collateral_token_after["amm"] == collateral_token_before["amm"] + 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_controller = ( + collateral_token_after["controller"] - collateral_token_before["controller"] + ) + 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_controller == callback_collateral + assert collateral_from_callback == -callback_collateral + assert collateral_token_after["amm"] == collateral_token_before["amm"] + 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 * 10_001 // 10_000) + with boa.env.prank(trader): + max_approve(borrowed_token, amm) + amm.exchange(0, 1, debt * 10_001 // 10_000, 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 xy_before[0] > 0 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 xy_before[0] > 0 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 xy_before[0] > 0 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 xy_before[0] > 0 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_controller = ( + collateral_token_after["controller"] - collateral_token_before["controller"] + ) + 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_controller == callback_collateral + assert collateral_from_callback == -callback_collateral + assert collateral_token_after["amm"] == collateral_token_before["amm"] + 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 xy_before[0] > 0 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_controller = ( + collateral_token_after["controller"] - collateral_token_before["controller"] + ) + 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_controller == callback_collateral + assert collateral_from_callback == -callback_collateral + assert collateral_token_after["amm"] == collateral_token_before["amm"] + 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) + + with boa.reverts("Can't shrink"): + controller.tokens_to_shrink(borrower) + + # ================= 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 xy_before[0] > 0 and xy_before[1] > 0 # Position is underwater + + # ================= Execute partial repayment ================= + + with boa.env.prank(payer): + # _shrink == True, reverts without approval + 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 + ) From 6c47969cd3f20b8f48a989e0f03d764e19f40ee6 Mon Sep 17 00:00:00 2001 From: macket Date: Wed, 22 Oct 2025 15:58:45 +0400 Subject: [PATCH 401/413] chore: comments for test_internal_repay_full --- .../controller/test_internal_repay_full.py | 172 +++++++++++++++++- 1 file changed, 164 insertions(+), 8 deletions(-) diff --git a/tests/unitary/controller/test_internal_repay_full.py b/tests/unitary/controller/test_internal_repay_full.py index 44f2393e..6e79f728 100644 --- a/tests/unitary/controller/test_internal_repay_full.py +++ b/tests/unitary/controller/test_internal_repay_full.py @@ -57,20 +57,30 @@ def test_repay_full_from_wallet( 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 @@ -82,12 +92,18 @@ def test_repay_full_from_wallet( 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"] ) @@ -97,12 +113,16 @@ def test_repay_full_from_wallet( ) 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 @@ -116,6 +136,8 @@ def test_repay_full_from_wallet( 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"] @@ -146,26 +168,35 @@ def test_repay_full_from_xy0( 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 the position to SL + # ================= 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( @@ -175,12 +206,18 @@ def test_repay_full_from_xy0( 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"] ) @@ -193,12 +230,16 @@ def test_repay_full_from_xy0( ) 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 @@ -212,6 +253,8 @@ def test_repay_full_from_xy0( 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 @@ -241,29 +284,40 @@ def test_repay_full_from_xy0_and_wallet( 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 the position to SL + # ================= 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(): @@ -274,12 +328,18 @@ def test_repay_full_from_xy0_and_wallet( 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"] ) @@ -290,12 +350,16 @@ def test_repay_full_from_xy0_and_wallet( ) 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 @@ -309,6 +373,8 @@ def test_repay_full_from_xy0_and_wallet( 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]) @@ -345,23 +411,33 @@ def test_repay_full_from_callback( 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. @@ -373,12 +449,18 @@ def test_repay_full_from_callback( 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"] ) @@ -395,12 +477,16 @@ def test_repay_full_from_callback( 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 @@ -414,6 +500,8 @@ def test_repay_full_from_callback( 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) @@ -449,26 +537,38 @@ def test_repay_full_from_wallet_and_callback( 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. @@ -481,12 +581,18 @@ def test_repay_full_from_wallet_and_callback( 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"] ) @@ -501,12 +607,16 @@ def test_repay_full_from_wallet_and_callback( 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 @@ -520,6 +630,8 @@ def test_repay_full_from_wallet_and_callback( 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) @@ -556,35 +668,44 @@ def test_repay_full_from_xy0_and_callback( 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 the position to SL + # ================= 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): - # 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.reverts(): controller.inject.repay_full( borrower, debt, False, xy_before, cb, fake_leverage.address @@ -593,12 +714,18 @@ def test_repay_full_from_xy0_and_callback( 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"] ) @@ -616,12 +743,16 @@ def test_repay_full_from_xy0_and_callback( 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 @@ -635,6 +766,8 @@ def test_repay_full_from_xy0_and_callback( 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) @@ -670,39 +803,50 @@ def test_repay_full_from_wallet_and_y0_and_callback( 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 the position to SL + # ================= 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) - # 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.reverts(): controller.inject.repay_full( borrower, debt, False, xy_before, cb, fake_leverage.address @@ -711,12 +855,18 @@ def test_repay_full_from_wallet_and_y0_and_callback( 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"] ) @@ -732,12 +882,16 @@ def test_repay_full_from_wallet_and_y0_and_callback( 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 @@ -751,6 +905,8 @@ def test_repay_full_from_wallet_and_y0_and_callback( 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) From 3e774f9c6937576ab6f28f066abdefcb659afe73 Mon Sep 17 00:00:00 2001 From: macket Date: Sun, 26 Oct 2025 16:59:36 +0400 Subject: [PATCH 402/413] chore: change order in test_internal_repay_full --- .../controller/test_internal_repay_full.py | 214 +++++++++--------- 1 file changed, 107 insertions(+), 107 deletions(-) diff --git a/tests/unitary/controller/test_internal_repay_full.py b/tests/unitary/controller/test_internal_repay_full.py index 6e79f728..b9086d81 100644 --- a/tests/unitary/controller/test_internal_repay_full.py +++ b/tests/unitary/controller/test_internal_repay_full.py @@ -154,15 +154,21 @@ def test_repay_full_from_wallet( @pytest.mark.parametrize("different_payer", [True, False]) -def test_repay_full_from_xy0( - controller, borrowed_token, collateral_token, amm, snapshot, different_payer +def test_repay_full_from_callback( + controller, + borrowed_token, + collateral_token, + amm, + snapshot, + fake_leverage, + different_payer, ): """ - Test full repayment using only AMM soft-liquidation (xy[0] >= DEBT). + Test full repayment using only callback tokens. - Money Flow: xy[0] (AMM) → Controller - xy[0] - DEBT (AMM) → Borrower (excess) - xy[1] (AMM) → Borrower + Money Flow: cb.borrowed (callback) → Controller + cb.borrowed - DEBT (callback) → Borrower (excess) + cb.collateral (callback) → Borrower """ borrower = payer = boa.env.eoa if different_payer: @@ -172,23 +178,21 @@ def test_repay_full_from_xy0( 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) + 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] > debt and xy_before[1] > 0 + 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 ================= @@ -198,12 +202,14 @@ def test_repay_full_from_xy0( # ================= Execute full repayment ================= with boa.env.prank(payer): - with boa.reverts(): + # 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, (0, 0, 0), ZERO_ADDRESS + borrower, debt, False, xy_before, cb, fake_leverage.address ) controller.inject.repay_full( - borrower, debt, True, xy_before, (0, 0, 0), ZERO_ADDRESS + borrower, debt, True, xy_before, cb, fake_leverage.address ) # ================= Capture logs ================= @@ -221,22 +227,26 @@ def test_repay_full_from_xy0( 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"] ) - 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"] + collateral_from_callbacker = ( + collateral_token_after["callback"] - collateral_token_before["callback"] + ) # ================= Verify position state ================= # Withdrawn from AMM - xy_after = amm.get_sum_xy(borrower) + xy = amm.get_sum_xy(borrower) assert amm.user_shares(borrower)[1][0] == 0 - assert xy_after[0] == 0 - assert xy_after[1] == 0 + assert xy[0] == 0 + assert xy[1] == 0 # ================= Verify logs ================= @@ -250,19 +260,19 @@ def test_repay_full_from_xy0( 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] + 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_amm == -xy_before[0] - assert borrowed_to_borrower == xy_before[0] - debt - assert borrowed_token_after["callback"] == borrowed_token_before["callback"] + assert borrowed_to_borrower == 1 + assert borrowed_from_callback == -(debt + 1) + assert borrowed_token_after["amm"] == borrowed_token_before["amm"] - 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_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: @@ -271,13 +281,14 @@ def test_repay_full_from_xy0( @pytest.mark.parametrize("different_payer", [True, False]) -def test_repay_full_from_xy0_and_wallet( +def test_repay_full_from_xy0( controller, borrowed_token, collateral_token, amm, snapshot, different_payer ): """ - Test full repayment using both AMM soft-liquidation (xy[0]) and wallet tokens. + Test full repayment using only AMM soft-liquidation (xy[0] >= DEBT). - Money Flow: xy[0] (AMM) + (DEBT - xy[0]) (wallet) → Controller + Money Flow: xy[0] (AMM) → Controller + xy[0] - DEBT (AMM) → Borrower (excess) xy[1] (AMM) → Borrower """ borrower = payer = boa.env.eoa @@ -289,27 +300,22 @@ def test_repay_full_from_xy0_and_wallet( 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 + 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) + 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 // 2, 0) + 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 0 < xy_before[0] < debt and xy_before[1] > 0 - - # ================= Setup payer tokens ================= - - if different_payer: - boa.deal(borrowed_token, payer, debt) + assert xy_before[0] > debt and xy_before[1] > 0 # ================= Capture initial balances ================= @@ -319,7 +325,6 @@ def test_repay_full_from_xy0_and_wallet( # ================= 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 @@ -343,7 +348,9 @@ def test_repay_full_from_xy0_and_wallet( borrowed_to_controller = ( borrowed_token_after["controller"] - borrowed_token_before["controller"] ) - borrowed_from_payer = borrowed_token_after["payer"] - borrowed_token_before["payer"] + 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"] @@ -377,7 +384,7 @@ def test_repay_full_from_xy0_and_wallet( assert borrowed_to_controller == debt assert borrowed_from_amm == -xy_before[0] - assert borrowed_from_payer == -(debt - 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] @@ -386,12 +393,12 @@ def test_repay_full_from_xy0_and_wallet( assert collateral_token_after["controller"] == collateral_token_before["controller"] if different_payer: - assert borrowed_token_after["borrower"] == borrowed_token_before["borrower"] + 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_callback( +def test_repay_full_from_wallet_and_callback( controller, borrowed_token, collateral_token, @@ -401,10 +408,9 @@ def test_repay_full_from_callback( different_payer, ): """ - Test full repayment using only callback tokens. + Test full repayment using wallet + callback tokens. - Money Flow: cb.borrowed (callback) → Controller - cb.borrowed - DEBT (callback) → Borrower (excess) + Money Flow: cb.borrowed (callback) + (DEBT - cb.borrowed) (wallet) → Controller cb.collateral (callback) → Borrower """ borrower = payer = boa.env.eoa @@ -423,13 +429,18 @@ def test_repay_full_from_callback( 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) + boa.deal(borrowed_token, fake_leverage, debt - 1) + cb = (amm.active_band(), debt - 1, COLLATERAL // 2) # ================= Capture initial balances ================= @@ -439,6 +450,7 @@ def test_repay_full_from_callback( # ================= 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(): @@ -467,9 +479,7 @@ def test_repay_full_from_callback( borrowed_from_callback = ( borrowed_token_after["callback"] - borrowed_token_before["callback"] ) - borrowed_to_borrower = ( - borrowed_token_after["borrower"] - borrowed_token_before["borrower"] - ) + borrowed_from_payer = borrowed_token_after["payer"] - borrowed_token_before["payer"] collateral_to_borrower = ( collateral_token_after["borrower"] - collateral_token_before["borrower"] ) @@ -503,8 +513,8 @@ def test_repay_full_from_callback( # ================= Verify money flows ================= assert borrowed_to_controller == debt - assert borrowed_to_borrower == 1 - assert borrowed_from_callback == -(debt + 1) + 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 @@ -513,25 +523,19 @@ def test_repay_full_from_callback( assert collateral_token_after["controller"] == collateral_token_before["controller"] if different_payer: - assert borrowed_token_after["payer"] == borrowed_token_before["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_wallet_and_callback( - controller, - borrowed_token, - collateral_token, - amm, - snapshot, - fake_leverage, - different_payer, +def test_repay_full_from_xy0_and_wallet( + controller, borrowed_token, collateral_token, amm, snapshot, different_payer ): """ - Test full repayment using wallet + callback tokens. + Test full repayment using both AMM soft-liquidation (xy[0]) and wallet tokens. - Money Flow: cb.borrowed (callback) + (DEBT - cb.borrowed) (wallet) → Controller - cb.collateral (callback) → Borrower + Money Flow: xy[0] (AMM) + (DEBT - xy[0]) (wallet) → Controller + xy[1] (AMM) → Borrower """ borrower = payer = boa.env.eoa if different_payer: @@ -541,26 +545,28 @@ def test_repay_full_from_wallet_and_callback( boa.deal(collateral_token, borrower, COLLATERAL) max_approve(collateral_token, controller) - controller.create_loan(COLLATERAL, 10**18, N_BANDS) + 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 xy_before[0] == 0 and xy_before[1] > 0 + assert 0 < 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, COLLATERAL, sender=amm.address) - boa.deal(borrowed_token, fake_leverage, debt - 1) - cb = (amm.active_band(), debt - 1, COLLATERAL // 2) + boa.deal(borrowed_token, payer, debt) # ================= Capture initial balances ================= @@ -571,14 +577,12 @@ def test_repay_full_from_wallet_and_callback( 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(): + with boa.reverts(): controller.inject.repay_full( - borrower, debt, False, xy_before, cb, fake_leverage.address + borrower, debt, False, xy_before, (0, 0, 0), ZERO_ADDRESS ) controller.inject.repay_full( - borrower, debt, True, xy_before, cb, fake_leverage.address + borrower, debt, True, xy_before, (0, 0, 0), ZERO_ADDRESS ) # ================= Capture logs ================= @@ -596,24 +600,20 @@ def test_repay_full_from_wallet_and_callback( 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"] + borrowed_from_amm = borrowed_token_after["amm"] - borrowed_token_before["amm"] collateral_to_borrower = ( collateral_token_after["borrower"] - collateral_token_before["borrower"] ) - collateral_from_callbacker = ( - collateral_token_after["callback"] - collateral_token_before["callback"] - ) + collateral_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] # ================= Verify position state ================= # Withdrawn from AMM - xy = amm.get_sum_xy(borrower) + xy_after = amm.get_sum_xy(borrower) assert amm.user_shares(borrower)[1][0] == 0 - assert xy[0] == 0 - assert xy[1] == 0 + assert xy_after[0] == 0 + assert xy_after[1] == 0 # ================= Verify logs ================= @@ -627,19 +627,19 @@ def test_repay_full_from_wallet_and_callback( 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 + 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_payer == -1 - assert borrowed_from_callback == -(debt - 1) - assert borrowed_token_after["amm"] == borrowed_token_before["amm"] + 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 == COLLATERAL // 2 - assert collateral_from_callbacker == -(COLLATERAL // 2) - assert collateral_token_after["amm"] == collateral_token_before["amm"] + 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: @@ -784,7 +784,7 @@ def test_repay_full_from_xy0_and_callback( @pytest.mark.parametrize("different_payer", [True, False]) -def test_repay_full_from_wallet_and_y0_and_callback( +def test_repay_full_from_wallet_and_xy0_and_callback( controller, borrowed_token, collateral_token, From 386b3ddd175a4dc568a74b130a2017d57c273231 Mon Sep 17 00:00:00 2001 From: macket Date: Sun, 26 Oct 2025 17:02:55 +0400 Subject: [PATCH 403/413] refactor: move CALLDATA_MAX_SIZE to constants.vy --- contracts/Controller.vy | 2 +- contracts/constants.vy | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index c8388676..d5a0c688 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -72,7 +72,7 @@ CALLBACK_LIQUIDATE: constant(bytes4) = method_id( "callback_liquidate(address,uint256,uint256,uint256,bytes)", output_type=bytes4, ) -CALLDATA_MAX_SIZE: constant(uint256) = 10**4 +CALLDATA_MAX_SIZE: constant(uint256) = c.CALLDATA_MAX_SIZE MAX_LOAN_DISCOUNT: constant(uint256) = 5 * 10**17 MIN_LIQUIDATION_DISCOUNT: constant(uint256) = 10**16 diff --git a/contracts/constants.vy b/contracts/constants.vy index 40b36e68..e4b517b7 100644 --- a/contracts/constants.vy +++ b/contracts/constants.vy @@ -6,3 +6,5 @@ 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 From 71895202334313a8fd02509d8bdf0f760943878a Mon Sep 17 00:00:00 2001 From: macket Date: Sun, 26 Oct 2025 17:16:53 +0400 Subject: [PATCH 404/413] fix: CS-CRVUSD-089 (2) --- contracts/Controller.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index d5a0c688..4f1e639f 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -980,7 +980,7 @@ def _repay_partial( tkn.transfer_from(BORROWED_TOKEN, msg.sender, self, _wallet_d_debt) # ================= Recover collateral tokens (xy[1]) ================= - tkn.transfer_from(COLLATERAL_TOKEN, _callbacker, self, _cb.collateral) + tkn.transfer_from(COLLATERAL_TOKEN, _callbacker, AMM.address, _cb.collateral) d_debt: uint256 = _debt - new_debt From 4dada429c8b1c4bc5035fda0608be839166afa90 Mon Sep 17 00:00:00 2001 From: macket Date: Sun, 26 Oct 2025 17:19:27 +0400 Subject: [PATCH 405/413] test: fix callback_collateral assertions --- .../controller/test_internal_repay_partial.py | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/unitary/controller/test_internal_repay_partial.py b/tests/unitary/controller/test_internal_repay_partial.py index 8020d5f4..383df0e3 100644 --- a/tests/unitary/controller/test_internal_repay_partial.py +++ b/tests/unitary/controller/test_internal_repay_partial.py @@ -246,8 +246,8 @@ def test_repay_partial_from_callback( borrowed_from_callback = ( borrowed_token_after["callback"] - borrowed_token_before["callback"] ) - collateral_to_controller = ( - collateral_token_after["controller"] - collateral_token_before["controller"] + collateral_to_amm = ( + collateral_token_after["amm"] - collateral_token_before["amm"] ) collateral_from_callback = ( collateral_token_after["callback"] - collateral_token_before["callback"] @@ -283,9 +283,9 @@ def test_repay_partial_from_callback( assert borrowed_token_after["payer"] == borrowed_token_before["payer"] assert borrowed_token_after["amm"] == borrowed_token_before["amm"] - assert collateral_to_controller == callback_collateral + assert collateral_to_amm == callback_collateral assert collateral_from_callback == -callback_collateral - assert collateral_token_after["amm"] == collateral_token_before["amm"] + assert collateral_token_after["controller"] == collateral_token_before["controller"] assert collateral_token_after["borrower"] == collateral_token_before["borrower"] if different_payer: @@ -382,8 +382,8 @@ def test_repay_partial_from_wallet_and_callback( borrowed_from_callback = ( borrowed_token_after["callback"] - borrowed_token_before["callback"] ) - collateral_to_controller = ( - collateral_token_after["controller"] - collateral_token_before["controller"] + collateral_to_amm = ( + collateral_token_after["amm"] - collateral_token_before["amm"] ) collateral_from_callback = ( collateral_token_after["callback"] - collateral_token_before["callback"] @@ -419,9 +419,9 @@ def test_repay_partial_from_wallet_and_callback( assert borrowed_from_callback == -callback_borrowed assert borrowed_token_after["amm"] == borrowed_token_before["amm"] - assert collateral_to_controller == callback_collateral + assert collateral_to_amm == callback_collateral assert collateral_from_callback == -callback_collateral - assert collateral_token_after["amm"] == collateral_token_before["amm"] + assert collateral_token_after["controller"] == collateral_token_before["controller"] assert collateral_token_after["borrower"] == collateral_token_before["borrower"] if different_payer: @@ -934,8 +934,8 @@ def test_repay_partial_from_xy0_and_callback_underwater_shrink( borrowed_from_callback = ( borrowed_token_after["callback"] - borrowed_token_before["callback"] ) - collateral_to_controller = ( - collateral_token_after["controller"] - collateral_token_before["controller"] + collateral_to_amm = ( + collateral_token_after["amm"] - collateral_token_before["amm"] ) collateral_from_callback = ( collateral_token_after["callback"] - collateral_token_before["callback"] @@ -973,9 +973,9 @@ def test_repay_partial_from_xy0_and_callback_underwater_shrink( assert borrowed_from_callback == -callback_borrowed assert borrowed_token_after["payer"] == borrowed_token_before["payer"] - assert collateral_to_controller == callback_collateral + assert collateral_to_amm == callback_collateral assert collateral_from_callback == -callback_collateral - assert collateral_token_after["amm"] == collateral_token_before["amm"] + assert collateral_token_after["controller"] == collateral_token_before["controller"] assert collateral_token_after["borrower"] == collateral_token_before["borrower"] if different_payer: @@ -1091,8 +1091,8 @@ def test_repay_partial_from_xy0_and_wallet_and_callback_underwater_shrink( borrowed_from_callback = ( borrowed_token_after["callback"] - borrowed_token_before["callback"] ) - collateral_to_controller = ( - collateral_token_after["controller"] - collateral_token_before["controller"] + collateral_to_amm = ( + collateral_token_after["amm"] - collateral_token_before["amm"] ) collateral_from_callback = ( collateral_token_after["callback"] - collateral_token_before["callback"] @@ -1130,9 +1130,9 @@ def test_repay_partial_from_xy0_and_wallet_and_callback_underwater_shrink( assert borrowed_from_amm == -xy_before[0] assert borrowed_from_callback == -callback_borrowed - assert collateral_to_controller == callback_collateral + assert collateral_to_amm == callback_collateral assert collateral_from_callback == -callback_collateral - assert collateral_token_after["amm"] == collateral_token_before["amm"] + assert collateral_token_after["controller"] == collateral_token_before["controller"] assert collateral_token_after["borrower"] == collateral_token_before["borrower"] if different_payer: From 83715b2ab48918d5999049c7f9eb0173cff40822 Mon Sep 17 00:00:00 2001 From: macket Date: Sun, 26 Oct 2025 17:20:26 +0400 Subject: [PATCH 406/413] test: DummyCallback for testing --- contracts/testing/DummyCallback.vy | 47 ++++++++++++++++++++++++++++++ tests/conftest.py | 6 ++++ tests/utils/deployers.py | 3 ++ 3 files changed, 56 insertions(+) create mode 100644 contracts/testing/DummyCallback.vy diff --git a/contracts/testing/DummyCallback.vy b/contracts/testing/DummyCallback.vy new file mode 100644 index 00000000..5aa1c939 --- /dev/null +++ b/contracts/testing/DummyCallback.vy @@ -0,0 +1,47 @@ +# pragma version 0.4.3 +import contracts.lib.token_lib as tkn +from contracts.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/tests/conftest.py b/tests/conftest.py index 36eded9c..5f3fde0d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,7 @@ ERC20_MOCK_DEPLOYER, CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER, FAKE_LEVERAGE_DEPLOYER, + DUMMY_CALLBACK_DEPLOYER, ) from tests.utils.protocols import Llamalend @@ -80,6 +81,11 @@ def fake_leverage(controller, collateral_token, borrowed_token, price_oracle): 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 diff --git a/tests/utils/deployers.py b/tests/utils/deployers.py index d26e135d..257a1719 100644 --- a/tests/utils/deployers.py +++ b/tests/utils/deployers.py @@ -202,6 +202,9 @@ 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 ) From 4b77e8bbabb28a8fb0a1d2ef4012e3d6a3ed601a Mon Sep 17 00:00:00 2001 From: macket Date: Sun, 26 Oct 2025 18:28:23 +0400 Subject: [PATCH 407/413] refactor: test_internal_repay_partial --- .../controller/test_internal_repay_partial.py | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/unitary/controller/test_internal_repay_partial.py b/tests/unitary/controller/test_internal_repay_partial.py index 383df0e3..0cb11162 100644 --- a/tests/unitary/controller/test_internal_repay_partial.py +++ b/tests/unitary/controller/test_internal_repay_partial.py @@ -454,10 +454,10 @@ def test_repay_partial_from_wallet_underwater( # ================= Push position to underwater ================= trader = boa.env.generate_address() - boa.deal(borrowed_token, trader, debt * 10_001 // 10_000) + boa.deal(borrowed_token, trader, debt // 2) with boa.env.prank(trader): max_approve(borrowed_token, amm) - amm.exchange(0, 1, debt * 10_001 // 10_000, 0) + amm.exchange(0, 1, debt // 2, 0) # ================= Set new liquidation discount ================= @@ -472,7 +472,7 @@ def test_repay_partial_from_wallet_underwater( 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 underwater + assert 0 < xy_before[0] < debt and xy_before[1] > 0 # Position is underwater # ================= Setup payer tokens ================= @@ -615,7 +615,7 @@ def test_repay_partial_from_xy0_underwater_shrink( assert active_band_before == ticks_before[0] + 1 debt = controller.debt(borrower) xy_before = amm.get_sum_xy(borrower) - assert xy_before[0] > 0 and xy_before[1] > 0 # Position is underwater + assert 0 < xy_before[0] < debt and xy_before[1] > 0 # Position is underwater # ================= Capture initial balances ================= @@ -746,7 +746,7 @@ def test_repay_partial_from_xy0_and_wallet_underwater_shrink( assert active_band_before == ticks_before[0] debt = controller.debt(borrower) xy_before = amm.get_sum_xy(borrower) - assert xy_before[0] > 0 and xy_before[1] > 0 # Position is underwater + assert 0 < xy_before[0] < debt and xy_before[1] > 0 # Position is underwater # ================= Setup payer tokens ================= @@ -885,7 +885,7 @@ def test_repay_partial_from_xy0_and_callback_underwater_shrink( assert active_band_before == ticks_before[0] debt = controller.debt(borrower) xy_before = amm.get_sum_xy(borrower) - assert xy_before[0] > 0 and xy_before[1] > 0 # Position is underwater + assert 0 < xy_before[0] < debt and xy_before[1] > 0 # Position is underwater # ================= Setup callback tokens ================= @@ -1034,7 +1034,7 @@ def test_repay_partial_from_xy0_and_wallet_and_callback_underwater_shrink( assert active_band_before == ticks_before[0] debt = controller.debt(borrower) xy_before = amm.get_sum_xy(borrower) - assert xy_before[0] > 0 and xy_before[1] > 0 # Position is underwater + assert 0 < xy_before[0] < debt and xy_before[1] > 0 # Position is underwater # ================= Setup callback tokens ================= @@ -1175,21 +1175,20 @@ def test_repay_partial_cannot_shrink( max_approve(borrowed_token, amm) amm.exchange_dy(0, 1, amount_out, amount_in + 1) - with boa.reverts("Can't shrink"): - controller.tokens_to_shrink(borrower) - # ================= 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 xy_before[0] > 0 and xy_before[1] > 0 # Position is underwater + 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): - # _shrink == True, reverts without approval 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 From 1f565fd8fca8660d072c857a7d4120e588bad897 Mon Sep 17 00:00:00 2001 From: macket Date: Sun, 26 Oct 2025 18:28:45 +0400 Subject: [PATCH 408/413] test: complete test_repay --- tests/unitary/controller/test_repay.py | 1606 +++++++++++++++++++++++- 1 file changed, 1540 insertions(+), 66 deletions(-) diff --git a/tests/unitary/controller/test_repay.py b/tests/unitary/controller/test_repay.py index 2dd56840..8d2159d8 100644 --- a/tests/unitary/controller/test_repay.py +++ b/tests/unitary/controller/test_repay.py @@ -1,95 +1,1569 @@ +import pytest import boa +from eth_abi import encode from tests.utils import max_approve -from tests.utils.constants import MAX_INT256, MAX_UINT256 +from tests.utils.constants import MAX_UINT256, ZERO_ADDRESS -COLLATERAL = 10**21 -DEBT = 10**18 +COLLATERAL = 10**17 N_BANDS = 6 -def loan_state(controller, user): - return controller.eval( - f"(core.loan[{user}].initial_debt, core.loan[{user}].rate_mul)" +@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 -def open_loan(controller, collateral_token, borrowed_token): - borrower = boa.env.eoa - boa.deal(collateral_token, borrower, COLLATERAL) - max_approve(collateral_token, controller) - controller.create_loan(COLLATERAL, DEBT, N_BANDS) - debt, _ = loan_state(controller, borrower) - assert debt == DEBT - max_approve(borrowed_token, controller) - return 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"] -def test_default_behavior_full_repay(controller, borrowed_token, collateral_token): - borrower = open_loan(controller, collateral_token, borrowed_token) - repaid_before = controller.repaid() + 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"] - controller.repay(MAX_UINT256, borrower) + if different_payer: + assert borrowed_token_after["borrower"] == borrowed_token_before["borrower"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] - debt, _ = loan_state(controller, borrower) - assert debt == 0 - assert controller.repaid() == repaid_before + DEBT +@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. -def test_default_behavior_partial_repay(controller, borrowed_token, collateral_token): - borrower = open_loan(controller, collateral_token, borrowed_token) - partial = DEBT // 2 - repaid_before = controller.repaid() + Money Flow: callback_borrowed (callback) → Controller + callback_borrowed - debt (callback) → Borrower (excess) + COLLATERAL (AMM) → Callback → Borrower (excess) + Position is fully closed + """ + borrower = create_loan() - controller.repay(partial, borrower) + payer = borrower + if different_payer: + payer = boa.env.generate_address() - debt, _ = loan_state(controller, borrower) - assert debt == DEBT - partial - assert controller.repaid() == repaid_before + partial + # ================= 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") -def test_default_behavior_with_callback( - controller, borrowed_token, collateral_token, fake_leverage, amm -): - borrower = open_loan(controller, collateral_token, borrowed_token) - xy = amm.get_sum_xy(borrower) + # ================= Setup callback tokens ================= - boa.deal(collateral_token, fake_leverage.address, xy[1]) - boa.deal(borrowed_token, fake_leverage.address, DEBT) - hits_before = fake_leverage.callback_repay_hits() + 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() - controller.approve(fake_leverage.address, True) - controller.repay( - MAX_UINT256, - borrower, - MAX_INT256, - fake_leverage.address, - (0).to_bytes(32, "big"), + # ================= 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"] ) - debt, _ = loan_state(controller, borrower) - assert debt == 0 - assert fake_leverage.callback_repay_hits() == hits_before + 1 + # ================= 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 -def test_callback_needs_approval( - controller, borrowed_token, collateral_token, fake_leverage, amm -): - borrower = open_loan(controller, collateral_token, borrowed_token) - xy = amm.get_sum_xy(borrower) + # ================= Verify money flows ================= - boa.deal(collateral_token, fake_leverage.address, xy[1]) - boa.deal(borrowed_token, fake_leverage.address, DEBT) + 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"] - repayer = boa.env.generate_address("repayer") - boa.deal(borrowed_token, repayer, DEBT) + 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"] - with boa.reverts(dev="need approval for callback"): - controller.repay( - DEBT, - borrower, - MAX_INT256, - fake_leverage.address, - (0).to_bytes(32, "big"), - sender=repayer, - ) + 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) From 626fb2473c7849b981cedb6dc2187a448cebde73 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 27 Oct 2025 11:28:22 +0400 Subject: [PATCH 409/413] chore: natspec for min_collateral --- contracts/Controller.vy | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 4f1e639f..41feb51e 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -558,6 +558,13 @@ def max_borrowable( def min_collateral( debt: uint256, N: uint256, user: address = empty(address) ) -> uint256: + """ + @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 Minimum amount of collateral asset to provide + """ return staticcall self._view.min_collateral(debt, N, user) From 0e9b61f4dc5e000fef67b8d179f49c7200d84ffd Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 27 Oct 2025 11:30:18 +0400 Subject: [PATCH 410/413] refactor: max_borrowable --- contracts/ControllerView.vy | 9 +-------- contracts/lending/LendControllerView.vy | 15 ++------------- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/contracts/ControllerView.vy b/contracts/ControllerView.vy index 0e283020..afa1cdbe 100644 --- a/contracts/ControllerView.vy +++ b/contracts/ControllerView.vy @@ -288,7 +288,6 @@ def _max_borrowable( collateral: uint256, N: uint256, cap: uint256, - current_debt: uint256, user: address, ) -> uint256: @@ -340,13 +339,7 @@ def max_borrowable( staticcall BORROWED_TOKEN.balanceOf(CONTROLLER.address) + current_debt ) - return self._max_borrowable( - collateral, - N, - cap, - current_debt, - user, - ) + return self._max_borrowable(collateral, N, cap, user) @external diff --git a/contracts/lending/LendControllerView.vy b/contracts/lending/LendControllerView.vy index 0d32c51b..88dc6d91 100644 --- a/contracts/lending/LendControllerView.vy +++ b/contracts/lending/LendControllerView.vy @@ -86,22 +86,11 @@ def max_borrowable( 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 borrowed asset to borrow + @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._borrowed_balance(), cap) + current_debt - return core._max_borrowable( - collateral, - N, - cap, - current_debt, - user, - ) + return core._max_borrowable(collateral, N, cap, user) From 19b62e50d6be2ec96a6152b97e7b059aed67a026 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 27 Oct 2025 11:31:36 +0400 Subject: [PATCH 411/413] chore: formatting --- .../controller/test_internal_repay_partial.py | 434 ++++++++++++++---- tests/unitary/controller/test_repay.py | 426 +++++++++++++---- 2 files changed, 683 insertions(+), 177 deletions(-) diff --git a/tests/unitary/controller/test_internal_repay_partial.py b/tests/unitary/controller/test_internal_repay_partial.py index 0cb11162..e0de6d6a 100644 --- a/tests/unitary/controller/test_internal_repay_partial.py +++ b/tests/unitary/controller/test_internal_repay_partial.py @@ -98,11 +98,29 @@ def test_repay_partial_from_wallet( # _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 + borrower, + debt, + wallet_borrowed, + False, + xy_before, + (0, 0, 0), + ZERO_ADDRESS, + 2**255 - 1, + False, + ) + assert ( + controller.liquidation_discounts(borrower) == old_liquidation_discount ) - 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 + borrower, + debt, + wallet_borrowed, + True, + xy_before, + (0, 0, 0), + ZERO_ADDRESS, + 2**255 - 1, + False, ) # ================= Capture logs ================= @@ -128,7 +146,7 @@ def test_repay_partial_from_wallet( 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 + 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) @@ -145,7 +163,9 @@ def test_repay_partial_from_wallet( 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 + assert ( + repay_logs[0].collateral_decrease == 0 + ) # No collateral decrease in partial repay # ================= Verify money flows ================= @@ -166,7 +186,14 @@ def test_repay_partial_from_wallet( @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 + controller, + borrowed_token, + collateral_token, + amm, + snapshot, + admin, + fake_leverage, + different_payer, ): """ Test partial repayment using wallet + callback tokens (not underwater). @@ -202,7 +229,7 @@ def test_repay_partial_from_callback( 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) + 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) @@ -219,11 +246,29 @@ def test_repay_partial_from_callback( # _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 + borrower, + debt, + 0, + False, + xy_before, + cb, + fake_leverage.address, + 2**255 - 1, + False, + ) + assert ( + controller.liquidation_discounts(borrower) == old_liquidation_discount ) - 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 + borrower, + debt, + 0, + True, + xy_before, + cb, + fake_leverage.address, + 2**255 - 1, + False, ) # ================= Capture logs ================= @@ -246,9 +291,7 @@ def test_repay_partial_from_callback( borrowed_from_callback = ( borrowed_token_after["callback"] - borrowed_token_before["callback"] ) - collateral_to_amm = ( - collateral_token_after["amm"] - collateral_token_before["amm"] - ) + collateral_to_amm = collateral_token_after["amm"] - collateral_token_before["amm"] collateral_from_callback = ( collateral_token_after["callback"] - collateral_token_before["callback"] ) @@ -269,7 +312,9 @@ def test_repay_partial_from_callback( 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 ( + state_logs[0].collateral == callback_collateral + ) # Position still has collateral assert len(repay_logs) == 1 assert repay_logs[0].user == borrower @@ -295,7 +340,14 @@ def test_repay_partial_from_callback( @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 + controller, + borrowed_token, + collateral_token, + amm, + snapshot, + admin, + fake_leverage, + different_payer, ): """ Test partial repayment using wallet + callback tokens (not underwater). @@ -337,7 +389,7 @@ def test_repay_partial_from_wallet_and_callback( 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) + 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) @@ -354,11 +406,29 @@ def test_repay_partial_from_wallet_and_callback( # _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 + borrower, + debt, + wallet_borrowed, + False, + xy_before, + cb, + fake_leverage.address, + 2**255 - 1, + False, + ) + assert ( + controller.liquidation_discounts(borrower) == old_liquidation_discount ) - 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 + borrower, + debt, + wallet_borrowed, + True, + xy_before, + cb, + fake_leverage.address, + 2**255 - 1, + False, ) # ================= Capture logs ================= @@ -382,9 +452,7 @@ def test_repay_partial_from_wallet_and_callback( borrowed_from_callback = ( borrowed_token_after["callback"] - borrowed_token_before["callback"] ) - collateral_to_amm = ( - collateral_token_after["amm"] - collateral_token_before["amm"] - ) + collateral_to_amm = collateral_token_after["amm"] - collateral_token_before["amm"] collateral_from_callback = ( collateral_token_after["callback"] - collateral_token_before["callback"] ) @@ -405,7 +473,9 @@ def test_repay_partial_from_wallet_and_callback( 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 ( + state_logs[0].collateral == callback_collateral + ) # Position still has collateral assert len(repay_logs) == 1 assert repay_logs[0].user == borrower @@ -431,7 +501,14 @@ def test_repay_partial_from_wallet_and_callback( @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 + controller, + borrowed_token, + collateral_token, + amm, + snapshot, + admin, + fake_leverage, + different_payer, ): """ Test partial repayment from wallet when position is underwater (soft-liquidated). @@ -492,16 +569,42 @@ def test_repay_partial_from_wallet_underwater( # 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 + 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 + borrower, + debt, + wallet_borrowed, + False, + xy_before, + (0, 0, 0), + ZERO_ADDRESS, + 2**255 - 1, + False, + ) + assert ( + controller.liquidation_discounts(borrower) == old_liquidation_discount ) - 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 + borrower, + debt, + wallet_borrowed, + True, + xy_before, + (0, 0, 0), + ZERO_ADDRESS, + 2**255 - 1, + False, ) # ================= Capture logs ================= @@ -538,12 +641,16 @@ def test_repay_partial_from_wallet_underwater( 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 ( + 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 + assert ( + repay_logs[0].collateral_decrease == 0 + ) # No collateral decrease in underwater partial repay # ================= Verify money flows ================= @@ -564,7 +671,14 @@ def test_repay_partial_from_wallet_underwater( @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 + controller, + borrowed_token, + collateral_token, + amm, + snapshot, + admin, + fake_leverage, + different_payer, ): """ Test partial repayment from wallet when position is underwater (soft-liquidated). @@ -593,7 +707,7 @@ def test_repay_partial_from_xy0_underwater_shrink( 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_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): @@ -628,11 +742,29 @@ def test_repay_partial_from_xy0_underwater_shrink( # _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 + borrower, + debt, + 0, + False, + xy_before, + (0, 0, 0), + ZERO_ADDRESS, + 2**255 - 1, + True, + ) + assert ( + controller.liquidation_discounts(borrower) == old_liquidation_discount ) - 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 + borrower, + debt, + 0, + True, + xy_before, + (0, 0, 0), + ZERO_ADDRESS, + 2**255 - 1, + True, ) # ================= Capture logs ================= @@ -661,9 +793,15 @@ def test_repay_partial_from_xy0_underwater_shrink( # 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 ( + 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 ================= @@ -671,12 +809,16 @@ def test_repay_partial_from_xy0_underwater_shrink( 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 ( + 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 + assert ( + repay_logs[0].collateral_decrease == 0 + ) # No collateral decrease in underwater partial repay # ================= Verify money flows ================= @@ -697,7 +839,14 @@ def test_repay_partial_from_xy0_underwater_shrink( @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 + controller, + borrowed_token, + collateral_token, + amm, + snapshot, + admin, + fake_leverage, + different_payer, ): """ Test partial repayment from wallet when position is underwater (soft-liquidated). @@ -724,10 +873,10 @@ def test_repay_partial_from_xy0_and_wallet_underwater_shrink( 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)) + 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) + 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 @@ -765,11 +914,29 @@ def test_repay_partial_from_xy0_and_wallet_underwater_shrink( # _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 + 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 ) - 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 + borrower, + debt, + tokens_to_shrink, + True, + xy_before, + (0, 0, 0), + ZERO_ADDRESS, + 2**255 - 1, + True, ) # ================= Capture logs ================= @@ -797,12 +964,17 @@ def test_repay_partial_from_xy0_and_wallet_underwater_shrink( 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 ( + 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 ================= @@ -810,12 +982,16 @@ def test_repay_partial_from_xy0_and_wallet_underwater_shrink( 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 ( + 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 + assert ( + repay_logs[0].collateral_decrease == 0 + ) # No collateral decrease in underwater partial repay # ================= Verify money flows ================= @@ -836,7 +1012,14 @@ def test_repay_partial_from_xy0_and_wallet_underwater_shrink( @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 + controller, + borrowed_token, + collateral_token, + amm, + snapshot, + admin, + fake_leverage, + different_payer, ): """ Test partial repayment from callback when position is underwater (soft-liquidated). @@ -863,10 +1046,10 @@ def test_repay_partial_from_xy0_and_callback_underwater_shrink( 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)) + 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) + 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 @@ -891,7 +1074,7 @@ def test_repay_partial_from_xy0_and_callback_underwater_shrink( 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) + 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) @@ -907,11 +1090,29 @@ def test_repay_partial_from_xy0_and_callback_underwater_shrink( # _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 + borrower, + debt, + 0, + False, + xy_before, + cb, + fake_leverage.address, + 2**255 - 1, + True, + ) + assert ( + controller.liquidation_discounts(borrower) == old_liquidation_discount ) - 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 + borrower, + debt, + 0, + True, + xy_before, + cb, + fake_leverage.address, + 2**255 - 1, + True, ) # ================= Capture logs ================= @@ -934,9 +1135,7 @@ def test_repay_partial_from_xy0_and_callback_underwater_shrink( borrowed_from_callback = ( borrowed_token_after["callback"] - borrowed_token_before["callback"] ) - collateral_to_amm = ( - collateral_token_after["amm"] - collateral_token_before["amm"] - ) + collateral_to_amm = collateral_token_after["amm"] - collateral_token_before["amm"] collateral_from_callback = ( collateral_token_after["callback"] - collateral_token_before["callback"] ) @@ -949,9 +1148,15 @@ def test_repay_partial_from_xy0_and_callback_underwater_shrink( # 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 ( + 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 ================= @@ -959,7 +1164,9 @@ def test_repay_partial_from_xy0_and_callback_underwater_shrink( 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 ( + state_logs[0].collateral == callback_collateral + ) # Collateral amount after partial repay assert len(repay_logs) == 1 assert repay_logs[0].user == borrower @@ -985,7 +1192,14 @@ def test_repay_partial_from_xy0_and_callback_underwater_shrink( @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 + controller, + borrowed_token, + collateral_token, + amm, + snapshot, + admin, + fake_leverage, + different_payer, ): """ Test partial repayment from callback when position is underwater (soft-liquidated). @@ -1012,10 +1226,10 @@ def test_repay_partial_from_xy0_and_wallet_and_callback_underwater_shrink( 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)) + 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) + 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 @@ -1040,7 +1254,7 @@ def test_repay_partial_from_xy0_and_wallet_and_callback_underwater_shrink( 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) + 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) @@ -1063,11 +1277,29 @@ def test_repay_partial_from_xy0_and_wallet_and_callback_underwater_shrink( # _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 + borrower, + debt, + wallet_borrowed, + False, + xy_before, + cb, + fake_leverage.address, + 2**255 - 1, + True, + ) + assert ( + controller.liquidation_discounts(borrower) == old_liquidation_discount ) - 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 + borrower, + debt, + wallet_borrowed, + True, + xy_before, + cb, + fake_leverage.address, + 2**255 - 1, + True, ) # ================= Capture logs ================= @@ -1091,9 +1323,7 @@ def test_repay_partial_from_xy0_and_wallet_and_callback_underwater_shrink( borrowed_from_callback = ( borrowed_token_after["callback"] - borrowed_token_before["callback"] ) - collateral_to_amm = ( - collateral_token_after["amm"] - collateral_token_before["amm"] - ) + collateral_to_amm = collateral_token_after["amm"] - collateral_token_before["amm"] collateral_from_callback = ( collateral_token_after["callback"] - collateral_token_before["callback"] ) @@ -1106,21 +1336,34 @@ def test_repay_partial_from_xy0_and_wallet_and_callback_underwater_shrink( # 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 ( + 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 ( + 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].loan_decrease + == xy_before[0] + callback_borrowed + wallet_borrowed + ) assert repay_logs[0].collateral_decrease == xy_before[1] - callback_collateral # ================= Verify money flows ================= @@ -1142,7 +1385,14 @@ def test_repay_partial_from_xy0_and_wallet_and_callback_underwater_shrink( @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 + 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 @@ -1167,8 +1417,12 @@ def test_repay_partial_cannot_shrink( 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_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): @@ -1191,5 +1445,13 @@ def test_repay_partial_cannot_shrink( 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 + 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 index 8d2159d8..376da3d0 100644 --- a/tests/unitary/controller/test_repay.py +++ b/tests/unitary/controller/test_repay.py @@ -31,7 +31,12 @@ 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], + [ + borrowed_token.address, + collateral_token.address, + borrowed_amount, + collateral_amount, + ], ) return fn @@ -52,7 +57,9 @@ def fn(token, borrower, payer): @pytest.mark.parametrize("different_payer", [True, False]) -def test_full_repay_from_wallet(controller, borrowed_token, collateral_token, create_loan, snapshot, different_payer): +def test_full_repay_from_wallet( + controller, borrowed_token, collateral_token, create_loan, snapshot, different_payer +): """ Test full repayment using wallet tokens. @@ -132,7 +139,17 @@ def test_full_repay_from_wallet(controller, borrowed_token, collateral_token, cr @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): +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. @@ -172,9 +189,13 @@ def test_full_repay_from_callback(controller, amm, borrowed_token, collateral_to 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.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) + controller.repay( + 0, borrower, amm.active_band(), dummy_callback, calldata, sender=payer + ) # ================= Capture final balances ================= @@ -192,9 +213,7 @@ def test_full_repay_from_callback(controller, amm, borrowed_token, collateral_to borrowed_from_callback = ( borrowed_token_after["callback"] - borrowed_token_before["callback"] ) - collateral_from_amm = ( - collateral_token_after["amm"] - collateral_token_before["amm"] - ) + collateral_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] collateral_to_borrower = ( collateral_token_after["borrower"] - collateral_token_before["borrower"] ) @@ -231,7 +250,15 @@ def test_full_repay_from_callback(controller, amm, borrowed_token, collateral_to @pytest.mark.parametrize("different_payer", [True, False]) -def test_full_repay_from_xy0(controller, amm, borrowed_token, collateral_token, create_loan, snapshot, different_payer): +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). @@ -292,9 +319,7 @@ def test_full_repay_from_xy0(controller, amm, borrowed_token, collateral_token, borrowed_to_borrower = ( borrowed_token_after["borrower"] - borrowed_token_before["borrower"] ) - collateral_from_amm = ( - collateral_token_after["amm"] - collateral_token_before["amm"] - ) + collateral_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] collateral_to_borrower = ( collateral_token_after["borrower"] - collateral_token_before["borrower"] ) @@ -327,7 +352,17 @@ def test_full_repay_from_xy0(controller, amm, borrowed_token, collateral_token, @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): +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. @@ -372,9 +407,18 @@ def test_full_repay_from_wallet_and_callback(controller, amm, borrowed_token, co 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.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) + controller.repay( + MAX_UINT256, borrower, amm.active_band(), dummy_callback, calldata, sender=payer + ) # ================= Capture final balances ================= @@ -390,9 +434,7 @@ def test_full_repay_from_wallet_and_callback(controller, amm, borrowed_token, co 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_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] collateral_to_borrower = ( collateral_token_after["borrower"] - collateral_token_before["borrower"] ) @@ -429,7 +471,15 @@ def test_full_repay_from_wallet_and_callback(controller, amm, borrowed_token, co @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): +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. @@ -493,9 +543,7 @@ def test_full_repay_from_xy0_and_wallet(controller, amm, borrowed_token, collate ) 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_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] collateral_to_borrower = ( collateral_token_after["borrower"] - collateral_token_before["borrower"] ) @@ -528,7 +576,17 @@ def test_full_repay_from_xy0_and_wallet(controller, amm, borrowed_token, collate @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): +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. @@ -579,9 +637,13 @@ def test_full_repay_from_xy0_and_callback(controller, amm, borrowed_token, colla 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.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) + controller.repay( + 0, borrower, amm.active_band(), dummy_callback, calldata, sender=payer + ) # ================= Capture final balances ================= @@ -600,9 +662,7 @@ def test_full_repay_from_xy0_and_callback(controller, amm, borrowed_token, colla borrowed_to_borrower = ( borrowed_token_after["borrower"] - borrowed_token_before["borrower"] ) - collateral_from_amm = ( - collateral_token_after["amm"] - collateral_token_before["amm"] - ) + collateral_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] collateral_to_borrower = ( collateral_token_after["borrower"] - collateral_token_before["borrower"] ) @@ -639,7 +699,17 @@ def test_full_repay_from_xy0_and_callback(controller, amm, borrowed_token, colla @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): +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. @@ -695,9 +765,18 @@ def test_full_repay_from_xy0_and_wallet_and_callback(controller, amm, borrowed_t 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.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) + controller.repay( + MAX_UINT256, borrower, amm.active_band(), dummy_callback, calldata, sender=payer + ) # ================= Capture final balances ================= @@ -714,9 +793,7 @@ def test_full_repay_from_xy0_and_wallet_and_callback(controller, amm, borrowed_t 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_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] collateral_to_borrower = ( collateral_token_after["borrower"] - collateral_token_before["borrower"] ) @@ -753,7 +830,9 @@ def test_full_repay_from_xy0_and_wallet_and_callback(controller, amm, borrowed_t @pytest.mark.parametrize("different_payer", [True, False]) -def test_partial_repay_from_wallet(controller, borrowed_token, collateral_token, create_loan, snapshot, different_payer): +def test_partial_repay_from_wallet( + controller, borrowed_token, collateral_token, create_loan, snapshot, different_payer +): """ Test partial repayment using wallet tokens. @@ -829,7 +908,17 @@ def test_partial_repay_from_wallet(controller, borrowed_token, collateral_token, @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): +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. @@ -868,9 +957,13 @@ def test_partial_repay_from_callback(controller, amm, borrowed_token, collateral 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.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) + controller.repay( + 0, borrower, amm.active_band(), dummy_callback, calldata, sender=payer + ) # ================= Capture final balances ================= @@ -885,9 +978,7 @@ def test_partial_repay_from_callback(controller, amm, borrowed_token, collateral borrowed_from_callback = ( borrowed_token_after["callback"] - borrowed_token_before["callback"] ) - collateral_from_amm = ( - collateral_token_after["amm"] - collateral_token_before["amm"] - ) + collateral_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] collateral_to_callback = ( collateral_token_after["callback"] - collateral_token_before["callback"] ) @@ -921,7 +1012,17 @@ def test_partial_repay_from_callback(controller, amm, borrowed_token, collateral @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): +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. @@ -965,12 +1066,25 @@ def test_partial_repay_from_wallet_and_callback(controller, amm, borrowed_token, # ================= 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.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) + controller.repay( + wallet_borrowed, + borrower, + amm.active_band(), + dummy_callback, + calldata, + sender=payer, + ) # ================= Capture final balances ================= @@ -986,9 +1100,7 @@ def test_partial_repay_from_wallet_and_callback(controller, amm, borrowed_token, borrowed_from_callback = ( borrowed_token_after["callback"] - borrowed_token_before["callback"] ) - collateral_from_amm = ( - collateral_token_after["amm"] - collateral_token_before["amm"] - ) + collateral_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] collateral_to_callback = ( collateral_token_after["callback"] - collateral_token_before["callback"] ) @@ -1001,7 +1113,9 @@ def test_partial_repay_from_wallet_and_callback(controller, amm, borrowed_token, 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 ( + controller.eval("core.repaid") == repaid + wallet_borrowed + callback_borrowed + ) assert dummy_callback.callback_repay_hits() == repay_hits + 1 # ================= Verify money flows ================= @@ -1022,7 +1136,15 @@ def test_partial_repay_from_wallet_and_callback(controller, amm, borrowed_token, @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): +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). @@ -1110,7 +1232,15 @@ def test_partial_repay_from_wallet_underwater(controller, amm, borrowed_token, c @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): +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. @@ -1132,7 +1262,7 @@ def test_partial_repay_from_xy0_underwater_shrink(controller, amm, borrowed_toke 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_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): @@ -1158,9 +1288,13 @@ def test_partial_repay_from_xy0_underwater_shrink(controller, amm, borrowed_toke if different_payer: # Repay reverts without approval with boa.reverts(): - controller.repay(0, borrower, amm.active_band(), ZERO_ADDRESS, b'', True, sender=payer) + 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) + controller.repay( + 0, borrower, amm.active_band(), ZERO_ADDRESS, b"", True, sender=payer + ) # ================= Capture final balances ================= @@ -1202,7 +1336,15 @@ def test_partial_repay_from_xy0_underwater_shrink(controller, amm, borrowed_toke @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): +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. @@ -1222,10 +1364,10 @@ def test_partial_repay_from_xy0_and_wallet_underwater_shrink(controller, amm, bo 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)) + 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) + 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 @@ -1255,9 +1397,25 @@ def test_partial_repay_from_xy0_and_wallet_underwater_shrink(controller, amm, bo 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.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) + controller.repay( + wallet_borrowed, + borrower, + amm.active_band(), + ZERO_ADDRESS, + b"", + True, + sender=payer, + ) # ================= Capture final balances ================= @@ -1277,10 +1435,17 @@ def test_partial_repay_from_xy0_and_wallet_underwater_shrink(controller, amm, bo 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[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 + 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 ================= @@ -1300,7 +1465,17 @@ def test_partial_repay_from_xy0_and_wallet_underwater_shrink(controller, amm, bo @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): +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. @@ -1321,10 +1496,10 @@ def test_partial_repay_from_xy0_and_callback_underwater_shrink(controller, amm, 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)) + 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) + 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 @@ -1355,9 +1530,19 @@ def test_partial_repay_from_xy0_and_callback_underwater_shrink(controller, amm, if different_payer: # Repay reverts without approval with boa.reverts(): - controller.repay(0, borrower, amm.active_band(), dummy_callback, calldata, True, sender=payer) + 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) + controller.repay( + 0, borrower, amm.active_band(), dummy_callback, calldata, True, sender=payer + ) # ================= Capture final balances ================= @@ -1373,9 +1558,7 @@ def test_partial_repay_from_xy0_and_callback_underwater_shrink(controller, amm, borrowed_from_callback = ( borrowed_token_after["callback"] - borrowed_token_before["callback"] ) - collateral_from_amm = ( - collateral_token_after["amm"] - collateral_token_before["amm"] - ) + collateral_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] collateral_to_callback = ( collateral_token_after["callback"] - collateral_token_before["callback"] ) @@ -1385,10 +1568,17 @@ def test_partial_repay_from_xy0_and_callback_underwater_shrink(controller, amm, 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[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 ( + 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 ================= @@ -1409,7 +1599,17 @@ def test_partial_repay_from_xy0_and_callback_underwater_shrink(controller, amm, @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): +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. @@ -1430,10 +1630,10 @@ def test_partial_repay_from_xy0_and_wallet_and_callback_underwater_shrink(contro 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)) + 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) + 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 @@ -1455,7 +1655,9 @@ def test_partial_repay_from_xy0_and_wallet_and_callback_underwater_shrink(contro # ================= Setup callback tokens ================= - callback_borrowed = tokens_to_shrink - wallet_borrowed # Callback provides 1/6 of debt + 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) @@ -1471,9 +1673,25 @@ def test_partial_repay_from_xy0_and_wallet_and_callback_underwater_shrink(contro 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.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) + controller.repay( + wallet_borrowed, + borrower, + amm.active_band(), + dummy_callback, + calldata, + True, + sender=payer, + ) # ================= Capture final balances ================= @@ -1490,9 +1708,7 @@ def test_partial_repay_from_xy0_and_wallet_and_callback_underwater_shrink(contro borrowed_from_callback = ( borrowed_token_after["callback"] - borrowed_token_before["callback"] ) - collateral_from_amm = ( - collateral_token_after["amm"] - collateral_token_before["amm"] - ) + collateral_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] collateral_to_callback = ( collateral_token_after["callback"] - collateral_token_before["callback"] ) @@ -1502,15 +1718,27 @@ def test_partial_repay_from_xy0_and_wallet_and_callback_underwater_shrink(contro 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[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 ( + 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_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 @@ -1526,7 +1754,15 @@ def test_partial_repay_from_xy0_and_wallet_and_callback_underwater_shrink(contro @pytest.mark.parametrize("different_payer", [True, False]) -def test_partial_repay_cannot_shrink(controller, amm, borrowed_token, collateral_token, create_loan, snapshot, different_payer): +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. """ @@ -1544,8 +1780,12 @@ def test_partial_repay_cannot_shrink(controller, amm, borrowed_token, collateral 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_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): @@ -1558,7 +1798,9 @@ def test_partial_repay_cannot_shrink(controller, amm, borrowed_token, collateral 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 + assert ( + 0 < user_state_before[1] < debt and user_state_before[0] > 0 + ) # Position is underwater # ================= Verify shrink reverts ================= @@ -1566,4 +1808,6 @@ def test_partial_repay_cannot_shrink(controller, amm, borrowed_token, collateral controller.tokens_to_shrink(borrower) with boa.reverts("Can't shrink"): - controller.repay(0, borrower, amm.active_band(), ZERO_ADDRESS, b'', True, sender=payer) + controller.repay( + 0, borrower, amm.active_band(), ZERO_ADDRESS, b"", True, sender=payer + ) From 39e700389709f5f9c93c41827649795ffe15b049 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 27 Oct 2025 13:12:49 +0400 Subject: [PATCH 412/413] refactor: markets getter in LendController --- contracts/interfaces/ILlamalendController.vyi | 3 +- contracts/interfaces/IVault.vyi | 4 +- contracts/lending/LendController.vy | 4 +- contracts/lending/LendingFactory.vy | 58 +++++++------------ 4 files changed, 27 insertions(+), 42 deletions(-) diff --git a/contracts/interfaces/ILlamalendController.vyi b/contracts/interfaces/ILlamalendController.vyi index f351e9eb..7cd53204 100644 --- a/contracts/interfaces/ILlamalendController.vyi +++ b/contracts/interfaces/ILlamalendController.vyi @@ -1,4 +1,3 @@ -from contracts.interfaces import IVault from contracts.interfaces import IERC20 from contracts.interfaces import ILMGauge from contracts.interfaces import IAMM @@ -325,7 +324,7 @@ event SetBorrowCap: @view @external -def vault() -> IVault: +def vault() -> address: ... diff --git a/contracts/interfaces/IVault.vyi b/contracts/interfaces/IVault.vyi index 177a1e81..24d65610 100644 --- a/contracts/interfaces/IVault.vyi +++ b/contracts/interfaces/IVault.vyi @@ -1,6 +1,6 @@ from contracts.interfaces import IERC20 from contracts.interfaces import IAMM -# from contracts.interfaces import ILlamalendController as IController +from contracts.interfaces import ILlamalendController as IController @view def collateral_token() -> IERC20: @@ -28,7 +28,7 @@ def initialize(amm: IAMM, controller: address, borrowed_token: IERC20, collatera @view @external -def controller() -> address: +def controller() -> IController: ... diff --git a/contracts/lending/LendController.vy b/contracts/lending/LendController.vy index 02d7a32c..2a9dc31f 100644 --- a/contracts/lending/LendController.vy +++ b/contracts/lending/LendController.vy @@ -88,11 +88,11 @@ VAULT: immutable(IVault) # https://github.com/vyperlang/vyper/issues/4721 @external @view -def vault() -> IVault: +def vault() -> address: """ @notice Address of the vault """ - return VAULT + return VAULT.address # cumulative amount of assets collected by admin diff --git a/contracts/lending/LendingFactory.vy b/contracts/lending/LendingFactory.vy index c36321d7..f0f8fd7a 100644 --- a/contracts/lending/LendingFactory.vy +++ b/contracts/lending/LendingFactory.vy @@ -29,6 +29,15 @@ exports: ( ownable.transfer_ownership, ) +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 @@ -188,43 +197,20 @@ def create( @external @view @reentrant -def controllers(_n: uint256) -> IController: - return IController(staticcall self.vaults[_n].controller()) - - -@external -@view -@reentrant -def amms(_n: uint256) -> IAMM: - return staticcall self.vaults[_n].amm() +def markets(_n: uint256) -> Market: + vault: IVault = self.vaults[_n] + controller: IController = staticcall vault.controller() + amm: IAMM = staticcall vault.amm() - -@external -@view -@reentrant -def borrowed_tokens(_n: uint256) -> IERC20: - return staticcall self.vaults[_n].borrowed_token() - - -@external -@view -@reentrant -def collateral_tokens(_n: uint256) -> IERC20: - return staticcall self.vaults[_n].collateral_token() - - -@external -@view -@reentrant -def price_oracles(_n: uint256) -> IPriceOracle: - return staticcall (staticcall self.vaults[_n].amm()).price_oracle_contract() - - -@external -@view -@reentrant -def monetary_policies(_n: uint256) -> IMonetaryPolicy: - return staticcall IController(staticcall self.vaults[_n].controller()).monetary_policy() + 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 From 104cd635d335bd16a010ecd1820a3c48a3087934 Mon Sep 17 00:00:00 2001 From: macket Date: Mon, 27 Oct 2025 13:13:28 +0400 Subject: [PATCH 413/413] chore: formatting --- contracts/lending/LendingFactory.vy | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/lending/LendingFactory.vy b/contracts/lending/LendingFactory.vy index f0f8fd7a..e900665f 100644 --- a/contracts/lending/LendingFactory.vy +++ b/contracts/lending/LendingFactory.vy @@ -29,6 +29,7 @@ exports: ( ownable.transfer_ownership, ) + struct Market: vault: IVault controller: IController @@ -38,6 +39,7 @@ struct Market: 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 @@ -209,7 +211,7 @@ def markets(_n: uint256) -> Market: 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() + monetary_policy=staticcall controller.monetary_policy(), )