"""Reserve, TVL and interest data reading of pools.
- Reads Aave reserves metrics data on-chain from a certain timestamp/block number
- Relies on a lot of undocumented Aave v3 source code to pull out the data
- Based on
- https://github.com/aave/aave-utilities
- https://github.com/aave/aave-utilities/tree/master/packages/contract-helpers/src/v3-UiPoolDataProvider-contract
- https://github.com/aave/aave-v3-periphery/blob/master/contracts/misc/UiPoolDataProviderV3.sol
- https://github.com/aave/aave-ui/blob/f34f1cfc4fa6c1128b31eaa70b37b5b2109d1dc5/src/libs/pool-data-provider/hooks/use-v2-protocol-data-with-rpc.tsx#L62
- https://github.com/aave/aave-utilities/blob/664e92b5c7710e8060d4dcac5d6c0ebb48bb069f/packages/math-utils/src/formatters/user/index.ts#L95
- https://github.com/aave/aave-utilities/blob/664e92b5c7710e8060d4dcac5d6c0ebb48bb069f/packages/math-utils/src/formatters/reserve/index.ts#L310
"""
from dataclasses import dataclass
from typing import Dict, List, Tuple, TypeAlias, TypedDict
from web3 import Web3
from web3._utils.abi import named_tree
from web3.contract import Contract
from eth_defi.aave_v3.deployer import AaveDeployer
from eth_defi.event_reader.conversion import convert_jsonrpc_value_to_int
#:
#: Aave contracts we need to know about to read reserves data.
#:
#: Chain id -> labelled address mapping from Aave documentation
#: https://docs.aave.com/developers/deployed-contracts/v3-mainnet
#:
#:
_addresses = {
# Ethereum
1: {
"PoolAddressProvider": "0x2f39d218133AFaB8F2B819B1066c7E434Ad94E9e",
"UiPoolDataProviderV3": "0x91c0eA31b49B69Ea18607702c5d9aC360bf3dE7d",
},
# Polygon
137: {
"PoolAddressProvider": "0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb",
"UiPoolDataProviderV3": "0xC69728f11E9E6127733751c8410432913123acf1",
},
# Binance Smarrt Chain mainnet (not supported by AAVE v3)
# 56: {
# "PoolAddressProvider": "",
# "UiPoolDataProviderV3": "",
# },
# Avalanche C-chain
43114: {
"PoolAddressProvider": "0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb",
"UiPoolDataProviderV3": "0xF71DBe0FAEF1473ffC607d4c555dfF0aEaDb878d",
},
# Arbitrum One
42161: {
"PoolAddressProvider": "0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb",
"UiPoolDataProviderV3": "0x145dE30c929a065582da84Cf96F88460dB9745A7",
},
# Ethereum Classic (not supported by AAVE v3)
# 61: {
# "PoolAddressProvider": "",
# "UiPoolDataProviderV3": "",
# },
# Ganache test chain (not supported by AAVE v3)
# 1337: {
# "PoolAddressProvider": "",
# "UiPoolDataProviderV3": "",
# },
}
[docs]@dataclass
class HelperContracts:
"""Contracts needed to resolve reserve info on Aave v3."""
#: Which EVM chain
chain_id: int
#: See
#: - https://github.com/aave/aave-v3-periphery/blob/master/contracts/misc/interfaces/IUiPoolDataProviderV3.sol
#: - https://github.com/aave/aave-v3-periphery/blob/master/contracts/misc/UiPoolDataProviderV3.sol
ui_pool_data_provider: Contract
#: See https://github.com/aave/aave-v3-core/blob/27a6d5c83560694210849d4abf09a09dec8da388/contracts/interfaces/IPoolAddressesProvider.sol#L5
pool_addresses_provider: Contract
#: Quick and dirty "any" Solidity value hack
StructVal: TypeAlias = str | bool | int
[docs]class AggregatedReserveData(TypedDict):
"""Rough mapping of AggreatedReserveData in Aave v3 Solidity source code.
.. note ::
This data is not useful until JavaScript based formatters from
aave-utilities are applied. As writing of this, these formatters are only
available as undocumented JavaScript code in this repository.
`See the repository for more information <https://github.com/aave/aave-utilities>`__.
"""
underlyingAsset: StructVal
name: StructVal
symbol: StructVal
decimals: StructVal
baseLTVasCollateral: StructVal
reserveLiquidationThreshold: StructVal
reserveLiquidationBonus: StructVal
reserveFactor: StructVal
usageAsCollateralEnabled: StructVal
borrowingEnabled: StructVal
stableBorrowRateEnabled: StructVal
isActive: StructVal
isFrozen: StructVal
liquidityIndex: StructVal
variableBorrowIndex: StructVal
liquidityRate: StructVal
variableBorrowRate: StructVal
stableBorrowRate: StructVal
lastUpdateTimestamp: StructVal
aTokenAddress: StructVal
stableDebtTokenAddress: StructVal
variableDebtTokenAddress: StructVal
interestRateStrategyAddress: StructVal
availableLiquidity: StructVal
totalPrincipalStableDebt: StructVal
averageStableRate: StructVal
stableDebtLastUpdateTimestamp: StructVal
totalScaledVariableDebt: StructVal
priceInMarketReferenceCurrency: StructVal
priceOracle: StructVal
variableRateSlope1: StructVal
variableRateSlope2: StructVal
stableRateSlope1: StructVal
stableRateSlope2: StructVal
baseStableBorrowRate: StructVal
baseVariableBorrowRate: StructVal
optimalUsageRatio: StructVal
isPaused: StructVal
isSiloedBorrowing: StructVal
accruedToTreasury: StructVal
unbacked: StructVal
isolationModeTotalDebt: StructVal
flashLoanEnabled: StructVal
debtCeiling: StructVal
debtCeilingDecimals: StructVal
eModeCategoryId: StructVal
borrowCap: StructVal
supplyCap: StructVal
eModeLtv: StructVal
eModeLiquidationThreshold: StructVal
eModeLiquidationBonus: StructVal
eModePriceSource: StructVal
eModeLabel: StructVal
borrowableInIsolation: StructVal
[docs]class BaseCurrencyInfo(TypedDict):
"""Rough mapping of BaseCurrencyInfo in Aave v3 Solidity source code.
Aave internally gets this data from ChainLink feed.
"""
marketReferenceCurrencyUnit: StructVal
marketReferenceCurrencyPriceInUsd: StructVal
networkBaseTokenPriceInUsd: StructVal
networkBaseTokenPriceDecimals: StructVal
[docs]class JSONSerialisableReserveData(TypedDict):
"""JSON friendly way to store Aave v3 protocol reserve status.
All ints are converted to JavaScript to avoid BigInt issues.
.. note ::
This data is not useful until JavaScript based formatters from
aave-utilities are applied. As writing of this, these formatters are only
available as undocumented JavaScript code in this repository.
`See the repository for more information <https://github.com/aave/aave-utilities>`__.
"""
#: Which chain this was one
chain_id: int
#: When this fetch was performed
block_number: int
#: When this fetch was performed
block_hash: str
#: Unix timestamp when this fetch was performed
timestamp: int
#: ERC-20 address -> reserve info mapping.
#:
#: All addresses are lowercased 0x strings
reserves: Dict[str, AggregatedReserveData]
#: Chainlink currency conversion multipliers
#: needed by aave-utilities to convert values to USD/useful/human-readable
#:
base_currency_info: BaseCurrencyInfo
[docs]def get_helper_contracts(web3: Web3) -> HelperContracts:
"""Get helper contracts need to read Aave reserve data.
:raise AaveContractsNotConfigured:
If we do not have labelled addresses for this chain
"""
chain_id = web3.eth.chain_id
if chain_id not in _addresses:
raise AaveContractsNotConfigured(f"Chain {chain_id} does not have Aave v3 addresses configured")
deployer = AaveDeployer()
Contract = deployer.get_contract(web3, "UiPoolDataProviderV3.json")
Contract.decode_tuples = False
ui_pool_data_provider = Contract(Web3.to_checksum_address(_addresses[chain_id]["UiPoolDataProviderV3"]))
Contract = deployer.get_contract(web3, "PoolAddressesProvider.json")
Contract.decode_tuples = False
pool_addresses_provider = Contract(Web3.to_checksum_address(_addresses[chain_id]["PoolAddressProvider"]))
return HelperContracts(
chain_id,
ui_pool_data_provider,
pool_addresses_provider,
)
[docs]def fetch_reserves(
contracts: HelperContracts,
block_identifier=None,
) -> List[str]:
"""Enumerate available reserves.
https://github.com/aave/aave-v3-core/blob/27a6d5c83560694210849d4abf09a09dec8da388/contracts/interfaces/IPool.sol#L603
:return:
Returns the list of the underlying assets of all the initialized reserves.
List of ERC-20 addresses.
"""
reserve_list = contracts.ui_pool_data_provider.functions.getReservesList(contracts.pool_addresses_provider.address).call(block_identifier=block_identifier)
return reserve_list
[docs]def fetch_reserve_data(
contracts: HelperContracts,
block_identifier=None,
) -> Tuple[List[AggregatedReserveData], BaseCurrencyInfo]:
"""Fetch data for all reserves.
:param contracts:
Helper contracts needed to pull the data
:return:
List of data of all reserves, currency data from ChainLink used to convert this info for display
"""
func = contracts.ui_pool_data_provider.functions.getReservesData(contracts.pool_addresses_provider.address)
aggregated_reserve_data, base_currency_info = func.call(block_identifier=block_identifier)
# Manually decode anonymous tuples to named struct fields
outputs = func.abi["outputs"]
AggregatedReserveData = outputs[0]["components"]
BaseCurrencyInfo = outputs[1]["components"]
aggregated_reserve_data_decoded = [named_tree(AggregatedReserveData, a) for a in aggregated_reserve_data]
base_currency_info_decoded = named_tree(BaseCurrencyInfo, base_currency_info)
return aggregated_reserve_data_decoded, base_currency_info_decoded
[docs]def fetch_aave_reserves_snapshot(web3: Web3, block_identifier=None) -> JSONSerialisableReserveData:
"""Get a snapshot of all data of Aave reserves at a certain point of time.
See :py:class:`JSONSerialisableReserveData` for notes on how to transform the output
to useful and/or human readable.
Example:
.. code-block:: python
# Read Polygon Aave v3 reserves data at current block
snapshot = fetch_aave_reserves_snapshot(web3)
Example output:
.. code-block:: text
{'block_number': 46092890,
'block_hash': '0x66b91e13e66978632d7687fa37d61994a092194dd83ab800c4b3fbbfbbc4b882',
'timestamp': 1691574096,
'reserves': {'0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063': {'underlyingAsset': '0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063',
'name': '(PoS) Dai Stablecoin',
'symbol': 'DAI',
'decimals': '18',
'baseLTVasCollateral': '7600',
'reserveLiquidationThreshold': '8100',
'reserveLiquidationBonus': '10500',
'reserveFactor': '1000',
'usageAsCollateralEnabled': True,
'borrowingEnabled': True,
'stableBorrowRateEnabled': True,
'isActive': True,
'isFrozen': False,
'liquidityIndex': '1022026858597482843618393800',
'variableBorrowIndex': '1039320957656647363864994430',
'liquidityRate': '28850861922310792585422606',
'variableBorrowRate': '39579583454495318816309720',
'stableBorrowRate': '54947447931811914852038715',
'lastUpdateTimestamp': '1691574072',
'aTokenAddress': '0x82E64f49Ed5EC1bC6e43DAD4FC8Af9bb3A2312EE',
'stableDebtTokenAddress': '0xd94112B5B62d53C9402e7A60289c6810dEF1dC9B',
'variableDebtTokenAddress': '0x8619d80FB0141ba7F184CbF22fd724116D9f7ffC',
'interestRateStrategyAddress': '0xA9F3C3caE095527061e6d270DBE163693e6fda9D',
'availableLiquidity': '1889483036044495898670614',
'totalPrincipalStableDebt': '411830124610128093102375',
'averageStableRate': '55554322387136738659305167',
'stableDebtLastUpdateTimestamp': '1691573968',
'totalScaledVariableDebt': '6509001421349391268535081',
'priceInMarketReferenceCurrency': '99970000',
'priceOracle': '0x4746DeC9e833A82EC7C2C1356372CcF2cfcD2F3D',
'variableRateSlope1': '40000000000000000000000000',
'variableRateSlope2': '750000000000000000000000000',
'stableRateSlope1': '5000000000000000000000000',
'stableRateSlope2': '750000000000000000000000000',
'baseStableBorrowRate': '50000000000000000000000000',
'baseVariableBorrowRate': '0',
'optimalUsageRatio': '800000000000000000000000000',
'isPaused': False,
'isSiloedBorrowing': False,
'accruedToTreasury': '142442743829638527556',
'unbacked': '0',
'isolationModeTotalDebt': '0',
'flashLoanEnabled': True,
'debtCeiling': '0',
'debtCeilingDecimals': '2',
'eModeCategoryId': '1',
'borrowCap': '30000000',
'supplyCap': '45000000',
'eModeLtv': '9300',
'eModeLiquidationThreshold': '9500',
'eModeLiquidationBonus': '10100',
'eModePriceSource': '0x0000000000000000000000000000000000000000',
'eModeLabel': 'Stablecoins',
'borrowableInIsolation': True},
'0x53E0bca35eC356BD5ddDFebbD1Fc0fD03FaBad39': {'underlyingAsset': '0x53E0bca35eC356BD5ddDFebbD1Fc0fD03FaBad39',
'name': 'ChainLink Token',
'symbol': 'LINK',
'decimals': '18',
:param web3:
Web3 connection for some of the chain for which we have Aave v3 contract data available.
:param block_identifier:
Block when to take the snapshot.
If not given, use the latest block.
:return:
JSON friendly dict where all ints are converted to string
"""
helpers = get_helper_contracts(web3)
if block_identifier is None:
block_identifier = web3.eth.block_number
block = web3.eth.get_block(block_identifier)
aggregated_reserve_data, base_currency_info = fetch_reserve_data(helpers, block_identifier=block_identifier)
reserve_map = {a["underlyingAsset"].lower(): _to_json_friendly(a) for a in aggregated_reserve_data}
return JSONSerialisableReserveData(
chain_id=helpers.chain_id,
block_number=convert_jsonrpc_value_to_int(block["number"]),
block_hash=block["hash"].hex(),
timestamp=convert_jsonrpc_value_to_int(block["timestamp"]),
reserves=reserve_map,
base_currency_info=base_currency_info,
)
def _to_json_friendly(d: dict) -> dict:
"""Deal with JavaScript lacking of good number types"""
result = {}
for k, v in d.items():
if type(v) == int:
v = str(v)
result[k] = v
return result