Skip to content

Commit cbd67c6

Browse files
timbrindedarmoking32matias-gonzCopilot
authored
audit: 🛠️ Combined FYEO Audit fixes RC2 (#132)
* WIP * prettified * types * fixes * docs * feat: ✨ Implement conflict detection for tvl_only asset flags (#86) Co-authored-by: matias-gonz <[email protected]> * feat: 🛡️ Improve confidence checks in pyth (#82) * feat: confidence check fails if confidence ratio exceeds maximum in PythAdapter * add pyth cfg example --------- Co-authored-by: matias-gonz <[email protected]> * fix: use block_number and block_identifier in all rpc calls (#81) Co-authored-by: matias-gonz <[email protected]> * refactor: 🛠️ Ameliorate precision loss in `fetch_native_price()` (#84) * refactor: 🛠️ Change fetch_native_price return type to string to prevent float precision loss * add test --------- Co-authored-by: matias-gonz <[email protected]> * fix: 🐛 Handle exceptions when fetching asset balances and items in IdleBalancesAdapter (#75) Co-authored-by: matias-gonz <[email protected]> * refactor: ♻️ Simplify vault address resolution logic in StakeWiseAdapter (#76) * refactor: ♻️ Simplify vault address resolution logic in StakeWiseAdapter * fmt * typo --------- Co-authored-by: matias-gonz <[email protected]> * chore: move import outside of loop (#83) Co-authored-by: matias-gonz <[email protected]> * chore: remove unused units module (#78) Co-authored-by: matias-gonz <[email protected]> * feat: raise error when IdleBalancesAdapter.fetch_all_assets fails to fetch assets from a vault (#79) Co-authored-by: matias-gonz <[email protected]> * feat: raise error on adapter failures in _process_adapter_results (#80) Co-authored-by: matias-gonz <[email protected]> * feat: ✨ add price tolerance validation (#87) * feat: 🩹 add cowswap timeout (#88) * chore: 🛡️ Add overflow guard to pyth scale function (#77) * feat: add overflow check for PythAdapter._scale_to18 * fix: update overflow check in PythAdapter to use uint224 max and improve error handling * change guard --------- Co-authored-by: matias-gonz <[email protected]> * refactor: ♻️ remove dead param (#92) vault_address no longer required for pre_check function call * fix: 🥅 Enforce impl of required methods (#93) * fix: 🐛 Rewrite deviation formula (#94) * feat: ✨ added cli option for gate dangerous options (#89) * feat: ✨ Normalize asset addresses to EIP-55 checksummed format in aggregation (#85) Co-authored-by: matias-gonz <[email protected]> * refactor: ♻️ remove deprecated check (#90) checks were implemented in separate files, this is just stubs * fix: 🔧 Added timeout to Safe API to prevent hang (#91) * fix: 🔧 add timeouts added timeout to safe external api so it doesn't hang indefinitely * Update src/tq_oracle/report/publisher.py Co-authored-by: Copilot <[email protected]> * fix --------- Co-authored-by: Copilot <[email protected]> * Audit/fyeo 5 correct scaling dps (#109) * fix: 🐛 fix cowswap scaling * refactor: ♻️ Tidy code * Revert "Audit/fyeo 5 correct scaling dps (#109)" This reverts commit b5268e5. * fixes * fixes * feat: ✨ add TVL to dry-run report * pr comment fixes * refactor: ♻️ use native osETH price (#127) * refactor: ♻️ use native osETH price * fix: remove claimed tickets from totals * remote escrow * refactor: tidy * pr comments * Fix async blocking * clarify tvl-only comments * fix: 🐛 Fix stETH Audit issues (#131) * skip streth on non-mainnet * add abi error handling * add web3 conn check * zero addr refactor * fix tests * lint * Add settings docs --------- Co-authored-by: Andrey Borzenkov <[email protected]> Co-authored-by: matias-gonz <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 551c407 commit cbd67c6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+2736
-496
lines changed

SETTINGS.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# TQ Oracle Settings & CLI
2+
3+
## Overview
4+
5+
The oracle must be configured to correctly determine the TVL, and therefore share prices, for a flexible-vault.
6+
7+
Where possible, values are gathered from blockchain calls but discovery is not always possible due to the lack of a registry which determines what is connected to each subvault. As such we require a fully operational vault to have a configuration which fully maps to the assets and integrations it is expected to support ahead of time.
8+
9+
## Types & Precedence
10+
11+
| Order | Source | Notes |
12+
| --- | --- | --- |
13+
| 1 | CLI args (`tq-oracle` entrypoint) | Highest precedence |
14+
| 2 | Env vars (`TQ_ORACLE_*`, `.env` loaded) | Secrets must be env-only |
15+
| 3 | TOML config | `--config/-c` or `TQ_ORACLE_CONFIG`; else `./tq-oracle.toml` then `~/.config/tq-oracle/config.toml`; accepts top-level or `[tq_oracle]` table. |
16+
17+
> [!IMPORTANT]
18+
> Settings `private_key` and `safe_txn_srvc_api_key` are rejected when added to the config file. They should instead be provided by environment variable.
19+
20+
## Global Settings
21+
22+
| Setting / Arg | CLI flag | Env var | TOML key | Default | Effect in run |
23+
| --- | --- | --- | --- | --- | --- |
24+
| vault_address | positional | TQ_ORACLE_VAULT_ADDRESS | vault_address | required | Target vault; required before pipeline runs. |
25+
| config path | `--config,-c` | TQ_ORACLE_CONFIG | n/a | auto-discovery | Selects TOML file loaded at lowest precedence. |
26+
| show_config | `--show-config` | n/a | n/a | false | Print effective settings (secrets redacted) then exit. |
27+
| network | `--network,-n` | TQ_ORACLE_NETWORK | network | mainnet | Picks asset list, RPC default, oracle_helper default (mainnet/sepolia/base). |
28+
| vault_rpc | `--vault-rpc` | TQ_ORACLE_VAULT_RPC | vault_rpc | per-network HTTP RPC | RPC used everywhere; auto-set from `network` if missing; tracked via `using_default_rpc`. |
29+
| oracle_helper_address | none | TQ_ORACLE_ORACLE_HELPER_ADDRESS | oracle_helper_address | per-network constant | Address used in final price derivation; set from network if absent. |
30+
| block_number | `--block-number` | TQ_ORACLE_BLOCK_NUMBER | block_number | latest at runtime | Snapshot height for all calls; fetched if not provided. |
31+
| eth_mainnet_rpc | none | TQ_ORACLE_ETH_MAINNET_RPC | eth_mainnet_rpc | null | Reserved for cross-chain lookups when vault not on mainnet. |
32+
| dry_run | `--dry-run/--no-dry-run` | TQ_ORACLE_DRY_RUN | dry_run | true | When false, Safe broadcast mode; requires `safe_address` + `private_key`. |
33+
| safe_address | none | TQ_ORACLE_SAFE_ADDRESS | safe_address | null | Gnosis Safe used for submission; mandatory when `dry_run` is false. |
34+
| private_key | none | TQ_ORACLE_PRIVATE_KEY | private_key | null | Signer for Safe tx; env/CLI only; config file rejected. |
35+
| safe_txn_srvc_api_key | none | TQ_ORACLE_SAFE_TXN_SRVC_API_KEY | safe_txn_srvc_api_key | null | Optional Safe Transaction Service API key; env-only; config file rejected. |
36+
| allow_dangerous | `--allow-dangerous/--disallow-dangerous` | TQ_ORACLE_ALLOW_DANGEROUS | allow_dangerous | false | Must be true to permit `skip_subvault_existence_check` in `subvault_adapters`. |
37+
| ignore_empty_vault | `--ignore-empty-vault/--require-nonempty-vault` | TQ_ORACLE_IGNORE_EMPTY_VAULT | ignore_empty_vault | false | If true, zero-asset OracleHelper errors return zeros instead of failing. |
38+
| ignore_timeout_check | `--ignore-timeout-check/--enforce-timeout-check` | TQ_ORACLE_IGNORE_TIMEOUT_CHECK | ignore_timeout_check | false | Pre-check allows submission even if oracle timeout has not elapsed. |
39+
| ignore_active_proposal_check | `--ignore-active-proposal-check/--enforce-active-proposal-check` | TQ_ORACLE_IGNORE_ACTIVE_PROPOSAL_CHECK | ignore_active_proposal_check | false | Pre-check allows submission even if Safe has active submitReport proposals. |
40+
| pre_check_retries | none | TQ_ORACLE_PRE_CHECK_RETRIES | pre_check_retries | 3 | Retry count for preflight checks. |
41+
| pre_check_timeout | none | TQ_ORACLE_PRE_CHECK_TIMEOUT | pre_check_timeout | 12.0s | Backoff interval between preflight retries. |
42+
| log_level | `--log-level` | TQ_ORACLE_LOG_LEVEL | log_level | INFO | Logger level; accepts TRACE/DEBUG/INFO/WARNING/ERROR/CRITICAL. |
43+
| additional_asset_support | none | TQ_ORACLE_ADDITIONAL_ASSET_SUPPORT | additional_asset_support | true | Enables default+extra idle balance tokens and addresses. |
44+
| max_calls | none | TQ_ORACLE_MAX_CALLS | max_calls | 3 | Semaphore size for adapter RPC throttling. |
45+
| rpc_max_concurrent_calls | none | TQ_ORACLE_RPC_MAX_CONCURRENT_CALLS | rpc_max_concurrent_calls | 5 | Declared concurrency cap (currently unused by adapters). |
46+
| rpc_delay | none | TQ_ORACLE_RPC_DELAY | rpc_delay | 0.15s | Base sleep after adapter RPC calls. |
47+
| rpc_jitter | none | TQ_ORACLE_RPC_JITTER | rpc_jitter | 0.10s | Randomized extra sleep after adapter RPC calls. |
48+
| price_warning_tolerance_percentage | none | TQ_ORACLE_PRICE_WARNING_TOLERANCE_PERCENTAGE | price_warning_tolerance_percentage | 0.5 | Pyth validator: warn above this % deviation. |
49+
| price_failure_tolerance_percentage | none | TQ_ORACLE_PRICE_FAILURE_TOLERANCE_PERCENTAGE | price_failure_tolerance_percentage | 1.0 | Pyth validator: fail above this % deviation; must exceed warning threshold. |
50+
| pyth_enabled | none | TQ_ORACLE_PYTH_ENABLED | pyth_enabled | true | Toggles Pyth-based validation. |
51+
| pyth_hermes_endpoint | none | TQ_ORACLE_PYTH_HERMES_ENDPOINT | pyth_hermes_endpoint | https://hermes.pyth.network | Pyth price/metadata source. |
52+
| pyth_staleness_threshold | none | TQ_ORACLE_PYTH_STALENESS_THRESHOLD | pyth_staleness_threshold | 60s | Reject Pyth prices older than this window. |
53+
| pyth_max_confidence_ratio | none | TQ_ORACLE_PYTH_MAX_CONFIDENCE_RATIO | pyth_max_confidence_ratio | 0.03 | Max allowed `conf/price` ratio from Pyth. |
54+
| pyth_dynamic_discovery_enabled | none | TQ_ORACLE_PYTH_DYNAMIC_DISCOVERY_ENABLED | pyth_dynamic_discovery_enabled | true | Reserved; not yet wired. |
55+
56+
## Adapter Specific Settings
57+
58+
| Adapter defaults | Env var | TOML path | Default | Effect |
59+
| --- | --- | --- | --- | --- |
60+
| idle_balances.extra_tokens | TQ_ORACLE_ADAPTERS__IDLE_BALANCES__EXTRA_TOKENS | adapters.idle_balances.extra_tokens | {} | Map symbol→address added (tvl-only) when `additional_asset_support` is true. |
61+
| idle_balances.extra_addresses | TQ_ORACLE_ADAPTERS__IDLE_BALANCES__EXTRA_ADDRESSES | adapters.idle_balances.extra_addresses | [] | Extra vault-like addresses to scan for idle balances. |
62+
| stakewise.stakewise_vault_addresses | TQ_ORACLE_ADAPTERS__STAKEWISE__STAKEWISE_VAULT_ADDRESSES | adapters.stakewise.stakewise_vault_addresses | [] | Vault list for StakeWise adapter; if empty, uses network default. |
63+
| stakewise.stakewise_exit_queue_start_block | TQ_ORACLE_ADAPTERS__STAKEWISE__STAKEWISE_EXIT_QUEUE_START_BLOCK | adapters.stakewise.stakewise_exit_queue_start_block | 0 | From-block for exit queue scan. |
64+
| stakewise.stakewise_exit_max_lookback_blocks | TQ_ORACLE_ADAPTERS__STAKEWISE__STAKEWISE_EXIT_MAX_LOOKBACK_BLOCKS | adapters.stakewise.stakewise_exit_max_lookback_blocks | 28800 (~4 days) | Lookback cap when scanning exit logs. |
65+
| stakewise.extra_addresses | TQ_ORACLE_ADAPTERS__STAKEWISE__EXTRA_ADDRESSES | adapters.stakewise.extra_addresses | [] | Extra addresses (vault-like) to scan for StakeWise positions. |
66+
| stakewise.skip_exit_queue_scan | TQ_ORACLE_ADAPTERS__STAKEWISE__SKIP_EXIT_QUEUE_SCAN | adapters.stakewise.skip_exit_queue_scan | false | If true, skips exit queue tickets and only reads shares. |
67+
68+
## Custom Adapter Settings
69+
70+
| `subvault_adapters` entry (TOML list) | Key | Default | Purpose |
71+
| --- | --- | --- | --- |
72+
| subvault_address | required | n/a | Target subvault (or arbitrary address if `skip_subvault_existence_check=true`). |
73+
| additional_adapters | [] | [] | Names from registry: `idle_balances`, `stakewise`, `streth`; run against this subvault. |
74+
| skip_idle_balances | false | false | Skip default idle_balances for this subvault. |
75+
| skip_streth | false | false | Skip strETH adapter for this subvault. |
76+
| adapter_overrides | {} | {} | Per-adapter kwargs merged over defaults (e.g., custom `stakewise_vault_addresses`). |
77+
| skip_subvault_existence_check | false | false | Allows non-vault addresses; requires `allow_dangerous=true`. |
78+
79+
## Derived Settings
80+
81+
| Runtime derived | Source | Notes |
82+
| --- | --- | --- |
83+
| using_default_rpc | set in CLI callback | True when `vault_rpc` came from network default. |
84+
| chain_id | computed lazily | Derived from `vault_rpc` when first accessed. |
85+
| oracle_helper defaults | computed in CLI callback | Set per network if `oracle_helper_address` not provided. |
86+
| block_number fallback | computed in CLI callback | Latest block pulled from `vault_rpc` if not supplied. |

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ dev = [
2222
"pytest>=8.4.2",
2323
"pytest-asyncio>=1.2.0",
2424
"pytest-env>=1.1.0",
25+
"pytest-mock>=3.15.1",
2526
"ruff>=0.13.2",
2627
]
2728

src/tq_oracle/abi.py

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
from __future__ import annotations
22

3+
from typing import cast
4+
5+
import asyncio
6+
37
import json
48
from pathlib import Path
59

610
from eth_typing import URI, ChecksumAddress
711
from web3 import Web3
812

13+
from tq_oracle.settings import OracleSettings
14+
915
ABIS_DIR = Path(__file__).parent / "abis"
1016

17+
CORE_VAULTS_COLLECTOR_PATH = ABIS_DIR / "CoreVaultsCollector.json"
18+
MULTICALL_ABI_PATH = ABIS_DIR / "Multicall.json"
1119
ORACLE_ABI_PATH = ABIS_DIR / "IOracle.json"
1220
ORACLE_HELPER_ABI_PATH = ABIS_DIR / "OracleHelper.json"
1321
VAULT_ABI_PATH = ABIS_DIR / "Vault.json"
@@ -18,6 +26,7 @@
1826
FEE_MANAGER_ABI_PATH = ABIS_DIR / "FeeManager.json"
1927
STAKEWISE_VAULT_ABI_PATH = ABIS_DIR / "StakeWiseVault.json"
2028
STAKEWISE_OS_TOKEN_VAULT_ESCROW_ABI_PATH = ABIS_DIR / "StakeWiseOsTokenVaultEscrow.json"
29+
OSTOKEN_VAULT_CONTROLLER_ABI_PATH = ABIS_DIR / "OsTokenVaultController.json"
2130

2231

2332
def load_abi(path: str | Path) -> list[dict]:
@@ -40,6 +49,15 @@ def load_abi(path: str | Path) -> list[dict]:
4049
return data["abi"]
4150

4251

52+
def load_core_vaults_collector_abi() -> list[dict]:
53+
return load_abi(CORE_VAULTS_COLLECTOR_PATH)
54+
55+
56+
def load_multicall_abi() -> list[dict]:
57+
"""Load the Multicall ABI."""
58+
return load_abi(MULTICALL_ABI_PATH)
59+
60+
4361
def load_oracle_abi() -> list[dict]:
4462
"""Load the Oracle ABI."""
4563
return load_abi(ORACLE_ABI_PATH)
@@ -85,7 +103,12 @@ def load_stakewise_os_token_vault_escrow_abi() -> list[dict]:
85103
return load_abi(STAKEWISE_OS_TOKEN_VAULT_ESCROW_ABI_PATH)
86104

87105

88-
def get_oracle_address_from_vault(vault_address: str, rpc_url: str) -> ChecksumAddress:
106+
def load_ostoken_vault_controller_abi() -> list[dict]:
107+
"""Load the OsToken Vault Controller ABI."""
108+
return load_abi(OSTOKEN_VAULT_CONTROLLER_ABI_PATH)
109+
110+
111+
def get_oracle_address_from_vault(settings: OracleSettings) -> ChecksumAddress:
89112
"""Fetch the oracle address from the vault contract.
90113
91114
Args:
@@ -99,6 +122,9 @@ def get_oracle_address_from_vault(vault_address: str, rpc_url: str) -> ChecksumA
99122
ConnectionError: If RPC connection fails
100123
ValueError: If contract call fails
101124
"""
125+
rpc_url = settings.vault_rpc_required
126+
vault_address = settings.vault_address_required
127+
block_number = settings.block_number_required
102128
w3 = Web3(Web3.HTTPProvider(URI(rpc_url)))
103129
if not w3.is_connected():
104130
raise ConnectionError(f"Failed to connect to RPC: {rpc_url}")
@@ -108,7 +134,54 @@ def get_oracle_address_from_vault(vault_address: str, rpc_url: str) -> ChecksumA
108134
vault_contract = w3.eth.contract(address=checksum_vault, abi=vault_abi)
109135

110136
try:
111-
oracle_addr: ChecksumAddress = vault_contract.functions.oracle().call()
137+
oracle_addr: ChecksumAddress = vault_contract.functions.oracle().call(
138+
block_identifier=block_number
139+
)
112140
return oracle_addr
113141
except Exception as e:
114142
raise ValueError(f"Failed to fetch oracle address from vault: {e}") from e
143+
144+
145+
async def fetch_subvault_addresses(settings: OracleSettings) -> list[str]:
146+
"""Fetch all subvault addresses from the vault contract.
147+
148+
Args:
149+
settings: Oracle settings containing vault_address, vault_rpc, and block_number
150+
151+
Returns:
152+
List of subvault addresses
153+
154+
Raises:
155+
ConnectionError: If RPC connection fails
156+
ValueError: If contract call fails
157+
"""
158+
rpc_url = settings.vault_rpc_required
159+
vault_address = settings.vault_address_required
160+
block_number = settings.block_number_required
161+
162+
w3 = Web3(Web3.HTTPProvider(URI(rpc_url)))
163+
if not w3.is_connected():
164+
raise ConnectionError(f"Failed to connect to RPC: {rpc_url}")
165+
166+
vault_abi = load_vault_abi()
167+
checksum_vault = w3.to_checksum_address(vault_address)
168+
vault_contract = w3.eth.contract(address=checksum_vault, abi=vault_abi)
169+
170+
try:
171+
count: int = await asyncio.to_thread(
172+
vault_contract.functions.subvaults().call,
173+
block_identifier=block_number,
174+
)
175+
subvaults = await asyncio.gather(
176+
*[
177+
asyncio.to_thread(
178+
vault_contract.functions.subvaultAt(i).call,
179+
block_identifier=block_number,
180+
)
181+
for i in range(count)
182+
]
183+
)
184+
185+
return cast(list[str], subvaults)
186+
except Exception as e:
187+
raise ValueError(f"Failed to fetch subvault addresses from vault: {e}") from e
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
{
2+
"abi": [
3+
{
4+
"inputs": [
5+
{
6+
"internalType": "address",
7+
"name": "queue",
8+
"type": "address"
9+
},
10+
{
11+
"internalType": "address",
12+
"name": "holder",
13+
"type": "address"
14+
}
15+
],
16+
"name": "analyzeRequests",
17+
"outputs": [
18+
{
19+
"internalType": "uint256",
20+
"name": "assets",
21+
"type": "uint256"
22+
},
23+
{
24+
"internalType": "uint256",
25+
"name": "shares",
26+
"type": "uint256"
27+
}
28+
],
29+
"stateMutability": "view",
30+
"type": "function"
31+
},
32+
{
33+
"inputs": [
34+
{
35+
"internalType": "address",
36+
"name": "holder",
37+
"type": "address"
38+
},
39+
{
40+
"internalType": "bytes",
41+
"name": "deployment",
42+
"type": "bytes"
43+
},
44+
{
45+
"internalType": "address[]",
46+
"name": "",
47+
"type": "address[]"
48+
}
49+
],
50+
"name": "getDistributions",
51+
"outputs": [
52+
{
53+
"components": [
54+
{
55+
"internalType": "address",
56+
"name": "asset",
57+
"type": "address"
58+
},
59+
{
60+
"internalType": "int256",
61+
"name": "balance",
62+
"type": "int256"
63+
},
64+
{
65+
"internalType": "string",
66+
"name": "metadata",
67+
"type": "string"
68+
},
69+
{
70+
"internalType": "address",
71+
"name": "holder",
72+
"type": "address"
73+
}
74+
],
75+
"internalType": "struct IDistributionCollector.Balance[]",
76+
"name": "balances",
77+
"type": "tuple[]"
78+
}
79+
],
80+
"stateMutability": "view",
81+
"type": "function"
82+
}
83+
]
84+
}

0 commit comments

Comments
 (0)