-
Notifications
You must be signed in to change notification settings - Fork 2
feat: ✨ Add strETH & osETH Pricing changes
#130
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: fix/audit-issues-sherlock
Are you sure you want to change the base?
feat: ✨ Add strETH & osETH Pricing changes
#130
Conversation
…tq-oracle into feat/streth-asset-adapter-sher
…Labs/tq-oracle into audit/sherlock-combined-RC1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request introduces support for pricing additional assets (strETH and osETH) and refactors the StakeWise adapter to improve modularity and configurability. The changes enable the oracle to handle more complex asset scenarios while maintaining backward compatibility through a new additional_asset_support toggle.
Key Changes:
- Added
StrETHAdapterfor fetching strETH positions across subvaults using multicall - Added native osETH pricing via OsTokenVaultController in the ETH price adapter
- Refactored StakeWise adapter to remove escrow handling, add configurable exit queue scanning, and support additional addresses
- Added
tvl_in_base_assetfield to OracleReport for explicit TVL tracking - Introduced
additional_asset_supporttoggle to enable/disable additional asset features - Moved subvault address fetching to a shared utility function in
abi.py
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
src/tq_oracle/settings.py |
Added additional_asset_support toggle and StakeWise configuration options (exit max lookback, extra addresses, skip exit queue scan); added property accessors for strETH constants |
src/tq_oracle/constants.py |
Added OSETH to network assets, strETH/multicall/collector addresses, ostoken_vault_controller to StakeWise addresses, and DEFAULT_ADDITIONAL_ASSETS mapping |
src/tq_oracle/report/generator.py |
Added tvl_in_base_asset parameter and field to OracleReport |
src/tq_oracle/pipeline/report.py |
Passed tvl_in_base_asset from context to generate_report |
src/tq_oracle/pipeline/assets.py |
Added default StakeWise and StrETH adapters; refactored to use shared fetch_subvault_addresses utility |
src/tq_oracle/adapters/price_adapters/eth.py |
Added osETH pricing via OsTokenVaultController.convertToAssets() when additional_asset_support is enabled |
src/tq_oracle/adapters/price_adapters/cow_swap.py |
Added osETH to skipped assets to prevent CowSwap from pricing it |
src/tq_oracle/adapters/asset_adapters/streth.py |
New adapter for fetching strETH positions using CoreVaultsCollector and multicall |
src/tq_oracle/adapters/asset_adapters/stakewise.py |
Removed escrow handling; added skip_exit_queue_scan, extra_addresses support, and fetch_all_assets method; improved exit queue calculation to use left_tickets |
src/tq_oracle/adapters/asset_adapters/idle_balances.py |
Refactored to support default and extra additional assets with proper deduplication and tvl_only flagging |
src/tq_oracle/adapters/asset_adapters/__init__.py |
Registered StrETHAdapter in ADAPTER_REGISTRY |
src/tq_oracle/abi.py |
Added loaders for new ABIs (multicall, core vaults collector, ostoken controller); added fetch_subvault_addresses utility function |
src/tq_oracle/abis/*.json |
Added ABI files for OsTokenVaultController, Multicall, and CoreVaultsCollector |
tests/**/*.py |
Updated tests to include tvl_in_base_asset parameter, added osETH pricing/skipping tests, refactored StakeWise tests for new return signature, added additional asset configuration tests |
Comments suppressed due to low confidence (1)
src/tq_oracle/abi.py:120
- The docstring's Args section mentions
vault_addressandrpc_urlas parameters, but the function signature only takessettings: OracleSettings. The docstring should be updated to reflect that the function only accepts asettingsparameter.
def get_oracle_address_from_vault(settings: OracleSettings) -> ChecksumAddress:
"""Fetch the oracle address from the vault contract.
Args:
vault_address: The vault contract address
rpc_url: RPC endpoint URL
Returns:
The oracle contract address from the vault
Raises:
ConnectionError: If RPC connection fails
ValueError: If contract call fails
"""
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| from __future__ import annotations | ||
|
|
||
| from tq_oracle.constants import STAKEWISE_EXIT_MAX_LOOKBACK_BLOCKS |
Copilot
AI
Nov 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Import statement should be placed after from __future__ import annotations and before the standard library imports for proper organization. The import from tq_oracle.constants should be moved after the standard library imports (os, enum, pathlib, typing).
| from typing import TYPE_CHECKING | ||
|
|
||
| from web3 import Web3 | ||
| from web3.eth import Contract | ||
| import backoff | ||
| import random | ||
| from web3.exceptions import ProviderConnectionError | ||
| from ...logger import get_logger | ||
| from ...abi import load_multicall_abi, load_vault_abi, load_core_vaults_collector_abi | ||
| from .base import AssetData, BaseAssetAdapter | ||
| from eth_abi.abi import decode, encode |
Copilot
AI
Nov 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Import statements are not properly organized. Standard library imports (asyncio, random, typing) should come first, then third-party imports (web3, backoff, eth_abi), then local imports (logger, abi, base). Additionally, imports should be grouped and sorted alphabetically within each group.
| from typing import TYPE_CHECKING | |
| from web3 import Web3 | |
| from web3.eth import Contract | |
| import backoff | |
| import random | |
| from web3.exceptions import ProviderConnectionError | |
| from ...logger import get_logger | |
| from ...abi import load_multicall_abi, load_vault_abi, load_core_vaults_collector_abi | |
| from .base import AssetData, BaseAssetAdapter | |
| from eth_abi.abi import decode, encode | |
| import random | |
| from typing import TYPE_CHECKING | |
| import backoff | |
| from eth_abi.abi import decode, encode | |
| from web3 import Web3 | |
| from web3.eth import Contract | |
| from web3.exceptions import ProviderConnectionError | |
| from ...abi import load_core_vaults_collector_abi, load_multicall_abi, load_vault_abi | |
| from ...logger import get_logger | |
| from .base import AssetData, BaseAssetAdapter |
| self._rpc_jitter = getattr(self.config, "rpc_jitter", 0.10) # seconds | ||
|
|
||
| @backoff.on_exception( | ||
| backoff.expo, (ProviderConnectionError), max_time=30, jitter=backoff.full_jitter |
Copilot
AI
Nov 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Redundant tuple wrapping around a single exception type. The parentheses around (ProviderConnectionError) should be removed as they don't create a tuple with a single element.
| backoff.expo, (ProviderConnectionError), max_time=30, jitter=backoff.full_jitter | |
| backoff.expo, ProviderConnectionError, max_time=30, jitter=backoff.full_jitter |
| if should_run_streth: | ||
| streth_adapter = StrETHAdapter(s) | ||
| asset_fetch_tasks.append( | ||
| ("streth_vault_chain", streth_adapter._fetch_assets(subvault_addresses)) |
Copilot
AI
Nov 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The method _fetch_assets is called directly on the adapter, but it appears to be a private method (prefix _). Consider using the public fetch_all_assets() method instead, or if _fetch_assets needs to be called with specific subvault addresses, it should be documented as part of the public API.
| ("streth_vault_chain", streth_adapter._fetch_assets(subvault_addresses)) | |
| ("streth_vault_chain", streth_adapter.fetch_all_assets()) |
| class StrETHAdapter(BaseAssetAdapter): | ||
| streth_address: str | ||
| streth_redemption_asset: str | ||
| core_vaults_collector: str | ||
| multicall: Contract | ||
| w3: Web3 | ||
|
|
||
| def __init__(self, config: OracleSettings): | ||
| """Initialize the adapter. | ||
| Args: | ||
| config: Oracle configuration | ||
| """ | ||
| super().__init__(config) | ||
|
|
||
| self.w3 = Web3(Web3.HTTPProvider(config.vault_rpc_required)) | ||
| self.block_number = config.block_number_required | ||
|
|
||
| self.vault_address = Web3.to_checksum_address(config.vault_address_required) | ||
| self.streth_address = Web3.to_checksum_address(config.streth) | ||
| self.core_vaults_collector = Web3.to_checksum_address( | ||
| config.core_vaults_collector | ||
| ) | ||
| self.streth_redemption_asset = Web3.to_checksum_address( | ||
| config.streth_redemption_asset | ||
| ) | ||
|
|
||
| self.multicall: Contract = self.w3.eth.contract( | ||
| address=Web3.to_checksum_address(config.multicall), abi=load_multicall_abi() | ||
| ) | ||
|
|
||
| self._rpc_sem = asyncio.Semaphore(getattr(self.config, "max_calls", 5)) | ||
| self._rpc_delay = getattr(self.config, "rpc_delay", 0.15) # seconds | ||
| self._rpc_jitter = getattr(self.config, "rpc_jitter", 0.10) # seconds | ||
|
|
||
| @backoff.on_exception( | ||
| backoff.expo, (ProviderConnectionError), max_time=30, jitter=backoff.full_jitter | ||
| ) | ||
| async def _rpc(self, fn, *args, **kwargs): | ||
| """Throttle + backoff a single RPC.""" | ||
| async with self._rpc_sem: | ||
| try: | ||
| return await asyncio.to_thread(fn, *args, **kwargs) | ||
| finally: | ||
| delay = self._rpc_delay + random.random() * self._rpc_jitter | ||
| if delay > 0: | ||
| await asyncio.sleep(delay) | ||
|
|
||
| @property | ||
| def adapter_name(self) -> str: | ||
| return "streth" | ||
|
|
||
| async def _fetch_assets(self, subvault_addresses: list[str]) -> list[AssetData]: | ||
| """Fetch strETH positions for the given subvaults on the configured chain. | ||
| Args: | ||
| subvault_addresses: List of subvaults to query | ||
| Returns: | ||
| List of AssetData objects containing asset addresses and balances | ||
| """ | ||
| collector = self.w3.eth.contract( | ||
| Web3.to_checksum_address(self.core_vaults_collector), | ||
| abi=load_core_vaults_collector_abi(), | ||
| ) | ||
|
|
||
| calls = [] | ||
| for subvault in subvault_addresses: | ||
| calls.append( | ||
| [ | ||
| Web3.to_checksum_address(collector.address), | ||
| collector.encode_abi( | ||
| "getDistributions", | ||
| args=[ | ||
| Web3.to_checksum_address(subvault), | ||
| encode( | ||
| ["address", "address"], | ||
| [self.streth_address, self.streth_redemption_asset], | ||
| ), | ||
| [], | ||
| ], | ||
| ), | ||
| ] | ||
| ) | ||
|
|
||
| call_results = ( | ||
| await self._rpc( | ||
| self.multicall.functions.aggregate(calls).call, | ||
| block_identifier=self.block_number, | ||
| ) | ||
| )[1] | ||
|
|
||
| cumulative_amounts: dict[str, int] = {} | ||
| for call_result in call_results: | ||
| balances = list( | ||
| decode(["(address,int256,string,address)[]"], call_result)[0] | ||
| ) | ||
| for asset, amount, _, _ in balances: | ||
| if amount != 0: | ||
| cumulative_amounts[asset] = ( | ||
| cumulative_amounts.get(asset, 0) + amount | ||
| ) | ||
|
|
||
| result: list[AssetData] = [] | ||
| for asset, amount in cumulative_amounts.items(): | ||
| result.append(AssetData(Web3.to_checksum_address(asset), amount)) | ||
| return result | ||
|
|
||
| async def fetch_assets(self, subvault_address: str) -> list[AssetData]: | ||
| return await self._fetch_assets([subvault_address]) | ||
|
|
||
| async def fetch_all_assets(self) -> list[AssetData]: | ||
| """Fetch strETH positions for all subvaults of the vault on the configured chain. | ||
| Returns: | ||
| List of AssetData objects containing asset addresses and balances | ||
| """ | ||
| vault_contract: Contract = self.w3.eth.contract( | ||
| address=self.vault_address, abi=load_vault_abi() | ||
| ) | ||
| count: int = await self._rpc( | ||
| vault_contract.functions.subvaults().call, | ||
| block_identifier=self.block_number, | ||
| ) | ||
|
|
||
| calls = [ | ||
| [ | ||
| vault_contract.address, | ||
| vault_contract.encode_abi("subvaultAt", args=[index]), | ||
| ] | ||
| for index in range(count) | ||
| ] | ||
| responses = ( | ||
| await self._rpc( | ||
| self.multicall.functions.aggregate(calls).call, | ||
| block_identifier=self.block_number, | ||
| ) | ||
| )[1] | ||
| subvaults = [decode(["address"], response)[0] for response in responses] | ||
| return await self._fetch_assets(subvaults) |
Copilot
AI
Nov 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The newly introduced StrETHAdapter class lacks test coverage. Given that other adapters in the codebase have comprehensive test files (e.g., test_stakewise_adapter.py, tests/adapters/asset_adapters/test_idle_balances.py), the StrETH adapter should have similar test coverage to ensure its functionality is properly validated.
| logger.info( | ||
| f"StakeWise exit queue scan start for {context.address}, this might take some time..." | ||
| logger.warning( | ||
| f"StakeWise exit queue scan start for address:{user} StakewiseVault: {context.address} from block {min_block}, this might take some time..." |
Copilot
AI
Nov 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The log message uses an f-string with inconsistent formatting. The address and vault information should use the same format as other log statements (using %s placeholders instead of f-string interpolation). This should be: logger.warning("StakeWise exit queue scan start for address:%s StakewiseVault: %s from block %d, this might take some time...", user, context.address, min_block)
| f"StakeWise exit queue scan start for address:{user} StakewiseVault: {context.address} from block {min_block}, this might take some time..." | |
| "StakeWise exit queue scan start for address:%s StakewiseVault: %s from block %d, this might take some time...", | |
| user, | |
| context.address, | |
| min_block, |
This pull request introduces several enhancements and refactorings to the asset adapter system, especially around the handling of additional assets, subvault address fetching, and the StakeWise adapter. The changes improve modularity, configurability, and robustness when collecting asset data from vaults and subvaults.
Contains:
feat/use-native-oseth-price-sherlockstrETH(StrategyETH Flexible Vaults) Asset Adapter #126feat/streth-asset-adapter-sher