Repository: rotki/rotki

Issue:
Price calculation for curve pools is wrong in some cases
## Problem Definition

It seems that the calculation for the price of a curve pool is wrong in some cases. Presumably the result of `find_curve_pool_price` is what is wrong.

An example is this: `Curve.fi Factory Crypto Pool: YFI/ETH`. Address of the pool is `0x29059568bb40344487d62f7450e78b8e6c74e0e5`. The price seen for the amount of LP tokens seems to be roughly half.

Seen in 1.32.1.

## Task

Figure out and fix it.



Discussion:

Resolving patch:
diff --git a/rotkehlchen/inquirer.py b/rotkehlchen/inquirer.py
--- a/rotkehlchen/inquirer.py
+++ b/rotkehlchen/inquirer.py
@@ -757,12 +757,16 @@ def find_curve_pool_price(
     ) -> Price | None:
         """
         1. Obtain the pool for this token
-        2. Obtain prices for assets in pool
-        3. Obtain the virtual price for share and the balances of each
-        token in the pool
-        4. Calc the price for a share
+        2. Obtain total supply of lp tokens
+        3. Obtain value (in USD) for all assets in the pool
+        4. Calculate the price for an LP token
+
+        logic source: https://medium.com/coinmonks/the-joys-of-valuing-curve-lp-tokens-4e4a148eaeb9
 
         Returns the price of 1 LP token from the pool
+
+        May raise:
+        - RemoteError
         """
         ethereum = self.get_evm_manager(chain_id=ChainID.ETHEREUM)
         ethereum.assure_curve_cache_is_queried_and_decoder_updated()  # type:ignore  # ethereum is an EthereumManager here
