Reciprocal Price Normalization and Nonzero-Invariant Discipline

  • Name: reciprocal price normalization and nonzero-invariant discipline
  • Category: solvency arithmetic reference / reciprocal-pricing checklist / impossible-zero note
  • Summary: Some protocols do not consume collateral prices directly. They first invert them into K / price, 1 / exchangeRate, or another reciprocal scalar and then feed that result into borrow limits, LTV checks, health factors, or liquidation math. That transform creates a sharp failure boundary: a sufficiently large but locally honest upstream mark can floor all the way down to 0 in integer arithmetic. Healthy systems treat reciprocal pricing as its own security boundary, not as harmless normalization. This note exists to preserve the design rule that inverse == 0 is usually impossible state, and that both the upstream mark and the downstream reciprocal need explicit range discipline before solvency code may trust them.
  • Why this note exists:
    • The exploitable bug is often misdescribed as just a donation attack or just a weird oracle price, even when the decisive failure is the reciprocal transform itself.
    • Teams often reason about reciprocal formulas in real numbers while Solidity executes floor-rounded integers with hard zero cliffs.
    • A protocol can have a truthful upstream price and still create fake solvency if it interprets a floored reciprocal as valid low-risk state.

What this note means by reciprocal normalization discipline

  • Reciprocal normalization means the protocol stores or consumes a price-like scalar by inverting some upstream mark, typically with integer math such as K / price.
  • Nonzero-invariant discipline means the protocol treats 0 in any solvency-critical reciprocal as impossible state unless it is explicitly modeled and safely handled.
  • The design goal is not never use reciprocals. It is never let reciprocal range boundaries hide inside normalization code where the rest of the lending engine assumes they cannot fail.

The main design rules

1. Prefer direct-price formulations when practical

  • Every reciprocal transform creates a large number becomes zero boundary that direct-price math may avoid entirely.
  • If the protocol can express LTV, borrow limits, and liquidation thresholds with the raw mark, that is usually the simpler and safer formulation.
  • A direct price can still be manipulated, but it does not silently pass through a cliff where very expensive turns into literally zero cost.

2. Put range guards on both sides of the seam

  • The upstream oracle, adapter, or wrapper-price reader should reject marks outside the domain the lending engine can safely invert.
  • The downstream lending engine should still reject inverse == 0, absurdly small inverses, or other impossible reciprocal outputs even if the upstream layer promises bounds.
  • Healthy systems do not rely on one side of the seam alone. Oracle discipline and borrow-engine discipline should overlap.

3. Treat reciprocal math as part of the oracle boundary

  • K / price is not neutral plumbing. It changes what kinds of upstream mistakes are catastrophic.
  • A wrapper mark that seems only too high in the oracle layer can become no debt risk at all after inversion.
  • Audit reciprocal conversion code with the same seriousness as feed composition, decimal normalization, and staleness handling.

4. Publish the supported price domain explicitly

  • Reciprocal systems have an implicit maximum supported upstream mark: once price > K, K / price may floor below 1 and then to 0 depending on the fixed-point convention.
  • The protocol should make that domain explicit in code, tests, audits, and comments instead of leaving it buried in constants.
  • A healthy comment is not price should normally be around 1e18; it is this engine only supports upstream marks below X because larger values would underflow the reciprocal domain.

5. Fuzz upward, not only around ordinary market ranges

  • Many test suites explore price drops, modest increases, and decimal mismatches, but never force the reciprocal to the exact zero boundary.
  • A useful harness keeps pushing the upstream mark upward until the reciprocal hits 0, then asserts the system has already reverted, paused, or de-recognized the collateral path before that point.
  • This matters especially for donation-sensitive wrappers, fresh vaults, and thin local marks where an attacker can print extreme values cheaply.

6. Treat sentinel zero as a state-machine decision, not a free default

  • If 0 is a possible stored value, the system must decide what it means.
  • In healthy solvency code, 0 is usually an error, a pause condition, or a quarantine signal — not a valid exchangeRate, debt-scaling term, or collateral-cost input.
  • Using zero as an unguarded default often converts an arithmetic edge into a balance-sheet lie.

