Unit / Decimal Normalization Failure
- Name: unit / decimal normalization failure
- Category: exploit pattern / oracle integration failure / dimensional-analysis failure / configuration incident
- Summary: Unit / decimal normalization failure happens when a protocol takes a quantity that is correct in its own domain and treats it as if it already lived in the domain the protocol needs for solvency. The broken step may be a missing quote leg (
cbETH / ETHtreated ascbETH / USD), a bad decimal exponent (18-decimals collateral quoted against6-decimals debt without the right scale factor), or a wrapper / exchange-rate feed used where a final asset price was expected. Nobody has to move the market for the exploit to pay. The protocol publishes dimensionally wrong truth, and lending or liquidation logic monetizes it immediately. - What it is:
- A protocol needs a final answer in one unit domain: usually
collateral priced in debt assetorasset priced in USD. - Somewhere in the integration stack, it instead wires in a lower-level quantity with different semantics: a ratio feed, a wrapper exchange rate, or a value missing one decimal-conversion step.
- The protocol does not enforce dimensional sanity on the final output.
- Borrowers, liquidators, or opportunistic traders use the wrong price before operators can correct the configuration.
- A protocol needs a final answer in one unit domain: usually
- Typical preconditions:
- Oracle composition happens through governance, curator configuration, or deployment-time constructor parameters rather than one immutable hard-coded path.
- The integration consumes composite or wrapped assets whose price path requires multiplication, division, or decimal scaling across several legs.
- One adapter branch is responsible for reconciling heterogeneous source conventions — for example Chainlink
8-decimal answers, wrapper ratios, TWAPs, or AUM math that naturally live at very different scales — into one final solvency output. - The contracts trust
price()outputs as already normalized and do not assert plausible magnitude bands. - Liquidations or borrowing remain permissionless, so a mispricing becomes a race the protocol will lose.
- The team’s review process checks whether feeds are live, but not whether the dimensions of the final answer are correct.
- Common vulnerable surfaces:
- ratio feeds like
stAsset / ETHwired wherestAsset / USDis required - wrapper exchange-rate feeds substituted for asset prices during oracle migrations
- constructor or deployment parameters that ask humans to supply token decimals, feed decimals, or scale exponents correctly
- fallback paths where one branch returns a normalized dollar price and another returns only a base-asset ratio
- governance proposals that swap adapters or wrappers without re-deriving the full unit transformation end to end
- ratio feeds like
- Failure sequence:
- A protocol deploys or activates a price path that is missing a conversion leg or uses the wrong decimal exponent.
- The resulting answer is internally accepted because it is syntactically valid and numerically nonzero.
- The lending or liquidation engine treats that answer as final solvency truth.
- Borrowers or liquidators turn the dimensional error into real asset movement.
- Operators discover that fixing a wrong price path is slower than exploiting it.
- Variants:
- Missing quote leg — a feed such as
cbETH / ETHis used as if it were alreadycbETH / USD. - Decimal exponent mismatch — the integration fails to reconcile token and feed decimals, often by
10^n. - Wrapper-semantics confusion — an exchange rate, share price, or conversion ratio is mistaken for a final market price.
- Mixed-branch normalization — one branch of an oracle tree returns normalized values while another branch returns lower-level ratios, but both are treated identically downstream.
- Missing quote leg — a feed such as
Three unit-domain promotions reviewers should not collapse
-
A source-domain quantity exists
- A feed, ratio, exchange rate, decimal-scaled answer, or branch-local quote returned some number.
- That only proves
a machine-readable quantity is available, not that the quantity already lives in the solvency domain the protocol needs.
-
The protocol promotes that quantity into canonical solvency truth
- The system decides that this branch output now counts as the final
asset / USD,asset / debtAsset, or other balance-sheet answer downstream code may trust. - This is the dangerous interpretive jump: a locally correct
cbETH / ETHratio or1e18-scaled wrapper rate is stronger thanbytes returned, but still weaker thanthis is the final collateral price this market should honor.
- The system decides that this branch output now counts as the final
-
That accepted solvency truth still deserves live monetary authority
- Borrowing, liquidation, redemption, reserve release, or cap logic is allowed to spend against the published answer immediately.
- Many expensive oracle misconfigurations hide here: even if a protocol can later explain what the branch meant, it already let the wrong domain inherit same-window reserve authority.
Healthy review gets materially better once those promotions stay separate: a number exists, this is the right economic quantity, and the rest of the system may already move money against it are not one event.
- Boundary tests that keep this pattern distinct:
- Classify it here when the decisive bug is dimensional. If the preserved lesson is
the protocol published the wrong unit domain or scale as final solvency truth, this note is doing the analytical heavy lifting. - Classify it under inverse-price-floor-to-zero when the upstream mark may be locally correct or at least economically reachable, but the dangerous step is that reciprocal math like
K / pricefloor-rounds that large mark into literal zero borrow cost. There the units can be right and the reciprocal domain is still wrong. - Classify it under oracle-configuration failure when units are only one part of a broader publication failure involving fallback routing, stale overlay state, cap logic, or wrapper migration policy.
- Classify it under donation-share-price-manipulation when the main question is how a wrapper or vault printed the mark in the first place and the protocol largely honored that printed rate as-is, rather than mis-scaling it.
- Classify it here when the decisive bug is dimensional. If the preserved lesson is
- Detection heuristics:
- Write the full units on every oracle leg and make the algebra cancel explicitly. If you cannot do dimensional analysis on paper, the integration is not review-ready.
- Compare the final onchain answer to a trusted market reference immediately before and after any oracle migration or market launch.
- Check whether extreme but still syntactically valid answers like
1.12 USD for cbETHor10^12overvaluation would be rejected anywhere onchain. - Review constructor comments and deployment scripts for assumptions like
decimals passed are correct; those are not harmless boilerplate. - Test all fallback branches independently, not just the nominal primary path.
- In mixed-source adapters, trace each branch from raw upstream answer to final normalized output separately. A common failure is that one branch already returns a value close to
1e18semantics and gets scaled again, while another branch never gets scaled down at all.
- Defenses:
- Enforce explicit dimensional invariants on the final answer, not just on upstream feed freshness.
- Prefer adapters that encode token/feed decimals onchain rather than trusting human-supplied scale constants.
- Require pre-activation simulations that print the exact final market price and compare it to an independent reference.
- Add fast emergency controls to freeze borrowing and liquidations when a newly activated price deviates beyond plausible bounds.
- Treat oracle migrations as balance-sheet migrations: if semantics changed, require unit tests and reviewer signoff on the dimensional transform itself.
- For heterogeneous adapters, keep a branch-local unit table and regression tests for each source family rather than only testing the shared
getPrice()interface at the end.
What sound design looks like
- The protocol should be able to state one final solvency unit for every price path before launch:
asset / USD,asset / debtAsset,share / underlying, or another explicitly named domain. Anything upstream that lives in a different unit system should be treated as an intermediate value that is not yet safe to honor economically. - The protocol keeps three states explicit instead of flattening them into
price returned: a source-domain quantity exists, one explicit normalization step promotes it into canonical solvency truth, and only then do borrowing, liquidation, or redemption paths gain authority to spend against it. - Oracle composition should read like dimensional algebra, not folk knowledge. Reviewers should be able to annotate each leg with units, decimals, and quote/base semantics and watch them cancel into one final answer with no hidden assumptions left in deployment scripts or governance calldata.
- Configuration should narrow the space of possible mistakes. If a market needs
cbETH / USD, the safest adapter is one that can only be instantiated from a knowncbETH / ETHleg plus a knownETH / USDleg, rather than one generic constructor that trusts operators to supply the right decimals and semantic intent by hand. - Mixed-source adapters should preserve branch-local semantics all the way to the end. A branch returning a wrapper ratio, a branch returning a spot dollar price, and a branch returning a reserve-derived NAV are not interchangeable just because they all fit into
uint256. - Solvency-critical consumers should distrust freshly activated prices until the protocol proves they are dimensionally and economically plausible. Borrowing, liquidations, and large redemptions should not be the first integration test.
- Even after a branch output is recognized as locally meaningful, healthy systems may still stage its authority with caps, collateral-disable periods, or borrow-side quarantine until the protocol proves the published domain and scale are economically plausible in production.
Failure-prevention checklist
- Treat
a source-domain quantity exists,that quantity now counts as canonical solvency truth, andthat published truth may already move money hereas separate review questions in specs, launch checklists, and incident response. - Write a one-page unit sheet for every solvency oracle path: raw source feeds, source decimals, quote/base semantics, wrapper ratios, intermediate scaling steps, and the exact final output domain expected by the lending or liquidation engine.
- Make the final consumer declare what units it expects. If a market needs
asset / USDorasset / debtAsset, encode that expectation in the adapter or market config rather than leaving it implicit in reviewer memory. - Prefer constructors and governance payloads that derive decimals and feed metadata onchain where possible. Every human-supplied exponent or
baseUnitparameter is a live incident surface. - Simulate activation with real current feed values and print the final normalized answer beside at least one independent market reference before enabling borrowing, liquidations, or collateral credit.
- Add hard plausibility bounds and cross-checks on newly activated markets so obviously wrong outputs like
cbETH ~= $1.12or10^12overvaluation fail closed before users can race the protocol. - Test every fallback and alternate branch independently. Do not assume a correct primary path implies a correct backup path when different branches carry different unit semantics.
- Treat wrapper exchange rates, share prices, and conversion ratios as incomplete ingredients unless the downstream consumer explicitly expects that exact domain.
- Couple oracle changes with risk changes: new feed paths, wrapper migrations, decimal overrides, collateral admission, and cap increases should be reviewed as one balance-sheet change set.
- Keep emergency controls ready to freeze borrowing, disable collateral, or suspend liquidations on a market whose published price exits a plausible dimensional band.
- Record a golden test vector per market with expected normalized output, so later upgrades can prove they still publish the same unit domain and scale.
Audit prompts
- Which promotion is happening at each stage here:
some source-domain quantity exists,the protocol promotes it into canonical solvency truth, ordownstream reserve-moving logic is already allowed to spend against it? - What exact unit domain does the final consumer require, and where in code is that requirement made explicit rather than assumed?
- For each oracle leg, what are the units and decimals before and after the transform? Where could a reviewer accidentally confuse a ratio, exchange rate, or quote leg for a final market price?
- If one source returns
asset / ETHand another returnsETH / USD, where is the multiplication or division that actually producesasset / USD, and what test proves the direction is correct? - Which parameters are supplied by humans at deployment or governance time, and what prevents one wrong exponent, decimal constant, or adapter branch from silently publishing fiction?
- Do fallback branches preserve the same final semantics as the primary branch, or does one branch return a lower-level intermediate value that downstream code still honors as solvency truth?
- What onchain invariant or offchain monitor would catch a dimensionally wrong but syntactically valid price immediately after activation?
- If the normalized output were off by
10^6,10^8, or10^12, which protocol actions could still proceed before operators intervene? - Are wrapper, vault, or LP conversions being treated as prices when they are really only partial components of a larger valuation path?
- During migrations or listings, who signs off on the dimensional transform itself rather than only feed freshness, address correctness, or contract ownership?
- Can the protocol produce one review artifact showing raw feeds, intermediate values, final output units, and downstream reciprocal usage for this market end to end?
Lessons that should already be standard
-
A number exists,that number means the right economic thing, andthe protocol may already move money against itare three different claims. -
Correct data in the wrong unit domain is still false solvency.
-
Decimal bugs and missing quote legs are not cosmetic integration mistakes; they are balance-sheet publication failures.
-
A
uint256price interface hides semantics. If units are not named somewhere explicit, reviewers will eventually approve fiction. -
Wrapper exchange rates, share prices, and reserve ratios are not interchangeable with final collateral prices just because they move smoothly and look numerically plausible.
-
Oracle migrations should be treated like asset migrations. Changing the dimensional transform can be as dangerous as changing custody logic.
-
Fallback paths are where semantic drift likes to hide. Two branches that both compile and return nonzero values can still describe completely different economic objects.
-
The protocol should know what prices are impossible before attackers do. If a market can publish an absurd but nonreverting value and still leave borrowing live, the design is unfinished.
-
The real review question is not
does this feed work?butwhat exact economic quantity is this market honoring, and how do we know every branch publishes that same quantity? -
High-signal incidents:
- [[../../../incidents/lending-liquidations/tender-fi-tgmx-gmxpriceoracle-decimal-misconfiguration-borrow-drain-whitehat-2023-03-07|Tender.fi tGMX
GMXPriceOracledecimal misconfiguration borrow drain / whitehat (2023-03-07)]] — Tender’s mixed-source GMX adapter scaled a Chainlink8-decimal GMX answer twice on thetGMXbranch, so roughly1 GMXof collateral looked about10^20times too valuable and a whitehat could borrow about$1.6Mthrough ordinary Compound-style paths. - [[../../../incidents/lending-liquidations/blueberry-protocol-lending-market-priceoracleproxy-coreoracle-decimal-mismatch-borrow-drain-whitehat-2024-02-23|Blueberry Protocol lending market
PriceOracleProxy/CoreOracledecimal-mismatch borrow drain / whitehat (2024-02-23)]] — Blueberry forwarded a strategy-side1e18-scaledCoreOracle.getPrice()into a Compound-stylegetUnderlyingPrice()path that expected prices normalized for underlying-token decimals, makingOHM,USDC, andWBTCdrastically too cheap to borrow the moment borrowing went live. - USDC market oracle decimal misconfiguration (2024-10-13) — a permissionless-market oracle instantiated
MorphoChainlinkOracleV2with the wrong decimal assumptions, effectively overvaluing PAXG collateral by10^12and letting about$350of PAXG unlock roughly$230,000of USDC. - bad-debt liquidations (2026-02-15) — Moonwell’s
MIP-X43rollout wired a rawcbETH / ETHratio into a slot that required a dollar price, collapsing cbETH from roughly$2,200to about$1.12and turning liquidators into immediate extractors.
- [[../../../incidents/lending-liquidations/tender-fi-tgmx-gmxpriceoracle-decimal-misconfiguration-borrow-drain-whitehat-2023-03-07|Tender.fi tGMX
Internal linkages
- Broader family: oracle-configuration failure — use this when the self-inflicted problem is wider than units alone and also includes caps, anchors, timestamp drift, or fallback routing.
- Adjacent attacker-driven family: oracle-manipulation — useful when the protocol’s math is dimensionally correct but the underlying market input itself is gameable.
- Useful contrast: oracle-source-independence-failure — that pattern asks whether several sources are really independent; this one asks whether even a single correct source is being interpreted in the right unit domain.
- Useful contrast: inverse-price-floor-to-zero — that pattern begins only after the protocol already has a high or attacker-reachable mark and then destroys solvency by inverting it into a floored zero.
- Positive-design companion across the seam: Unit-domain seam typing and explicit conversion discipline — use this when the sharper follow-up is what exact source-domain ratio, destination-domain solvency object, and one explicit normalization owner should have existed before a local quote or decimal-scaled branch output could gain final market meaning.
- Positive-design companion across the seam: Price-domain and reciprocal-range discipline — use this when the sharper follow-up is what one end-to-end review artifact should have forced reviewers to name both the final price semantics and any downstream reciprocal-safe range before launch.
- Best preserved seed exemplars so far: tender-fi-tgmx-gmxpriceoracle-decimal-misconfiguration-borrow-drain-whitehat-2023-03-07, blueberry-protocol-lending-market-priceoracleproxy-coreoracle-decimal-mismatch-borrow-drain-whitehat-2024-02-23, morpho-paxg-usdc-market-oracle-decimal-misconfiguration-2024-10-13, and moonwell-cbeth-usd-oracle-misconfiguration-bad-debt-liquidations-2026-02-15.
Control surface
- The leverage sits in deployment scripts, governance proposals, oracle adapter constructors, wrapper migrations, and the exact normalization boundary where an upstream ratio, wrapper rate, or decimal-scaled branch output stops being
some quantityand starts being canonical balance-sheet truth. - The practical lesson is simple and brutal: every solvency-critical oracle path should be reviewable as dimensional algebra. If the units do not cancel cleanly, the protocol is one config change away from lending or liquidating against fiction.
- The key contrast with reciprocal-range failures is that these incidents are already wrong before any later borrow-side transform runs: the protocol did not discover an impossible edge of a correct mark, it published the wrong mark domain in the first place.
- The authority question comes one step later but matters just as much: once a mis-normalized answer is published, how quickly can it become live borrowing, liquidation, reserve-release, or cap-setting truth before anyone can stop it?
- A practical review upgrade is to make every launch or migration ship with one short domain sheet covering raw source units, final solvency output units, any downstream inverse transform, and explicit impossible states.
Sources
- https://www.numencyber.com/an-account-of-the-recent-white-hat-attack-on-defi-protocol-tender-fi/
- https://raw.githubusercontent.com/tender-finance/tender-fi-contracts/main/contracts/PriceOracle.sol
- https://raw.githubusercontent.com/tender-finance/tender-fi-contracts/main/contracts/Comptroller.sol
- https://forum.morpho.org/t/retrospective-paxg-usdc-market-oracle-configuration/891.json
- https://raw.githubusercontent.com/morpho-org/morpho-blue-oracles/main/src/morpho-chainlink/MorphoChainlinkOracleV2.sol
- https://forum.moonwell.fi/t/mip-x43-cbeth-oracle-incident-summary/2068
- https://github.com/moonwell-fi/moonwell-contracts-v2/pull/578
- https://rekt.news/moonwell-rekt
Last reviewed
- 2026-07-31 UTC