Skip to content

Latest commit

 

History

History
146 lines (101 loc) · 6.81 KB

File metadata and controls

146 lines (101 loc) · 6.81 KB

Vulnerability Technical Details

Table of Contents

  1. Overview
  2. Affected Contract
  3. Root Cause: Unsafe uint160 Downcast
  4. EVM Mechanics: Why It Silently Wraps
  5. Attack Vector: Flash Loan Price Manipulation
  6. Economic Impact
  7. CVSS v3.1 Breakdown
  8. Remediation

Overview

CVE-2026-4931 is a CWE-197: Numeric Truncation Error in the MarginalV1Pool smart contract deployed on Ethereum Mainnet. The contract stores prices using a 256-bit Q96 fixed-point format but silently narrows them to 160 bits during settlement calculations. An attacker who pushes the pool price above the 160-bit maximum causes the high bits to be discarded, making the protocol believe a $100,000,000 debt is worth approximately zero.


Affected Contract

Attribute Value
Contract Name MarginalV1Pool
Proxy Address 0x3A6C55Ce74d940A9B5dDDE1E57eF6e70bC8757A7
Network Ethereum Mainnet
Vulnerability Status Patched at block 24,386,649 (Feb 04, 2026)
Patched Implementation 0xd8be1b2571b7c43b77ff3ae87bc6f0a23fa224b8

Root Cause: Unsafe uint160 Downcast

The MarginalV1Pool contract represents pool prices using Q96 fixed-point arithmetic — a 256-bit unsigned integer where the value has been multiplied by 2^96 to preserve fractional precision. This is the same approach used by Uniswap V3.

During debt settlement and liquidation flows, the code casts this 256-bit value directly to uint160:

// VULNERABLE — no bounds check
uint160 price = uint160(sqrtPriceX96);

uint160 can hold values up to 2^160 - 1 (approximately 1.46 × 10^48). When sqrtPriceX96 exceeds this limit, Solidity does not revert. It silently truncates the value.


EVM Mechanics: Why It Silently Wraps

At the EVM bytecode level, a cast from a larger integer type to a smaller one is implemented as a bitwise AND operation that masks off the high bits:

uint160(x)  ≡  x AND (2^160 - 1)
            ≡  x AND 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

There is no overflow trap, no revert, and no event emitted. The narrowed value is used directly in subsequent arithmetic as if it were the true price.

Worked example from the PoC:

Value
sqrtPriceX96 (Q96 representation) 1461501637330902918203684832716283019655932599981
type(uint160).max 1461501637330902918203684832716283019655932542975
Bits that overflow 160 bits high-order bits silently dropped
Result after uint160(...) cast 57005
Precision loss 99.999999%

The contract then uses 57005 (instead of 1461501637330902918203684832716283019655932599981) in all downstream debt valuation math, making the $100M USDC debt appear to be worth 0.000000000000057005 ETH.


Attack Vector: Flash Loan Price Manipulation

The attack requires no privileged access and uses only standard DeFi primitives:

  1. Obtain a flash loan — Borrow a large amount of the pool's base or quote token from any flash loan provider (Aave, Uniswap, Balancer, etc.). This is permissionless.
  2. Swap into the pool — Use the borrowed funds to execute swaps against the MarginalV1Pool, driving the pool's sqrtPriceX96 upward past the uint160 maximum (type(uint160).max).
  3. Trigger settlement — While the manipulated price is in effect, call the settlement/liquidation function. The contract reads sqrtPriceX96, casts it to uint160, and computes debt settlement costs against the wrapped near-zero value.
  4. Drain the pool — Settle a massive debt position for essentially zero cost, extracting the pool's entire liquidity.
  5. Repay the flash loan — Return the borrowed tokens (plus a small fee). Net profit equals the full value of the drained pool.

The attack is fully atomic — it succeeds or fails in a single transaction, eliminating market risk.


Economic Impact

The figures below are taken directly from the PoC execution (see marginal_poc_output.txt):

Original Value:    1461501637330902918203684832716283019655932599981
Truncated Value:   57005
Precision Loss:    99.999999%

Actual Debt:       $100,000,000 USDC
Settlement Cost:   0.000000000000057005 ETH
Attacker Profit:   $99,999,999.99

Beyond the direct theft, the attack corrupts all protocol accounting for every pool that shares the affected logic:

  • Lenders lose their entire deposited principal.
  • The protocol accumulates unrecoverable bad debt.
  • Subsequent users cannot accurately assess pool solvency.

CVSS v3.1 Breakdown

Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N
Score: 9.1 (Critical)

Metric Value Rationale
Attack Vector (AV) Network Exploitable remotely via flash loans — no physical or local access needed
Attack Complexity (AC) Low Only standard DeFi flash loan primitives required; no brute force or race condition
Privileges Required (PR) None Any externally-owned account can execute; no admin or protocol role needed
User Interaction (UI) None Fully autonomous single-transaction attack
Scope (S) Changed Impact extends beyond the attacker's own session to all pool lenders and liquidity providers
Confidentiality (C) High Complete extraction of all deposited funds is possible
Integrity (I) High All debt accounting is corrupted — 99.999999% precision loss
Availability (A) None N/A — the harm is financial drain, not a service disruption

Remediation

The Marginal V1 team patched the vulnerability at block 24,386,649 by injecting OpenZeppelin's SafeCast library into the contract bytecode. The SafeCast library replaces the silent truncation with an explicit revert:

// VULNERABLE (original):
uint160 price = uint160(sqrtPriceX96);

// SAFE (patched — Option A: SafeCast):
uint160 price = SafeCast.toUint160(sqrtPriceX96);
// Reverts with: "SafeCast: value doesn't fit in 160 bits"

// SAFE (patched — Option B: manual check):
require(sqrtPriceX96 <= type(uint160).max, "Price overflow");
uint160 price = uint160(sqrtPriceX96);

Forensic proof that the patch uses SafeCast: The exact error string SafeCast: value doesn't fit in 160 bits (hex: 53616665436173743a2076616c756520646f65736e27742066697420696e203136302062697473) is present in the bytecode of the patched implementation 0xd8be1b2571b7c43b77ff3ae87bc6f0a23fa224b8.

See Stealth Patch Evidence for full on-chain proof.