tradi3_ Kurt Landman
Public report archive

Metric T3-003

Fixed-E8 synthetic-ratio truncation lets traders take 47.83% of one-sided RSR/cbBTC LP principal

AnchoredPriceProvider quantizes two-feed synthetic ratios into integer E8 units via Math.mulDiv(mid, 1e8, mid2) before converting to Q64, flooring the live RSR/cbBTC ratio from 1.9198e-8 to 1e-8 and letting traders drain 47.83% of one-sided LP principal.

Severity
Medium
Outcome
paid
Competition
Metric
Vulnerability class
AMM math / rounding
Published

Summary

Accepted and paid Medium finding in the Sherlock Metric contest (2026-07-metric @ d2706927622678939ad77d8389159973192ea19c).

When AnchoredPriceProvider prices a pair using a two-leg synthetic oracle ratio (base / quote), it divides the two E8 oracle legs into an intermediate integer E8 value before converting the result into the pool’s Q64 fixed-point price domain:

mid = Math.mulDiv(mid, ORACLE_DECIMALS, mid2);

For any pair whose true price ratio sits in [1e-8, 2e-8), integer flooring in E8 replaces the midpoint with 1e-8 (up to a 50% understatement), and ratios below 1e-8 floor to 0. The subsequent conversion to Q64 (2^64 ≈ 1.84e19) cannot recover the discarded fraction.

Root cause

In smart-contracts-poc/contracts/AnchoredPriceProvider.sol (lines 258-271):

if (_quote != bytes32(0)) {
    (uint256 mid2, uint256 spreadBps2, , bool ok2) = _readLeg(_quote);
    if (!ok2 || mid2 == 0) return (0, type(uint128).max);
    mid = Math.mulDiv(mid, ORACLE_DECIMALS, mid2);
    spreadBps += spreadBps2;
}

return _computeBidAsk(mid, spreadBps);

Only after mid has been quantized to ORACLE_DECIMALS (1e8) does _bandEdge() scale mid by Q64:

return Math.mulDiv(mid, Q64 * edgeFactor, STEP_DENOM, rounding);

Concrete pair & oracle reproduction

On Ethereum mainnet, canonical token sorting orders Reserve Rights (RSR, 0x320623b8E4fF03373931769A31Fc52A4E78B5d70) as token0 and Coinbase Wrapped BTC (cbBTC, 0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf) as token1:

  • Pyth feed 175 (Crypto.RSR/USD): price = 124,495 ($0.00124495, exponent -8, confidence 55 -> 5 bps)
  • Pyth feed 397 (Crypto.CBBTC/USD): price = 6,484,651,280,510 ($64,846.51280510, exponent -8, confidence 4,207,967,514 -> 7 bps)
true RSR/cbBTC ratio      = 124,495 / 6,484,651,280,510
                          = 1.91984109267644e-8 (1.919841 E8 units)
stored E8 midpoint        = floor(1.91984109267644) = 1 (1e-8)
midpoint understatement   = 47.91235567%

Through the real AnchoredProviderFactory, MetricOmmPoolFactory, and MetricOmmPool.swap():

  1. An LP supplies 10,000,000 RSR (worth 0.19198411 cbBTC at the authenticated oracle snapshot) into a token0-only bin.
  2. A trader buys all 10,000,000 RSR for 0.10014002 cbBTC.
  3. The LP loses 0.09184409 cbBTC (47.83% of principal, or $5,955.77) in a single swap.

Compute synthetic ratios directly in Q64 (or a high-precision intermediate such as E18/Q128) before applying bid/ask spread bounds, rather than quantizing the ratio to E8 first:

midX64 = Math.mulDiv(mid, Q64, mid2, rounding);