@@ -779,8 +783,7 @@ def find_curve_pool_price(
             pool_tokens_addresses = read_curve_pool_tokens(cursor=cursor, pool_address=pool_address)  # noqa: E501
 
         tokens: list[EvmToken] = []
-        # Translate addresses to tokens
-        try:
+        try:  # Translate addresses to tokens
             for token_address_str in pool_tokens_addresses:
                 token_address = string_to_evm_address(token_address_str)
                 if token_address == '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE':
@@ -803,14 +806,24 @@ def find_curve_pool_price(
                 return None
             prices.append(price)
 
-        # Query virtual price of LP share and balances in the pool for each token
+        # Query total supply of the LP token
+        contract = EvmContract(
+            address=lp_token.evm_address,
+            abi=ethereum.node_inquirer.contracts.abi('ERC20_TOKEN'),
+            deployed_block=0,
+        )
+        total_supply = contract.call(
+            node_inquirer=ethereum.node_inquirer,
+            method_name='totalSupply',
+        )
+
+        # Query balances for each token in the pool
         contract = EvmContract(
             address=pool_address,
             abi=ethereum.node_inquirer.contracts.abi('CURVE_POOL'),
             deployed_block=0,
         )
-        calls = [(pool_address, contract.encode(method_name='get_virtual_price'))]
-        calls += [
+        calls = [
             (pool_address, contract.encode(method_name='balances', arguments=[i]))
             for i in range(len(tokens))
         ]
@@ -832,14 +845,8 @@ def find_curve_pool_price(
             return None
         # Deserialize information obtained in the multicall execution
         data = []
-        # https://github.com/PyCQA/pylint/issues/4739
-        virtual_price_decoded = contract.decode(output[0][1], 'get_virtual_price')
-        if not _check_curve_contract_call(virtual_price_decoded):
-            log.debug(f'Failed to decode get_virtual_price while finding curve price. {output}')
-            return None
-        data.append(FVal(virtual_price_decoded[0]))
         for i, token in enumerate(tokens):
-            amount_decoded = contract.decode(output[i + 1][1], 'balances', arguments=[i])
+            amount_decoded = contract.decode(output[i][1], 'balances', arguments=[i])
             if not _check_curve_contract_call(amount_decoded):
                 log.debug(f'Failed to decode balances {i} while finding curve price. {output}')
                 return None
@@ -849,24 +856,21 @@ def find_curve_pool_price(
             data.append(normalized_amount)
 
         # Prices and data should verify this relation for the following operations
-        if len(prices) != len(data) - 1:
+        if len(prices) != len(data):
             log.debug(
                 f'Length of prices {len(prices)} does not match len of data {len(data)} '
                 f'while querying curve pool price.',
             )
             return None
         # Total number of assets price in the pool
-        total_assets_price = sum(map(operator.mul, data[1:], prices))
-        if total_assets_price == 0:
+        total_assets_value = sum(map(operator.mul, data, prices))
+        if total_assets_value == 0:
             log.error(
                 f'Curve pool price returned unexpected data {data} that lead to a zero price.',
             )
             return None
 
-        # Calculate weight of each asset as the proportion of tokens value
-        weights = (data[x + 1] * prices[x] / total_assets_price for x in range(len(tokens)))
-        assets_price = FVal(sum(map(operator.mul, weights, prices)))
-        return (assets_price * FVal(data[0])) / (10 ** lp_token.get_decimals())
+        return (total_assets_value / total_supply) * (10 ** lp_token.get_decimals())
 
     def find_yearn_price(
             self,


Test patch:
diff --git a/rotkehlchen/tests/unit/test_inquirer.py b/rotkehlchen/tests/unit/test_inquirer.py
--- a/rotkehlchen/tests/unit/test_inquirer.py
+++ b/rotkehlchen/tests/unit/test_inquirer.py
@@ -395,42 +395,37 @@ def test_find_velodrome_v2_lp_token_price(inquirer, optimism_manager):
     assert price is not None
 
 
-@pytest.mark.vcr()
+@pytest.mark.vcr(filter_query_parameters=['apikey'])
 @pytest.mark.parametrize('use_clean_caching_directory', [True])
 @pytest.mark.parametrize('should_mock_current_price_queries', [False])
 def test_find_curve_lp_token_price(inquirer_defi, ethereum_manager):
     with GlobalDBHandler().conn.write_ctx() as write_cursor:  # querying curve lp token price normally triggers curve cache query. Set all query ts to now, so it does not happen.  # noqa: E501
         write_cursor.execute('UPDATE general_cache SET last_queried_ts=? WHERE key=?', (ts_now(), 'CURVE_LP_TOKENS'))  # noqa: E501
-
-    lp_token_address = '0xA3D87FffcE63B53E0d54fAa1cc983B7eB0b74A9c'
-    pool_address = '0xc5424B857f758E906013F3555Dad202e4bdB4567'
-    identifier = ethaddress_to_identifier(lp_token_address)
     inquirer_defi.inject_evm_managers([(ChainID.ETHEREUM, ethereum_manager)])
-    with GlobalDBHandler().conn.write_ctx() as write_cursor:
-        globaldb_set_general_cache_values(
-            write_cursor=write_cursor,
-            key_parts=(CacheType.CURVE_LP_TOKENS,),
-            values=[lp_token_address],
-        )
-        globaldb_set_unique_cache_value(
-            write_cursor=write_cursor,
-            key_parts=(CacheType.CURVE_POOL_ADDRESS, lp_token_address),
-            value=pool_address,
-        )
-        globaldb_set_general_cache_values(
-            write_cursor=write_cursor,
-            key_parts=(CacheType.CURVE_POOL_TOKENS, pool_address, '0'),
-            values=['0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'],
-        )
-        globaldb_set_general_cache_values(
-            write_cursor=write_cursor,
-            key_parts=(CacheType.CURVE_POOL_TOKENS, pool_address, '1'),
-            values=['0x5e74C9036fb86BD7eCdcb084a0673EFc32eA31cb'],
-        )
-    price = inquirer_defi.find_curve_pool_price(EvmToken(identifier))
-    assert price is not None
-    # Check that the protocol is correctly caught by the inquirer
-    assert price == inquirer_defi.find_usd_price(EvmToken(identifier))
+
+    for lp_token_address, pool_address, expected_price in (
+            # Curve.fi ETH/sETH (eCRV)
+            ('0xA3D87FffcE63B53E0d54fAa1cc983B7eB0b74A9c', '0xc5424B857f758E906013F3555Dad202e4bdB4567', FVal('3037.58672247')),  # noqa: E501
+            # Curve.fi Factory Crypto Pool: YFI/ETH (YFIETH-f)
+            ('0x29059568bB40344487d62f7450E78b8E6C74e0e5', '0xC26b89A667578ec7b3f11b2F98d6Fd15C07C54ba', FVal('10129.93803885')),  # noqa: E501
+
+    ):
+        identifier = ethaddress_to_identifier(lp_token_address)
+        with GlobalDBHandler().conn.write_ctx() as write_cursor:
+            globaldb_set_general_cache_values(
+                write_cursor=write_cursor,
+                key_parts=(CacheType.CURVE_LP_TOKENS,),
+                values=[lp_token_address],
+            )
+            globaldb_set_unique_cache_value(
+                write_cursor=write_cursor,
+                key_parts=(CacheType.CURVE_POOL_ADDRESS, lp_token_address),
+                value=pool_address,
+            )
+        price = inquirer_defi.find_curve_pool_price(EvmToken(identifier))
+        assert price.is_close(expected_price)
+        # Check that the protocol is correctly caught by the inquirer
+        assert price == inquirer_defi.find_usd_price(EvmToken(identifier))
 
 
 @pytest.mark.parametrize('use_clean_caching_directory', [True])