# Chainlink roundId arithmetic assumptions

Code treating Chainlink roundId as a simple counter or doing flat arithmetic like roundId-1 across phase changes

_Area: DeFi oracles, liquidations and flash loans. Seen in 3 disclosed reports; latest 2025-02-05. Updated 2026-10-25._

## How it works
Chainlink's `roundId` is not a counter. It packs a phase/aggregator version in the high bits and the actual aggregator round in the low 64 bits. Code that treats `roundId` as monotonic and does flat arithmetic (`currentRoundId - 1`, `latestRoundId - i`, comparing full uint80 values) breaks whenever the phase/version changes: the value jumps by 2^64, so the derived round does not exist, is stale, or the lookup reverts. The broken assumption is "roundId increments by one."

## Where it shows up
- Any `getRoundData(roundId - 1)` or `latestRoundId - i` used to fetch a previous round [E1](https://bugbountylibrary.com/r/sherlock%3A2023-04-hubble-exchange%3AM-10.md)[E2](https://bugbountylibrary.com/r/c4%3A2024-02-wise-lending%3AM-02.md)[E3](https://bugbountylibrary.com/r/cantina%3Abima_december2024%3A3.2.7.md)
- Loops that iterate backwards over rounds by subtracting a flat count [E2](https://bugbountylibrary.com/r/c4%3A2024-02-wise-lending%3AM-02.md)
- Comparisons or thresholds applied to the full uint80 roundId instead of the low-64 aggregator round [E2](https://bugbountylibrary.com/r/c4%3A2024-02-wise-lending%3AM-02.md)
- Funding, pricing, or liquidation paths that revert if a round lookup fails, with no fallback [E1](https://bugbountylibrary.com/r/sherlock%3A2023-04-hubble-exchange%3AM-10.md)[E3](https://bugbountylibrary.com/r/cantina%3Abima_december2024%3A3.2.7.md)
- No handling for phase/aggregator upgrades or transitions [E1](https://bugbountylibrary.com/r/sherlock%3A2023-04-hubble-exchange%3AM-10.md)[E3](https://bugbountylibrary.com/r/cantina%3Abima_december2024%3A3.2.7.md)

## How to review
The invariant: never derive a prior round by subtracting from a packed roundId, and never assume consecutive roundIds exist. Check that code decodes phaseId (high bits) and aggregatorRoundId (low uint64) separately, trims to uint64 before comparing or thresholding, and handles phase transitions explicitly (try/catch, fall back to a lower phase, or track prior rounds explicitly). Confirm that a failed or stale round lookup cannot block critical paths like funding settlement, pricing, or liquidation.

## Variants seen
- Funding settlement assumes roundId increments by one; phase change skips 2^64 rounds and reverts settleFunding, DOSing all markets [E1](https://bugbountylibrary.com/r/sherlock%3A2023-04-hubble-exchange%3AM-10.md)
- Heartbeat calibration subtracts a flat count from a proxy roundId; when aggregator round < 50 it crosses into a nonexistent phase/round or returns stale prices, and the guard is unreachable due to uint80 comparison [E2](https://bugbountylibrary.com/r/c4%3A2024-02-wise-lending%3AM-02.md)
- Price feed derives previous round as `_currentRoundId - 1`; after an aggregator upgrade the lookup fails and fetchPrice reverts, DoSing troves and liquidations [E3](https://bugbountylibrary.com/r/cantina%3Abima_december2024%3A3.2.7.md)

## Impact and severity
In all three cases the result was denial of service on core functions: funding settlement halted across markets for up to 24h [E1](https://bugbountylibrary.com/r/sherlock%3A2023-04-hubble-exchange%3AM-10.md), stale or reverting prices [E2](https://bugbountylibrary.com/r/c4%3A2024-02-wise-lending%3AM-02.md), and blocked trove operations and liquidations [E3](https://bugbountylibrary.com/r/cantina%3Abima_december2024%3A3.2.7.md). Severity rises when the affected path is liquidation or settlement, since a stuck oracle can leave positions unliquidated or funds unsettled. No paid bounties in evidence.

## Why fixes fail
Guards that compare the full packed uint80 (e.g. `roundId < 50`) are unreachable or wrong because the high bits dominate; the check must operate on the low-64 aggregator round [E2](https://bugbountylibrary.com/r/c4%3A2024-02-wise-lending%3AM-02.md). Simply wrapping lookups in try/catch without a fallback still leaves the path unable to obtain a valid prior round across a phase change [E1](https://bugbountylibrary.com/r/sherlock%3A2023-04-hubble-exchange%3AM-10.md).

## Evidence
- E1: https://bugbountylibrary.com/r/sherlock%3A2023-04-hubble-exchange%3AM-10.md
- E2: https://bugbountylibrary.com/r/c4%3A2024-02-wise-lending%3AM-02.md
- E3: https://bugbountylibrary.com/r/cantina%3Abima_december2024%3A3.2.7.md
