Security (EVM-02): oracle-service reads exchangeRateStored at 'latest' block; no reorg protection #8

Closed
opened 2026-04-21 08:13:14 +00:00 by Brouie · 4 comments
Brouie commented 2026-04-21 08:13:14 +00:00 (Migrated from gitlab.com)

@PlasticDigits finding from security review on #5.

Severity

Low-to-Medium. BSC reorgs are infrequent but do occur.

Description

oracle-service/src/bsc.rs read_exchange_rate_stored calls c.exchangeRateStored().call().await without specifying a block tag. Alloy defaults to latest, which is the most recent block — vulnerable to reorg.

let r = c.exchangeRateStored().call().await.map_err(|e| eyre!("vToken call: {}", e))?;

Why this matters

If BSC reorgs out the block the rate was read from, and a different rate existed on the winning chain at that height, the service may have already broadcast the pre-reorg rate to Terra. Because INV-ORACLE-MONO-001 enforces strictly non-decreasing rates on Terra, a broadcast higher-than-reality rate can never be rolled back on-chain — the Terra state would be permanently ahead of true BSC state.

BSC finality (via fast-finality proposals) has become more robust but 6-12 block reorgs have been observed historically.

Recommendation

Either:

  1. Read at finalized tag: .block(BlockNumberOrTag::Finalized) (simplest, requires newer BSC RPCs supporting finalized tag)
  2. Read at latest - N where N = configurable confirmation depth (e.g. 15 blocks = ~45s on BSC)
  3. Read twice at latest, separated by N seconds, and require match before broadcasting

Option 2 is the most defensive and portable.

  • Review parent: #5
  • Pairs with EVM-01 (multi-provider), EVM-14 (chainId)
@PlasticDigits finding from security review on #5. ## Severity Low-to-Medium. BSC reorgs are infrequent but do occur. ## Description `oracle-service/src/bsc.rs` `read_exchange_rate_stored` calls `c.exchangeRateStored().call().await` without specifying a block tag. Alloy defaults to `latest`, which is the most recent block — vulnerable to reorg. ```rust let r = c.exchangeRateStored().call().await.map_err(|e| eyre!("vToken call: {}", e))?; ``` ## Why this matters If BSC reorgs out the block the rate was read from, and a different rate existed on the winning chain at that height, the service may have already broadcast the pre-reorg rate to Terra. Because INV-ORACLE-MONO-001 enforces strictly non-decreasing rates on Terra, a broadcast higher-than-reality rate can never be rolled back on-chain — the Terra state would be permanently ahead of true BSC state. BSC finality (via fast-finality proposals) has become more robust but 6-12 block reorgs have been observed historically. ## Recommendation Either: 1. Read at finalized tag: `.block(BlockNumberOrTag::Finalized)` (simplest, requires newer BSC RPCs supporting finalized tag) 2. Read at `latest - N` where N = configurable confirmation depth (e.g. 15 blocks = ~45s on BSC) 3. Read twice at `latest`, separated by N seconds, and require match before broadcasting Option 2 is the most defensive and portable. ## Related - Review parent: #5 - Pairs with EVM-01 (multi-provider), EVM-14 (chainId)
Brouie commented 2026-04-21 08:17:07 +00:00 (Migrated from gitlab.com)

mentioned in issue #5

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

mentioned in commit 0f5a3e43ba

mentioned in commit 0f5a3e43ba60bf72e82628914c614ab2905fa835
PlasticDigits commented 2026-04-21 13:41:12 +00:00 (Migrated from gitlab.com)

Fix merged (main @ 387e438)

What changed

  • oracle-service/src/bsc.rs: Before calling exchangeRateStored, the service now calls eth_blockNumber, then performs eth_call at block latest - BSC_CONFIRMATION_BLOCKS (default 15 blocks), matching the review recommendation for portable reorg protection on BSC (~45s at 3s/block).
  • BSC_CONFIRMATION_BLOCKS is read from the environment (optional); default 15 if unset.
  • Config / main.rs: wire the confirmation depth through the existing RPC fallback path.

Merge

  • Developed in a git worktree on fix/issue-8-bsc-reorg-protection, merged into main alongside the existing EVM-03 canonical VENUS_VTOKEN_ADDRESS validation (conflict resolved by keeping validation + adding bsc_confirmation_blocks).

Ops note

  • Set RUST_LOG=debug (or ust1_oracle_service=debug) to see latest_block, at_block, and confirmation_blocks on each BSC read.
## Fix merged (main @ 387e438) **What changed** - `oracle-service/src/bsc.rs`: Before calling `exchangeRateStored`, the service now calls `eth_blockNumber`, then performs `eth_call` at block `latest - BSC_CONFIRMATION_BLOCKS` (default **15** blocks), matching the review recommendation for portable reorg protection on BSC (~45s at 3s/block). - `BSC_CONFIRMATION_BLOCKS` is read from the environment (optional); default 15 if unset. - `Config` / `main.rs`: wire the confirmation depth through the existing RPC fallback path. **Merge** - Developed in a git worktree on `fix/issue-8-bsc-reorg-protection`, merged into `main` alongside the existing EVM-03 canonical `VENUS_VTOKEN_ADDRESS` validation (conflict resolved by keeping validation + adding `bsc_confirmation_blocks`). **Ops note** - Set `RUST_LOG=debug` (or `ust1_oracle_service=debug`) to see `latest_block`, `at_block`, and `confirmation_blocks` on each BSC read.
Brouie commented 2026-04-22 00:24:29 +00:00 (Migrated from gitlab.com)

@PlasticDigits verified fix on 0f5a3e4.

  • bsc.rs reads get_block_number then queries at at_block = latest.saturating_sub(confirmation_blocks) via BlockId::Number(BlockNumberOrTag::Number(at_block))
  • Default BSC_CONFIRMATION_BLOCKS=15 (~45s on BSC) — matches the recommendation exactly
  • saturating_sub safe against early-chain underflow
  • Logs latest_block + confirmation_blocks for operational visibility

Reorg window closed.

@PlasticDigits verified fix on 0f5a3e4. - `bsc.rs` reads `get_block_number` then queries at `at_block = latest.saturating_sub(confirmation_blocks)` via `BlockId::Number(BlockNumberOrTag::Number(at_block))` - Default `BSC_CONFIRMATION_BLOCKS=15` (~45s on BSC) — matches the recommendation exactly - `saturating_sub` safe against early-chain underflow - Logs latest_block + confirmation_blocks for operational visibility Reorg window closed.
Brouie (Migrated from gitlab.com) closed this issue 2026-04-22 00:24:30 +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#8
No description provided.