Skip to content

Conversation

@timbrinded
Copy link
Collaborator

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:

Copilot AI review requested due to automatic review settings November 28, 2025 09:46
@timbrinded timbrinded added audit sherlock Sherlock auditor changes labels Nov 28, 2025
Copilot finished reviewing on behalf of timbrinded November 28, 2025 09:50
Copy link
Contributor

Copilot AI left a 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 StrETHAdapter for 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_asset field to OracleReport for explicit TVL tracking
  • Introduced additional_asset_support toggle 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_address and rpc_url as parameters, but the function signature only takes settings: OracleSettings. The docstring should be updated to reflect that the function only accepts a settings parameter.
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
Copy link

Copilot AI Nov 28, 2025

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).

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +14
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
Copy link

Copilot AI Nov 28, 2025

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
self._rpc_jitter = getattr(self.config, "rpc_jitter", 0.10) # seconds

@backoff.on_exception(
backoff.expo, (ProviderConnectionError), max_time=30, jitter=backoff.full_jitter
Copy link

Copilot AI Nov 28, 2025

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.

Suggested change
backoff.expo, (ProviderConnectionError), max_time=30, jitter=backoff.full_jitter
backoff.expo, ProviderConnectionError, max_time=30, jitter=backoff.full_jitter

Copilot uses AI. Check for mistakes.
if should_run_streth:
streth_adapter = StrETHAdapter(s)
asset_fetch_tasks.append(
("streth_vault_chain", streth_adapter._fetch_assets(subvault_addresses))
Copy link

Copilot AI Nov 28, 2025

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.

Suggested change
("streth_vault_chain", streth_adapter._fetch_assets(subvault_addresses))
("streth_vault_chain", streth_adapter.fetch_all_assets())

Copilot uses AI. Check for mistakes.
Comment on lines 22 to 161
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)
Copy link

Copilot AI Nov 28, 2025

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.

Copilot uses AI. Check for mistakes.
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..."
Copy link

Copilot AI Nov 28, 2025

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)

Suggested change
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,

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

audit sherlock Sherlock auditor changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants