Security (EVM-14): No eth_chainId verification; wrong-chain RPC would be read silently #12

Closed
opened 2026-04-21 08:15:18 +00:00 by Brouie · 3 comments
Brouie commented 2026-04-21 08:15:18 +00:00 (Migrated from gitlab.com)

@PlasticDigits finding from security review on #5.

Severity

Low-to-Medium. Deployment footgun; misconfig rarely caught automatically.

Description

oracle-service/src/bsc.rs constructs an Alloy provider from rpc_url and immediately queries exchangeRateStored:

let provider = alloy::providers::ProviderBuilder::new().on_http(rpc_url.parse()?);
let addr = Address::from_str(vtoken).map_err(|e| eyre!("invalid vToken address: {}", e))?;
let c = VToken::new(addr, provider);
let r = c.exchangeRateStored().call().await ...

There is no eth_chainId check before the exchangeRateStored call. If BSC_RPC_URLS contains a URL pointing at:

  • opBNB (chainId 204) — different chain, same-format EVM, may have a contract at the configured address returning wrong data
  • Ethereum mainnet (1) — Venus isn't deployed there; call would fail or return garbage from an EOA
  • BSC testnet (97) — has separate Venus deployment with different rates
  • Any other EVM chain — silently reads whatever is at that address

...the service treats the response as authoritative BSC mainnet data.

Why this matters

Combined with EVM-03 (no Venus address allowlist), a single misconfiguration (wrong RPC URL AND/OR wrong token address) produces a silently-wrong on-chain oracle rate. With CL8Y bridge's recent Terra classic hexxagon.dev mislabel (handoff #109 — returns rebel-2 testnet), misconfigured fallback URLs are a real class of incident.

Recommendation

On provider creation, fetch eth_chainId and assert == 56:

let chain_id = provider.get_chain_id().await?;
if chain_id != 56 {
    return Err(eyre!("BSC RPC returned chainId {}, expected 56 (BSC mainnet)", chain_id));
}

Cache the chainId per provider so the check isn't re-run every poll tick; fail immediately at service startup if any configured RPC URL misreports.

  • Review parent: #5
  • Pairs with EVM-01 (multi-provider), EVM-03 (address allowlist)
@PlasticDigits finding from security review on #5. ## Severity Low-to-Medium. Deployment footgun; misconfig rarely caught automatically. ## Description `oracle-service/src/bsc.rs` constructs an Alloy provider from `rpc_url` and immediately queries `exchangeRateStored`: ```rust let provider = alloy::providers::ProviderBuilder::new().on_http(rpc_url.parse()?); let addr = Address::from_str(vtoken).map_err(|e| eyre!("invalid vToken address: {}", e))?; let c = VToken::new(addr, provider); let r = c.exchangeRateStored().call().await ... ``` There is no `eth_chainId` check before the `exchangeRateStored` call. If `BSC_RPC_URLS` contains a URL pointing at: - **opBNB (chainId 204)** — different chain, same-format EVM, may have a contract at the configured address returning wrong data - **Ethereum mainnet (1)** — Venus isn't deployed there; `call` would fail or return garbage from an EOA - **BSC testnet (97)** — has separate Venus deployment with different rates - **Any other EVM chain** — silently reads whatever is at that address ...the service treats the response as authoritative BSC mainnet data. ## Why this matters Combined with EVM-03 (no Venus address allowlist), a single misconfiguration (wrong RPC URL AND/OR wrong token address) produces a silently-wrong on-chain oracle rate. With CL8Y bridge's recent Terra classic hexxagon.dev mislabel (handoff #109 — returns `rebel-2` testnet), misconfigured fallback URLs are a real class of incident. ## Recommendation On provider creation, fetch `eth_chainId` and assert `== 56`: ```rust let chain_id = provider.get_chain_id().await?; if chain_id != 56 { return Err(eyre!("BSC RPC returned chainId {}, expected 56 (BSC mainnet)", chain_id)); } ``` Cache the chainId per provider so the check isn't re-run every poll tick; fail immediately at service startup if any configured RPC URL misreports. ## Related - Review parent: #5 - Pairs with EVM-01 (multi-provider), EVM-03 (address allowlist)
Brouie commented 2026-04-21 08:17:08 +00:00 (Migrated from gitlab.com)

mentioned in issue #5

mentioned in issue #5
PlasticDigits commented 2026-04-21 13:49:37 +00:00 (Migrated from gitlab.com)

Resolution (EVM-14 / chain-id verification)

