- Overview
- Affected Contract
- Root Cause: Unsafe uint160 Downcast
- EVM Mechanics: Why It Silently Wraps
- Attack Vector: Flash Loan Price Manipulation
- Economic Impact
- CVSS v3.1 Breakdown
- Remediation
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.
| 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 |
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.
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.
The attack requires no privileged access and uses only standard DeFi primitives:
- 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.
- Swap into the pool — Use the borrowed funds to execute swaps against the
MarginalV1Pool, driving the pool'ssqrtPriceX96upward past theuint160maximum (type(uint160).max). - Trigger settlement — While the manipulated price is in effect, call the settlement/liquidation function. The contract reads
sqrtPriceX96, casts it touint160, and computes debt settlement costs against the wrapped near-zero value. - Drain the pool — Settle a massive debt position for essentially zero cost, extracting the pool's entire liquidity.
- 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.
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.
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 |
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.