A physicist can spot a wrong formula in seconds — just by checking whether the units make sense. DeFi auditors should steal this technique.
Trail of Bits just published a great piece on dimensional analysis in DeFi, and it reminded me of something that doesn't get enough attention in smart contract security: most formula bugs in DeFi are dimensionally obvious if you know what to look for.
This isn't theoretical. In the first three months of 2026 alone, over $137M has been lost to DeFi exploits — and a meaningful chunk of those losses trace back to arithmetic that looks correct but mixes incompatible units. The Makina Finance oracle exploit ($4M), the Venus Protocol donation attack ($3.7M), the Aave CAPO oracle incident ($26M in wrongful liquidations) — all involved formulas where dimensions didn't match up.
Let me show you how to apply dimensional analysis as a practical audit technique — with patterns you can use today.
DeFi's Fundamental Dimensions
Physics has seven base dimensions (length, mass, time, etc.). DeFi has its own:
| Dimension | Symbol | Example |
|---|---|---|
| Token A quantity | [A] |
USDC balance |
| Token B quantity | [B] |
ETH balance |
| Price (A per B) | [A/B] |
ETH price in USDC |
| Share/LP tokens | [S] |
Vault shares |
| Time | [T] |
Block timestamps |
| Rate (per time) | [1/T] |
Interest rate per second |
| Scalar | [1] |
Percentage, ratio |
Everything else is a derived dimension. Liquidity in Uniswap V2 is √([A]·[B]). A vault's share price is [A]/[S]. An interest accumulator is [1] (dimensionless multiplier).
The golden rule: both sides of any equation must have the same dimension. You can't add quantities with different dimensions. You can't assign a value of one dimension to a variable of another.
Five Red-Flag Patterns
Here are the patterns I check for during audits:
Pattern 1: Adding Tokens of Different Types
// ❌ Dimensionally invalid
uint256 totalValue = tokenABalance + tokenBBalance;
// ✅ Convert to common dimension first
uint256 totalValue = tokenABalance + (tokenBBalance * priceAPerB / 1e18);
// [A] = [A] + [B] · [A/B] = [A] + [A] ✓
The exception: Curve StableSwap pools deliberately add different stablecoins, but only because they assume [USDC] ≈ [DAI] ≈ [USDT] under the pegging assumption. When that assumption breaks (see: UST), the formula breaks too.
Pattern 2: Decimals Masquerading as Quantities
This is what Trail of Bits found in the CAP Labs audit (TOB-CAP-17):
// ❌ decimals ≠ token amount
uint256 pricePerFullShare = IERC4626(asset).convertToAssets(
IERC20Metadata(capToken).decimals() // This is 18, not 1e18!
);
// ✅ Pass an actual token quantity
uint256 pricePerFullShare = IERC4626(asset).convertToAssets(
10 ** IERC20Metadata(capToken).decimals() // 1e18 tokens
);
decimals() returns 18 — a metadata number, not a token amount. 10 ** decimals() returns 1e18 — one full token. These are completely different dimensions:
-
decimals()→[scalar](just a number) -
10 ** decimals()→[token](one full unit)
Passing a scalar where a token quantity is expected gives you garbage output.
Pattern 3: Share Price Calculated from Wrong Pool State
Vault share price should be totalAssets / totalShares = [A]/[S]. Watch for formulas that accidentally compute something else:
// ❌ Uses liquidity instead of assets
uint256 sharePrice = sqrtK / totalShares;
// √([A]·[B]) / [S] = ??? (not a price in any meaningful sense)
// ❌ Divides by wrong denominator
uint256 sharePrice = totalAssets / totalSupply;
// If totalSupply is the token supply, not vault shares:
// [A] / [A] = [scalar], not [A/S]!
// ✅ Correct
uint256 sharePrice = vault.totalAssets() / vault.totalSupply();
// [A] / [S] = [A/S] ✓
The Venus Protocol donation attack exploited exactly this kind of confusion — the attacker inflated totalAssets while totalShares was tiny, making the share price enormous and breaking borrow calculations.
Pattern 4: Oracle Price Used Without Scale Normalization
// Chainlink returns price with 8 decimals
// Token has 18 decimals
// Final calculation needs 18-decimal precision
// ❌ Mixing scales
uint256 value = tokenAmount * oraclePrice;
// [tok · 1e18] × [USD/tok · 1e8] = [USD · 1e26] ← wrong scale!
// ✅ Normalize scales explicitly
uint256 value = tokenAmount * oraclePrice / 1e8;
// [tok · 1e18] × [USD/tok · 1e8] / [1e8] = [USD · 1e18] ✓
The Aave CAPO oracle incident involved a subtler version: the oracle's timestamp-based price adjustment produced a value with an implicit different reference frame than the liquidation engine expected, leading to $26M in wrongful liquidations.
Pattern 5: Interest Rate Accumulation with Wrong Time Dimension
// ❌ Mixing per-second and per-year rates
uint256 accruedInterest = principal * annualRate * timeElapsed;
// [tok] × [1/year] × [seconds] = [tok · seconds/year] ← nonsense!
// ✅ Convert to consistent time units
uint256 accruedInterest = principal * ratePerSecond * timeElapsed;
// [tok] × [1/second] × [seconds] = [tok] ✓
Compound's interest model gets this right with ratePerBlock, but forks sometimes mix up per-block and per-second rates — especially when porting from Ethereum (12s blocks) to L2s (2s blocks) or Solana (400ms slots).
The Dimensional Audit Checklist
When reviewing a DeFi protocol, I systematically check:
1. Label every variable's dimension. Before reading the logic, annotate what each state variable and parameter represents. Reserve Protocol's codebase is the gold standard here — every variable has comments like D27{UoA/tok} documenting both dimension and scale.
2. Trace dimensions through every formula. For each arithmetic operation:
- Multiplication: dimensions multiply (
[A] × [B/A] = [B]) - Division: dimensions divide (
[A] / [B] = [A/B]) - Addition/subtraction: dimensions must match (
[A] + [A] = [A], but[A] + [B]is invalid)
3. Check every external call's expected dimensions. What does convertToAssets() expect? What does getPrice() return? Document it. The CAP Labs bug was a mismatch between what ERC-4626 expected and what was passed.
4. Verify scale consistency across oracle integrations. Chainlink returns 8 decimals for USD prices, 18 for ETH prices. Uniswap TWAP returns prices in Q96 fixed-point. Band Protocol uses 18 decimals. If your protocol consumes multiple oracles, every scale conversion is a potential bug.
5. Check that return values match the function's documented dimension. If a function claims to return "price in USD per token," verify that the computation actually produces [USD/token] and not [USD²/token] or √([USD/token]).
Solana Considerations
Dimensional analysis applies equally to Solana programs, but with different footguns:
// ❌ Common Anchor mistake: confusing lamports and SOL
let fee = amount * fee_rate / 10000;
// If amount is in lamports (1e9 per SOL) but fee_rate
// was designed for whole SOL values, you're off by 1e9
// ❌ SPL Token decimal mismatch
// Token A has 6 decimals (USDC), Token B has 9 decimals
let price = reserve_a / reserve_b;
// [A·1e6] / [B·1e9] = [A/B · 1e-3] ← scale is wrong!
// ✅ Normalize first
let price = reserve_a * 10u64.pow(token_b_decimals)
/ (reserve_b * 10u64.pow(token_a_decimals));
Solana's account model also introduces a unique dimensional issue: rent-exempt minimums. The minimum balance for rent exemption is in lamports (a monetary quantity), but programs sometimes confuse it with account size (a storage quantity). These are completely different dimensions.
Making It Systematic: The Reserve Protocol Approach
The most effective dimensional safety system I've seen in production is Reserve Protocol's annotation convention:
/// @param amount D18{tok} Amount of tokens to deposit
/// @param price D27{UoA/tok} Price per token in unit of account
/// @return value D27{UoA} Total value in unit of account
function calculateValue(uint256 amount, uint256 price)
internal pure returns (uint256 value)
{
// D27{UoA} = D18{tok} × D27{UoA/tok} / D18
value = amount * price / 1e18;
}
Every variable carries both its dimension (tok, UoA, UoA/tok) and its decimal scale (D18, D27). The comment on the computation line shows the dimensional equation. Any reviewer can verify it in seconds.
You don't need to adopt this exact notation. But you need some system. Here's a minimal approach that works:
-
Suffix your variables:
_tok,_usd,_shares,_perSecond -
Comment conversions:
// [USD] = [tok] × [USD/tok] - Group related scales: Keep all 18-decimal values together, all 27-decimal values together
The $137M Question
As of March 2026, over $137M has been lost to DeFi exploits this year. Not all of those are formula bugs — private key compromises (Step Finance, $27M), flash loan oracle manipulation (Makina, $4M), and off-chain infrastructure failures (Resolv, $25M) are all in the mix.
But the formula bugs — the ones where someone divided where they should have multiplied, or forgot to scale between 8 and 18 decimals, or mixed up shares with tokens — those are the most preventable. Dimensional analysis catches them mechanically, without needing to understand the protocol's business logic.
Trail of Bits mentions they've built a Claude plugin for automated dimensional checking, with details coming in a follow-up post. That's exciting. But you don't need to wait for tooling. You need a pen, a piece of paper, and the discipline to write [A/B] next to every variable before you start reading the code.
The formulas will tell you when they're wrong. You just have to listen.
This article is part of the DeFi Security Research series. Follow for weekly deep dives into smart contract vulnerabilities, audit techniques, and defense patterns.
Top comments (0)