Fixed-Point Arithmetic (Q64.96) TODO
Concept
Fixed-point representation encodes a real number as a single integer with an implicit binary point position; the Qm.n notation means m bits for the integer part and n bits for the fractional part, and the actual value is the stored integer divided by 2 to the power n. Q64.96 puts 96 bits in the fractional part and fits within 160 bits — best known as the format Uniswap v3 uses to store the square root of price. Addition and subtraction work as plain integer operations, but multiplication produces a result with 2n fractional bits that must be scaled back down by dividing by 2^n, and division must multiply first before dividing — which makes intermediate overflow the single biggest risk. That's why implementations need mulDiv-style logic that carries a 512-bit intermediate result, and since division inevitably truncates low-order bits, standard practice is to consistently round in the direction that doesn't favor the user over the protocol.
The EVM has no floating point, so every price, interest, and fee calculation runs on fixed-point arithmetic, and getting the order of a single multiply-then-divide wrong can turn into an overflow or a rounding vulnerability that favors the attacker.
Code & Formula
# Day 32 — 고정소수점 산술 (Q64.96)
# 정수 하나에 소수부 96비트를 암묵적으로 두는 Q64.96 형식을 흉내내어 곱셈/나눗셈 스케일링을 보여준다.
Q96 = 96
SCALE = 1 << Q96 # 2^96
def to_fixed(x: float) -> int:
return int(round(x * SCALE))
def from_fixed(x_fixed: int) -> float:
return x_fixed / SCALE
def fixed_mul(a_fixed: int, b_fixed: int) -> int:
# 곱셈 결과의 소수부는 2*96비트가 되므로 다시 SCALE로 나눠 되돌린다.
return (a_fixed * b_fixed) // SCALE
def fixed_div(a_fixed: int, b_fixed: int) -> int:
# 나눗셈은 먼저 SCALE을 곱해 정밀도를 보존한 뒤 나눈다.
return (a_fixed * SCALE) // b_fixed
price_a = to_fixed(1.0001) # Uniswap v3 스타일 sqrtPrice 유사값
price_b = to_fixed(2.5)
product_fixed = fixed_mul(price_a, price_b)
quotient_fixed = fixed_div(price_a, price_b)
print(f"a = {from_fixed(price_a)}, b = {from_fixed(price_b)}")
print(f"a * b (fixed) = {from_fixed(product_fixed):.10f} (참값 {1.0001 * 2.5})")
print(f"a / b (fixed) = {from_fixed(quotient_fixed):.10f} (참값 {1.0001 / 2.5})")
print(f"\n원시 정수 a_fixed 자릿수: {len(str(price_a))} (Q64.96은 160비트 안에 들어감)")
Exercise
Implement Q64.96 multiplication and division in Solidity, deliberately find inputs that trigger intermediate overflow, then fix it with a mulDiv-style approach and verify it produces the correct value on the same inputs.
Practical Connection
LMSR price calculation and settlement amounts both need fixed-point approximations of exponentials and logarithms, so the choice of scale and rounding direction directly determines whether the protocol's balance can leak.
Where it lands in Jayverse
- Verex: use mulDiv-style 512-bit intermediates for every LMSR and settlement calculation. Never multiply and divide as two separate operations, and pick a rounding direction that favors the protocol over the user.
- Verex: fuzz-test Q64.96 math for intermediate overflow before any mainnet-bound deployment. Deliberately search for overflow inputs in the price and settlement math, mirroring the exercise, and fix them with the mulDiv approach.
- Auditor: require the rounding direction to be a reviewable, explicit line. For every fixed-point calculation in Verex or DeFi, a wrong rounding direction is a silent balance-leak vulnerability, so it should never be implicit.
Key expressions
| Expression | 뜻 · 쓰이는 자리 |
|---|---|
| scale back down | (원래 규모로) 다시 축소하다 · 곱셈 후 늘어난 소수부 비트를 되돌려 줄이는 과정. "must be scaled back down by dividing by 2^n" |
| the single biggest risk | 가장 큰 단일 위험 요소 · 중간 오버플로가 최대 위험임을 강조할 때. "makes intermediate overflow the single biggest risk" |
| carry (an intermediate result) | (계산 중간값을) 담아 처리하다 · 512비트 중간 결과를 유지하는 mulDiv 로직. "carries a 512-bit intermediate result" |
| truncate | (하위 비트를) 잘라내다, 버리다 · 나눗셈 과정에서 정밀도가 손실되는 이유. "division inevitably truncates low-order bits" |
| favor X over Y | Y보다 X에 유리하게 처리하다 · 반올림 방향이 프로토콜보다 사용자에게 유리하지 않게 한다는 원칙. "doesn't favor the user over the protocol" |
| leak (balance) | (자금·잔고가) 의도치 않게 새어나가다 · 반올림 실수가 프로토콜 잔고 유출로 이어지는 상황. "the protocol's balance can leak" |
| Q64.96 | 정수부 64비트, 소수부 96비트의 고정소수점 표기(Qm.n notation) · Uniswap v3가 가격의 제곱근을 저장하는 포맷. "Q64.96 puts 96 bits in the fractional part" |
| Uniswap v3 | 대표적인 탈중앙 거래소(DEX) 프로토콜 · Q64.96 포맷으로 가격의 제곱근을 저장하는 주체. "the format Uniswap v3 uses to store the square root" |
| LMSR | 로그 시장 스코어링 규칙(Logarithmic Market Scoring Rule) · 예측시장 가격 결정에 쓰이는 메커니즘, 고정소수점 근사가 필요한 지점. "LMSR price calculation and settlement amounts both need fixed-point approximations" |
| mulDiv | 곱한 뒤 나누되 512비트 중간값을 유지해 오버플로를 막는 함수 패턴(mulDiv) · 정밀도 손실과 오버플로를 동시에 방지하는 구현 방식. "implementations need mulDiv-style logic that carries a 512-bit intermediate result" |
If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/math-50-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.