Workspace IndexMath › Day 32

Fixed-Point Arithmetic (Q64.96) TODO

Math · Day 32 / 52 · October — Calculus & Optimization (Day 27-34)

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.

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/.


한국어

고정소수점 산술(Q64.96) TODO

Math · Day 32 / 52 · 10월 — 미적분·최적화 (Day 27–34)

개념

고정소수점은 정수 하나에 이진 소수점의 위치를 암묵적으로 정해 실수를 표현하는 방식으로, Qm.n 표기는 정수부 m비트와 소수부 n비트를 뜻하며 실제 값은 저장된 정수를 2의 n제곱으로 나눈 값이다. Q64.96은 소수부를 96비트로 두는 형식으로 160비트 안에 담기며, Uniswap v3가 가격의 제곱근을 이 형식으로 저장하는 것으로 잘 알려져 있다. 덧셈과 뺄셈은 정수 연산 그대로지만, 곱셈은 결과의 소수부가 2n비트가 되므로 다시 2의 n제곱으로 나눠 스케일을 되돌려야 하고 나눗셈은 반대로 먼저 곱해야 해서, 중간값 오버플로가 가장 큰 위험 요소가 된다. 그래서 512비트 중간 결과를 다루는 mulDiv 계열 구현이 필요하며, 나눗셈에서 하위 비트가 버려지는 절단 오차는 불가피하므로 반올림 방향을 프로토콜에 불리하지 않은 쪽으로 일관되게 정하는 것이 표준 관행이다.

EVM에는 부동소수점이 없어 모든 가격·이자·수수료 계산이 고정소수점으로 이루어지고, 곱셈과 나눗셈의 순서 하나가 오버플로나 사용자에게 유리한 반올림 취약점으로 이어진다.

코드 · 수식

# 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비트 안에 들어감)")

연습

Q64.96 곱셈과 나눗셈을 Solidity로 구현해 중간 오버플로가 발생하는 입력을 직접 찾아낸 뒤, mulDiv 방식으로 고쳐 같은 입력에서 정확한 값이 나오는지 검증하라.

실무 · Verex 연결

LMSR 가격 계산과 정산 금액 산출은 지수와 로그를 고정소수점으로 근사해야 하므로, 스케일 선택과 반올림 방향이 곧 프로토콜에서 잔액이 새는지 여부를 결정한다.

공부한 날 원본 커리큘럼(docs/knowledge/math-50-curriculum.md)의 이 줄에 노트 링크와 ✅ 를 붙이면, 이 자리는 노트 본문으로 바로 이어집니다. 노트 없이 이 페이지에 바로 적어도 됩니다 — 다만 다시 생성하면 덮어쓰이므로, 남길 글은 docs/algorithms/ 의 마크다운으로 쓰는 편이 안전합니다.

← 31. 뉴턴법/고정점 반복(StableSwap 필수)33. 테일러 급수(1차 근사) →