Fixed-Point Arithmetic and Rounding Policy — The Rounding Direction That Preserves Invariants (Preventing Dust Leaks), and LMSR's exp/ln Approximation Error Bound TODO
Concept
The EVM has no floating point, so ratios and prices are handled as fixed-point numbers — integers with an implicit scale factor (e.g., 1e18). Multiplication doubles the scale, so it has to be divided back down, and each such division introduces truncation error whose direction either preserves or breaks the system's invariants. The rule is to always round in the protocol's (pool/contract's) favor: round down what a user receives, round up what a user pays, to prevent a dust leak where repeated trades siphon off the remainder. Cost functions that need exp and ln, like LMSR's, have to be implemented as integer approximations, and you need to work out both the error bound of that approximation and whether the error breaks the cost function's monotonicity or convexity. The thing that ultimately needs verifying isn't the precision of any single operation — it's whether the invariant holds no matter what order the operations run in.
Getting a single rounding direction backwards turns a mathematically tiny error into a free, infinitely repeatable withdrawal path.
Code & Formula
# 고정소수점 산술과 반올림 정책 — 정수 스케일(1e18)로 나눗셈 절사 오차를 다루고,
# "프로토콜에 유리한 방향"으로 반올림해 dust leak(잔여분 누적 착취)을 막는 것을 시연.
SCALE = 10**18 # 고정소수점 스케일 (EVM의 흔한 관례)
def mul_div_floor(a, b, denom):
return (a * b) // denom # 사용자가 "받는" 양 — 내림 (프로토콜에 유리)
def mul_div_ceil(a, b, denom):
return -((-(a * b)) // denom) # 사용자가 "내는" 양 — 올림 (프로토콜에 유리)
price = 3 * SCALE // 7 # 나누어떨어지지 않는 가격 (절사 오차가 필연적으로 생김)
def swap_user_receives(amount_in):
# 사용자가 amount_in 을 내고 price 만큼의 비율로 얼마를 받는지: 내림 처리
return mul_div_floor(amount_in, SCALE, price)
def swap_user_pays(amount_out_wanted):
# 사용자가 amount_out_wanted 를 원할 때 얼마를 내야 하는지: 올림 처리
return mul_div_ceil(amount_out_wanted * price, 1, SCALE)
amount_in = 1_000_000 # 아주 작은 입력 (절사 오차가 상대적으로 크게 드러나도록)
received_correct = swap_user_receives(amount_in) # 내림 (안전)
received_wrong = (amount_in * SCALE) // price if True else None
paid_correct = swap_user_pays(received_correct) # 올림 (안전)
print("price (scaled):", price, "=> approx", price / SCALE)
print("user receives (floor, protocol-favoring):", received_correct)
print("re-quoted amount user must pay (ceil, protocol-favoring):", paid_correct)
print("paid >= amount_in (no value leaked to user via rounding):", paid_correct >= amount_in)
# 반대로 "받는 양"을 올림 처리하면 반복 거래로 프로토콜에서 조금씩 잔여분을 긁어갈 수 있다
def swap_user_receives_UNSAFE(amount_in):
return mul_div_ceil(amount_in, SCALE, price)
unsafe_received = swap_user_receives_UNSAFE(amount_in)
print()
print("UNSAFE variant (rounds in user's favor) receives:", unsafe_received)
print("unsafe > safe by:", unsafe_received - received_correct, "=> repeated trades could drain dust")
docs/code/algorithms/algorithms-89.py
Exercise
Implement fixed-point mulDiv in both round-down and round-up versions, then simulate thousands of rapid buy-then-sell cycles and check whether the pool balance grows or shrinks.
Practical Connection
Verex's LMSR cost function and its CLOB matching/settlement path sit right at the center of this problem, and without documenting the rounding convention in one place, different modules will drift in different directions.
Where it lands in Jayverse
- Verex: write the one rounding-convention document and link every module to it. LMSR's cost function, the CLOB and the settlement path each do their own fixed-point division; a single round-down-what-user-receives, round-up-what-user-pays rule, stated once, is what stops them drifting apart.
- Auditor: turn the buy-then-sell cycle test into a standing CI check, not a one-time exercise. Simulating thousands of rapid trade cycles and asserting pool balance never leaks is the concrete test the Auditor row should require before any change to Verex's fixed-point math ships.
Key expressions
| Expression | 뜻 · 쓰이는 자리 |
|---|---|
| dust leak | (먼지처럼 아주 작은 금액이 새어나가는) 더스트 유출 · 반올림 방향을 잘못 잡으면 생기는 누수 · "a dust leak where repeated trades siphon off the remainder" |
| siphon off | (조금씩) 빼돌리다, 빨아가다 · 반복 거래로 잔여분을 조금씩 빼가는 것 · "repeated trades siphon off the remainder" |
| in ~'s favor | ~에 유리한 방향으로 · 반올림을 항상 프로토콜 쪽에 유리하게 하는 규칙 · "always round in the protocol's (pool/contract's) favor" |
| truncation error | 절삭 오차 · 나눗셈으로 스케일을 되돌릴 때 생기는 오차 · "each such division introduces truncation error" |
| no matter what order | 순서가 어떻든 상관없이 · 연산 순서와 무관하게 불변식이 지켜져야 함 · "whether the invariant holds no matter what order the operations run" |
| monotonicity | 단조성(항상 증가하거나 항상 감소하는 성질) · 근사 오차가 이 성질을 깨는지 확인해야 함 · "breaks the cost function's monotonicity or convexity" |
| drift | (기준 없이 각자) 제멋대로 어긋나다 · 반올림 규칙을 안 정해두면 모듈마다 다르게 틀어짐 · "different modules will drift in different directions" |
| LMSR | 로그 마켓 스코어링 룰(Logarithmic Market Scoring Rule, LMSR) · exp/ln 근사가 필요한 예측시장 비용함수, 이 카드의 오차 분석 대상. "LMSR's exp/ln Approximation Error Bound" |
| CLOB | 중앙집중형 지정가 주문서(Central Limit Order Book, CLOB) · 매칭·정산 경로에서 반올림 규칙이 함께 적용되는 대상. "its CLOB matching/settlement path sit right at the center" |
| mulDiv | mulDiv(고정소수점 곱셈 후 나눗셈) · 스케일을 되돌리는 표준 고정소수점 연산, 반올림 방향 정책이 적용되는 지점. "Implement fixed-point mulDiv in both round-down and round-up versions" |
If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/dev-100-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/.