What sound design looks like

  • The price domain is explicit.
    • The system names the largest upstream mark it can safely invert, and rejects marks beyond that domain before they become solvency inputs.
  • Borrow-side code never accepts impossible reciprocals.
    • inverse == 0 or other pathological reciprocal outputs revert, pause, or route into a bounded recovery path.
  • Upstream and downstream guards overlap.
    • The oracle adapter rejects out-of-domain prices, and the lending engine still refuses impossible reciprocal state.
  • Integer-floor behavior is tested directly.
    • The protocol proves with fuzzing and invariants that no reachable collateral state can make a solvency-critical reciprocal cross into zero.
  • Comments match real arithmetic.
    • The code and docs describe actual floor-rounded fixed-point semantics, not only real-number intuition.
  • Fresh wrappers are distrusted before inversion.
    • Donation-sensitive or thin wrappers do not get to define the first economically meaningful mark and then immediately flow through reciprocal borrow math.

Useful positive design shapes

1. Oracle-side upper-bound rejection

  • The wrapper oracle or adapter rejects prices above the reciprocal-safe maximum before publication.
  • This is the cleanest first boundary because it keeps impossible values from entering logs, caches, and downstream state at all.
  • It is still not sufficient on its own; downstream code should continue treating inverse == 0 as impossible.

2. Pair-side nonzero guard after inversion

  • The lending pair computes the reciprocal, then immediately reverts if the result is 0 or below a protocol-defined minimum safe value.
  • This is the decisive last line of defense when upstream assumptions fail, deployments change, or a new wrapper behaves differently than expected.
  • A healthy pair treats this as core solvency logic, not as a UX sanity check.

3. Reciprocal-domain metadata in audits and deployment runbooks

  • Deployment and listing reviews should include one explicit question: what upstream price range can this market safely invert?
  • This keeps launch reviewers from approving a wrapper whose ordinary or attacker-reachable exchange-rate domain already brushes against the reciprocal floor.
  • The question is especially important for ERC-4626 collateral, rebasing wrappers, and oracle overlays with their own scaling.

4. Reciprocal-aware collateral quarantine

  • If the upstream mark comes from a donation-sensitive or fresh wrapper, the protocol should quarantine collateral usage long before reciprocal range exhaustion becomes reachable.
  • This is the operational companion to arithmetic guards: make it expensive or impossible for attackers to print the extreme mark in the first place.
  • The useful habit is to treat fresh wrapper + reciprocal borrow math as a compounded risk surface, not two independent low-risk choices.

Negative exemplar cluster: what no reciprocal discipline looks like

  • zero-exchange-rate exploit (2025-06-26) — the canonical preserved case where a truthful but attacker-printed ERC-4626 share price was inverted as 1e36 / price, floored to 0, and then interpreted by _isSolvent() as if the collateral imposed no borrow-side cost at all.
  • The recurring lesson is not merely the price got too high. It is the protocol had no explicit theory of what should happen when reciprocal math leaves the supported domain.

Red flags that usually mean the reciprocal boundary is underdesigned

  • This normalization is harmless; the real risk is only upstream oracle quality
  • The price should never get that large without an enforced bound proving it
  • A zero exchange rate just means very safe collateral or any algebra that quietly treats impossible scalar state as benign
  • The oracle rejects weird values, so the pair does not need its own guard
  • We tested realistic prices when attacker-controlled wrappers can generate unrealistic ones cheaply
  • Comments or audits that reason from 0.49... without acknowledging Solidity will store 0

Design questions worth asking first

  • What exact reciprocal does the protocol store or consume: K / price, 1 / exchangeRate, or another normalized inverse?
  • What is the largest upstream mark that can be represented safely before floor rounding crosses the protocol’s minimum reciprocal domain?
  • Where is that boundary enforced today: oracle adapter, pair contract, collateral registry, deployment runbook, or nowhere?
  • If the reciprocal becomes 0, what exact behavior follows in borrow, liquidation, health-factor, and collateral-admission code?
  • Can a donation-sensitive wrapper, thin market, or fresh collateral listing reach the dangerous price domain cheaply?
  • Do comments, tests, and audits reason about real-number intuition or about actual integer-floor execution?