Implemented in `oracle-service` (see `673ac74` on `main`, plus follow-up `12051ea` rustfmt-only):

  1. `eth_chainId` check — After each HTTP provider is built, we call `get_chain_id()` and require it to match an allowlist. Results are cached per RPC URL (in-process `HashMap`) so we do not re-query chain id every poll tick.

  2. Startup — `verify_all_bsc_rpc_urls` runs once at process start and validates every configured URL in `BSC_RPC_URLS`, so a misconfigured endpoint fails immediately.

  3. `BSC_ALLOWED_CHAIN_IDS` — Comma-separated list; default is `56` (BSC mainnet only). For BSC Chapel testnet use `97`, for Anvil use `31337` (Foundry default). Example for mixed local/dev testing: `BSC_ALLOWED_CHAIN_IDS=56,97,31337`.

  4. Canonical Venus vToken (glab #9) — Still enforced when the allowlist is mainnet-only (`[56]` only). If you allow testnet/local chain ids, any well-formed contract address is accepted so mocks work.

This closes the “wrong RPC URL silently returns wrong chain” footgun while keeping production strict (56 by default) and test/local workflows explicit via env.

## Resolution (EVM-14 / chain-id verification) Implemented in \`oracle-service\` (see \`673ac74\` on \`main\`, plus follow-up \`12051ea\` rustfmt-only): 1. **\`eth_chainId\` check** — After each HTTP provider is built, we call \`get_chain_id()\` and require it to match an allowlist. Results are **cached per RPC URL** (in-process \`HashMap\`) so we do not re-query chain id every poll tick. 2. **Startup** — \`verify_all_bsc_rpc_urls\` runs once at process start and **validates every** configured URL in \`BSC_RPC_URLS\`, so a misconfigured endpoint fails immediately. 3. **\`BSC_ALLOWED_CHAIN_IDS\`** — Comma-separated list; **default is \`56\`** (BSC mainnet only). For **BSC Chapel testnet** use \`97\`, for **Anvil** use \`31337\` (Foundry default). Example for mixed local/dev testing: \`BSC_ALLOWED_CHAIN_IDS=56,97,31337\`. 4. **Canonical Venus vToken (glab #9)** — Still enforced when the allowlist is **mainnet-only** (\`[56]\` only). If you allow testnet/local chain ids, any well-formed contract address is accepted so mocks work. This closes the “wrong RPC URL silently returns wrong chain” footgun while keeping production strict (56 by default) and test/local workflows explicit via env.
Brouie commented 2026-04-22 00:33:14 +00:00 (Migrated from gitlab.com)

@PlasticDigits verified fix on 673ac74 — this was quietly addressed alongside the Debug redact commit.

New functions in oracle-service/src/bsc.rs:

  • verify_bsc_rpc_chain_id — calls get_chain_id(), checks against allowed_chain_ids, caches result per-URL
  • verify_all_bsc_rpc_urls — startup check across every configured BSC_RPC_URL
  • read_exchange_rate_stored now takes allowed_chain_ids and re-verifies before each call (defense in depth with cache)

Config:

  • BSC_ALLOWED_CHAIN_IDS env var (default "56" = BSC mainnet only)
  • Supports comma-separated for dev: e.g. "56,97,31337" for mainnet + Chapel + Anvil
  • Nice coupling: validate_venus_vtoken_address only requires canonical vFDUSD when allowed_chain_ids is exactly [56] — dev environments can bypass, production mainnet enforces strictly

Wrong-chain RPC now fails fast at startup with clear error message. Closing.

@PlasticDigits verified fix on 673ac74 — this was quietly addressed alongside the Debug redact commit. New functions in `oracle-service/src/bsc.rs`: - `verify_bsc_rpc_chain_id` — calls `get_chain_id()`, checks against `allowed_chain_ids`, caches result per-URL - `verify_all_bsc_rpc_urls` — startup check across every configured BSC_RPC_URL - `read_exchange_rate_stored` now takes `allowed_chain_ids` and re-verifies before each call (defense in depth with cache) Config: - `BSC_ALLOWED_CHAIN_IDS` env var (default "56" = BSC mainnet only) - Supports comma-separated for dev: e.g. "56,97,31337" for mainnet + Chapel + Anvil - Nice coupling: `validate_venus_vtoken_address` only requires canonical vFDUSD when allowed_chain_ids is exactly `[56]` — dev environments can bypass, production mainnet enforces strictly Wrong-chain RPC now fails fast at startup with clear error message. Closing.
Brouie (Migrated from gitlab.com) closed this issue 2026-04-22 00:33:18 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
code/ust1-window#12
No description provided.