# Self-comparing oracle deviation checks

Deviation or sanity checks whose operands alias, are overwritten, or skip pairs, so they always pass

_Area: DeFi oracles, liquidations and flash loans. Seen in 2 disclosed reports; latest 2026-05-18. Updated 2026-10-24._

## How it works
A guard is supposed to compare a live price against a trusted reference and reject large gaps. The assumption that breaks is that the two operands are independent. When the reference value is written into the same variable the live price is later derived from, or when the check only covers some of the pairs it claims to, the comparison becomes a tautology: it compares a value to itself, or leaves a gap unmeasured, and always passes. The code looks protected but enforces nothing.

## Where it shows up
- A named return variable reused as an `abi.decode` or assignment target, then read again in a later comparison [E1](https://bugbountylibrary.com/r/c4%3A2026-01-olas%3AH-02.md)
- Early-return branches that hand back raw spot price without running the deviation check [E1](https://bugbountylibrary.com/r/c4%3A2026-01-olas%3AH-02.md)
- Thresholds computed from the current call's size rather than aggregate exposure [E2](https://bugbountylibrary.com/r/sherlock%3A2023-02-gmx%3AM-27.md)
- Pairwise ratio checks that cover only some of N(N-1)/2 pairs, or that are direction-dependent [E3](https://bugbountylibrary.com/r/c4%3A2021-06-gro%3AH-02.md)
- NatSpec claiming an oracle or sanity check the code does not perform [E3](https://bugbountylibrary.com/r/c4%3A2021-06-gro%3AH-02.md)

## How to review
Trace every operand of the deviation comparison back to its source and confirm they come from independent inputs (spot vs. TWAP, order vs. aggregate). Confirm no assignment overwrites a value used later in the same comparison. Enumerate all return paths and verify each one runs the check. For pairwise checks, confirm all pairs are covered and the check is symmetric under inversion. The invariant: the guard must be able to fail — there must exist an input where the two operands differ and the check rejects it.

## Variants seen
- Named-return overwrite making TWAP compare to itself, zeroing deviation [E1](https://bugbountylibrary.com/r/c4%3A2026-01-olas%3AH-02.md)
- Unvalidated early-return paths bypassing the guard entirely [E1](https://bugbountylibrary.com/r/c4%3A2026-01-olas%3AH-02.md)
- Per-order threshold evaded by splitting into sub-threshold orders in one block [E2](https://bugbountylibrary.com/r/sherlock%3A2023-02-gmx%3AM-27.md)
- Partial pairwise ratio coverage leaving one pair bounded only by transitivity, plus inverted ratios passing [E3](https://bugbountylibrary.com/r/c4%3A2021-06-gro%3AH-02.md)

## Impact and severity
In the evidence these guards were the only protection against price manipulation: liquidity operations ran unprotected [E1](https://bugbountylibrary.com/r/c4%3A2026-01-olas%3AH-02.md), virtual price-impact protection was bypassed [E2](https://bugbountylibrary.com/r/sherlock%3A2023-02-gmx%3AM-27.md), and manipulated ratios could halt deposits/withdrawals [E3](https://bugbountylibrary.com/r/c4%3A2021-06-gro%3AH-02.md). Severity is high because the failure is silent — the check exists and appears to pass. It worsens when the guard is the sole defense and no independent bound exists.

## Why fixes fail
Fixes that patch one branch miss the aliasing root cause; the overwrite pattern recurs wherever a named return is reused. Threshold tweaks do not stop fragmentation [E2](https://bugbountylibrary.com/r/sherlock%3A2023-02-gmx%3AM-27.md), and adding one more pair check does not fix direction-dependence [E3](https://bugbountylibrary.com/r/c4%3A2021-06-gro%3AH-02.md).

## Evidence
- E1: https://bugbountylibrary.com/r/c4%3A2026-01-olas%3AH-02.md
- E2: https://bugbountylibrary.com/r/sherlock%3A2023-02-gmx%3AM-27.md
- E3: https://bugbountylibrary.com/r/c4%3A2021-06-gro%3AH-02.md