Failure-prevention checklist

  • For every reciprocal path, write down the exact upstream economic object, units, normalization constant K, minimum acceptable reciprocal, and maximum supported upstream mark before floor rounding crosses that minimum.
  • Keep the direct mark and the reciprocal-safe range in the same review artifact so launch reviewers cannot approve a collateral path without also seeing where the inverse becomes unsafe.
  • Reject out-of-domain direct marks before publication, and still reject impossible reciprocal outputs after inversion. Do not let either side assume the other side owns the whole invariant.
  • Treat inverse == 0 as impossible solvency state everywhere it can matter: cache writes, collateral admission, borrow checks, liquidation math, migrations, and admin-set price paths.
  • Prefer deriving reciprocals at the narrowest possible consumption point instead of storing them as long-lived truth unless the protocol can prove the cached reciprocal stays in range across upgrades and state changes.
  • Fuzz upward through attacker-reachable wrapper or share-price inflation, not just ordinary volatility, until the reciprocal would hit 0; assert the system has already reverted, paused, or de-recognized the collateral path.
  • Quarantine fresh, thin, or donation-sensitive wrappers before reciprocal borrow math goes live, because the cheapest route to inverse == 0 is often launch-state economics rather than wild external market movement.
  • Make impossible-state handling machine-visible in monitoring: alerts for reciprocal outputs near zero, sudden direct-mark jumps toward the supported ceiling, and any market that starts relying on one thin wrapper for borrow-side truth.
  • Keep blast-radius controls under the arithmetic anyway: collateral caps, borrow-side quarantine, and fast pauses should still contain loss if one reciprocal guard is bypassed.

Audit prompts

  • Where exactly is reciprocal math introduced in the code path, and why does the protocol need the inverse at all instead of consuming the direct mark?
  • What fixed-point constant K is used, what units does it assume, and what exact upstream value makes the reciprocal floor to 0 in deployed arithmetic?
  • Which component is supposed to enforce the upper bound on the direct mark, and what still protects the system if that component is reused, upgraded, bypassed, or misconfigured later?
  • If the reciprocal becomes 0, which concrete branches become unsafe: collateral valuation, borrow power, liquidation discounting, health-factor checks, reserve accounting, or migration logic?
  • Can an attacker reach the dangerous domain honestly through donation, first-depositor control, stale wrapper state, or thin local liquidity rather than through an obviously fake oracle answer?
  • Are there any stored or cached reciprocal values whose validity depends on assumptions that can change later, such as wrapper replacement, decimal rewiring, or collateral-type migration?
  • Do tests prove nonzero reciprocal behavior only around expected market ranges, or do they force the system across the exact zero boundary and verify fail-closed behavior first?
  • Is zero used anywhere as a sentinel for unset, safe, or not applicable in a way that could silently leak into solvency code?

Lessons that should already be standard

  • Reciprocal math is part of the security boundary, not a cosmetic normalization step behind the oracle.
  • The upstream price looked truthful is not a defense if the downstream engine cannot safely represent the reciprocal of that price.
  • Extreme but attacker-reachable wrapper marks matter more than ordinary volatility bands when the failure mode is floor-to-zero.
  • Zero is almost never a benign solvency value. If a reciprocal can hit zero, the system should already be in revert, quarantine, or pause territory.
  • Launch-state collateral is arithmetic risk, not just business-process risk. Fresh wrappers and thin supply are exactly where reciprocal assumptions are weakest.
  • Reviewers should be able to name the reciprocal-safe range out loud. If nobody can state the maximum supported direct mark, the invariant probably is not owned.
  • Shared responsibility beats delegated folklore: oracle adapters, collateral registries, and lending pairs should all know they are jointly preserving the nonzero reciprocal boundary.
  • A protocol that stores or consumes reciprocals without explicit zero-boundary tests is usually trusting real-number intuition more than the actual machine it deployed.

How this note complements the exploit corpus

Internal linkages

Control surface