Skip to content

Latest commit

 

History

History
303 lines (216 loc) · 15.7 KB

File metadata and controls

303 lines (216 loc) · 15.7 KB

LSMath — Developer Reference

Technical Documentation for the LS-LMSR Mathematics Library


📋 Overview

LSMath is a high-precision, gas-optimized library implementing the Logarithmic-Scale Logarithmic Market Scoring Rule (LS-LMSR). It provides the core mathematical primitives required for automated market makers (AMMs) to calculate costs, prices, and liquidity parameters with 18-decimal fixed-point precision.


🔍 Table of Contents

  1. Library Architecture
  2. System Constants
  3. Error Catalogue
  4. Fixed-Point Arithmetic
  5. Core LS-LMSR Functions
  6. Mathematical Primitives
  7. Utility Functions
  8. Validation Helpers
  9. Numerical Stability & Precision

1. Library Architecture

LSMath is designed as a Solidity library. All functions are internal, ensuring they are inlined into consuming contracts via JUMP instructions, eliminating external call overhead and proxy complexities.

Logical Components

graph TD
    A[LSMath Library] --> B[Core LS-LMSR]
    A --> C[Math Primitives]
    A --> D[Fixed-Point Engine]
    A --> E[Validation & Utility]

    B --> B1[costFunction]
    B --> B2[getPrice/getAllPrices]
    B --> B3[liquidityParameter]

    C --> C1[exp - Taylor Series]
    C --> C2[ln - Binary Search]

    D --> D1[mulScale - Round Half Up]
    D --> D2[divScale - Round Half Up]
Loading

Compiler Rationale:

  • Determinism: Strict pragma ensures consistent bytecode across environments.
  • Safety: Built-in checked arithmetic prevents silent overflows.
  • Optimization: Explicit unchecked blocks are used for loop increments where bounds are guaranteed (MAX_OUTCOMES = 100).

2. System Constants

All constants are uint256 internal constant, embedded directly into the bytecode.

Constant Raw Value Decoded Purpose
SCALE 1000000000000000000 1e18 Base for 18-decimal fixed-point arithmetic.
MIN_ALPHA 1000000000000 1e12 (= 0.000001) Lower bound to prevent numerical collapse.
MAX_ALPHA 200000000000000000 2e17 (= 0.2) Upper bound (20% max commission).
MAX_OUTCOMES 100 100 Hard cap on array lengths to bound gas costs.
LN_2 693147180559945309 ln(2) · 1e18 Used in exp range reduction and ln search.
MAX_EXP_INPUT 135305999368893231589 ≈ 135.3 · 1e18 Maximum safe input to exp() before overflow.
MIN_TERM 100 100 (raw units) Taylor series early-exit threshold.

Note

On MAX_EXP_INPUT: This limit ensures that the bit-shifting in the range-reduction step of exp() does not exceed the 256-bit capacity of a uint256. Derived from: ln(2^255 / 1e18) ≈ 135.3.


3. Error Catalogue

LSMath uses custom errors (Solidity 0.8.4+) for gas efficiency and programmatic error handling.

Error Source Trigger Condition
InvalidAlpha() liquidityParameter alpha < MIN_ALPHA or alpha > MAX_ALPHA.
EmptyQuantities() liquidityParameter, calculateWorstCaseLoss, hasOutcomeIndependentProfit quantities.length == 0.
NegativeQuantity() (reserved) Declared but not currently thrown; uint256 type enforces non-negativity.
ZeroQuantitySum() liquidityParameter Σqᵢ == 0 (all quantities are zero).
ArithmeticOverflow() liquidityParameter, costFunction, getAllPrices, sumOfPrices, calculateTradeCost Integer overflow detected in accumulation.
MultiplicationOverflow() mulScale, divScale a * b overflows uint256.
DivisionByZero() divScale Denominator b == 0.
InvalidMarketState() costFunction, getAllPrices sumExp == 0 or denominator == 0.
InsufficientOutcomes() (reserved) Declared; outcome count < 2 is handled by validateQuantities returning false.
ArrayLengthMismatch() calculateWorstCaseLoss, hasOutcomeIndependentProfit Input arrays have different lengths.
InvalidLogInput() ln x == 0 or x < SCALE (logarithm of values less than 1 would be negative; not supported).
ExponentialOverflow() exp x > MAX_EXP_INPUT, or final bit-shift would overflow uint256.

Note

On NegativeQuantity and InsufficientOutcomes: These errors are declared but not actively thrown. NegativeQuantity is impossible with uint256, and InsufficientOutcomes is handled by validateQuantities().

🛡️ Error Handling in Consuming Contracts

Developers should use try/catch blocks to handle custom errors from LSMath gracefully:

try LSMath.costFunction(quantities, alpha) returns (uint256 cost) {
    // Success: proceed with trade
} catch (bytes memory reason) {
    bytes4 selector = bytes4(reason);
    if (selector == LSMath.InvalidAlpha.selector) { /* Handle alpha error */ }
    if (selector == LSMath.ExponentialOverflow.selector) { /* Handle overflow */ }
    // etc.
}

4. Fixed-Point Arithmetic

The library adheres to 18-decimal fixed-point conventions:

  • 1.0 is represented as 1e18 = 1000000000000000000
  • 0.5 is represented as 5e17
  • 2.5 is represented as 25e17

Basic Operations

  • Addition/Subtraction: Standard + and - (no scaling needed).
  • Multiplication (mulScale): (a * b) in 18-decimal produces a 36-decimal value. Divide by SCALE to return to 18-decimal: (a * b) / SCALE.
  • Division (divScale): To divide a by b in fixed-point: (a * SCALE) / b.

Rounding Strategy: Round-Half-Up

Unlike standard Solidity truncation, LSMath uses round-half-up (also called round-to-nearest) rather than truncation:

  • mulScale: (a * b + SCALE/2) / SCALE
  • divScale: (a * SCALE + b/2) / b

This halves the expected rounding error per operation and eliminates the systematic downward bias that pure truncation introduces. In long chains of fixed-point operations, unbiased rounding is important for accuracy.

Overflow Detection

Both mulScale and divScale detect overflow using the standard post-multiplication division check (e.g., if (product / a != b) revert MultiplicationOverflow();). This is crucial for maintaining safety in uint256 arithmetic.


5. Core LS-LMSR Functions

5.1 liquidityParameter

Signature: function liquidityParameter(uint256[] memory quantities, uint256 alpha) internal pure returns (uint256 b, uint256 sumQ)

This function calculates the b parameter, which controls price sensitivity, and the sum of quantities sumQ.

Step-by-step:

  1. Validate inputs: Checks alpha bounds, quantities.length, and MAX_OUTCOMES.
  2. Accumulate Σqᵢ: Sums quantities with post-addition overflow detection (if (sumQ < qi) revert ArithmeticOverflow();).
  3. Compute b: b = (alpha * sumQ) / SCALE.
  4. Clamp to minimum 1: If b is 0, it is clamped to 1 wei to prevent division-by-zero downstream and ensure maximum price sensitivity in thinly-funded markets.

Tip

The tuple return of (b, sumQ) eliminates redundant sumQ recomputation in callers like getAllPrices(), saving ~300–500 gas per price query.

5.2 costFunction

Signature: function costFunction(uint256[] memory quantities, uint256 alpha) internal pure returns (uint256 cost)

Implements the LS-LMSR cost identity: $$C(q) = b(q) \cdot \ln\left(\sum e^{q_i/b(q)}\right)$$

Key Implementation Details:

  • Uses the Log-Sum-Exp trick (see Section 9.1) to prevent overflow when individual exp(q_i/b) values are large.
  • Ensures the invariant $C(q) \geq \max(q_i)$ is maintained through numerical clamping.

5.3 getAllPrices()

Signature: function getAllPrices(uint256[] memory quantities, uint256 alpha) internal pure returns (uint256[] memory prices)

Calculates the price of all outcomes in a single pass.

  1. Liquidity: Calls liquidityParameter to get b and sumQ.
  2. Exponentials: Calculates exp(q_i/b) for each outcome using the Log-Sum-Exp trick.
  3. Normalization: Divides each exponential by the sum of all exponentials to get the price.

5.4 getPrice()

Signature: function getPrice(uint256[] memory quantities, uint256 alpha, uint256 outcomeIndex) internal pure returns (uint256 price)

Calculates the price for a single outcome. More gas-efficient than getAllPrices if only one price is needed, but uses the same underlying Log-Sum-Exp logic for stability.

5.5 calculateTradeCost()

Signature: function calculateTradeCost(uint256[] memory quantities, uint256[] memory delta, uint256 alpha) internal pure returns (uint256 cost)

Calculates the cost of a trade by computing costFunction(quantities + delta) - costFunction(quantities).

5.6 sumOfPrices()

Signature: function sumOfPrices(uint256[] memory quantities, uint256 alpha) internal pure returns (uint256 sum)

Calculates the sum of all outcome prices. In a perfectly balanced LS-LMSR, this should be exactly 1.0 (1e18). Used for validation and invariant checks.


6. Mathematical Primitives

6.1 exp() Implementation

The exponential function uses a combination of range reduction and Taylor series expansion.

  1. Range Reduction: Uses the identity $e^x = 2^k \cdot e^r$, where $r = x - k \cdot \ln(2)$ and $r \in [0, \ln(2))$.
  2. Taylor Expansion: Approximates $e^r$ using 18 terms: $$e^r \approx 1 + r + \frac{r^2}{2!} + \frac{r^3}{3!} + \dots$$
  3. Reconstruction: The result is shifted left by $k$ bits to restore the original scale.

6.2 ln() Implementation

The natural logarithm uses a bit-by-bit iterative squaring algorithm (binary search in the log domain).

  1. Normalization: Scales the input into the range $[1, 2)$ by tracking the number of bit-shifts (intPart).
  2. Iterative Squaring: For 59 iterations, the value is squared. If the result exceeds 2.0, the corresponding bit of the logarithm is set, and the value is halved.
  3. Precision: Achieves $\approx 1.2 \times 10^{-18}$ resolution, matching the fixed-point floor.

6.3 mulScale & divScale

These functions provide the backbone for fixed-point arithmetic with overflow protection and round-half-up logic.


7. Utility Functions

7.1 calculateWorstCaseLoss

Signature: function calculateWorstCaseLoss(uint256[] memory quantities, uint256[] memory initialQuantities, uint256 alpha) internal pure returns (uint256 worstCaseLoss)

Implements Definition 4.7 from the paper: Loss = C(q₀) - C(q) + max(qᵢ).

Key Logic:

  • Validates input array lengths.
  • Calculates costCurrent and costInitial using costFunction.
  • Finds maxQ (maximum quantity).
  • Handles edge cases where costCurrent < maxQ (theoretically impossible, practically guarded).
  • Returns 0 when C(q) - max(qᵢ) ≥ C(q₀) (market maker is in outcome-independent profit).

7.2 hasOutcomeIndependentProfit

Signature: function hasOutcomeIndependentProfit(uint256[] memory quantities, uint256[] memory initialQuantities, uint256 alpha) internal pure returns (bool hasProfit, uint256 profit)

Computes R(q) = C(q) - max(qᵢ) - C(q₀) and tests its sign.

Key Logic:

  • Uses int256 arithmetic for revenue calculation because it can be negative.
  • Returns true for hasProfit if revenue > 0, along with the profit amount.

8. Validation Helpers

These functions provide pre-validation checks, allowing consuming contracts to handle invalid states gracefully without necessarily reverting.

validateQuantities

Signature: function validateQuantities(uint256[] memory quantities) internal pure returns (bool)

Returns false (does not revert) for:

  • quantities.length < 2 (single-outcome and empty markets are invalid)
  • quantities.length > MAX_OUTCOMES
  • Σqᵢ == 0 (all zeros)

Returns true otherwise. The consuming contract is responsible for deciding whether to revert or handle an invalid return.

validateAlpha

Signature: function validateAlpha(uint256 alpha) internal pure returns (bool)

Returns true iff alpha ∈ [MIN_ALPHA, MAX_ALPHA]. Used for external pre-validation before calling core functions that revert on invalid alpha.


9. Numerical Stability & Precision

9.1 Log-Sum-Exp Trick

Both costFunction and getAllPrices must compute Σ exp(qᵢ/b). For large markets with high outstanding quantities, the individual exp(qᵢ/b) values can be enormous, causing overflow when accumulated.

The Identity: $$\ln\left(\sum e^{x_i}\right) = m + \ln\left(\sum e^{x_i - m}\right)$$ where $m = \max(x_i)$.

Why this matters: Because $x_i - m \leq 0$ for all $i$, each $e^{x_i - m} \in (0, 1]$. The shifted exponentials are bounded and cannot overflow on their own; only their sum might, but with MAX_OUTCOMES = 100 and each term $\leq \text{SCALE} = 1e18$, the maximum sum is $100 \cdot 1e18 = 1e20$, safely within uint256.

Implementation Detail for Negative-Shift Branch: When ratio < maxRatio, we need $e^{\text{ratio} - \text{maxRatio}} = e^{-(\text{maxRatio} - \text{ratio})}$. In fixed-point unsigned arithmetic, this is computed as: $$e^{-\text{diff}} = \frac{1}{e^{\text{diff}}} = \frac{\text{SCALE}^2}{e^{\text{diff}}}$$ The numerator is SCALE² = 1e36 because exp(diff) is already in 18-decimal. Dividing by it gives (SCALE²) / (e^diff · SCALE) = SCALE / e^diff, which is $e^{-\text{diff}}$ in 18-decimal. This is correct.

9.2 Rounding Policy

Component Precision / Error
Fixed-Point Floor $10^{-18}$ (1 wei)
Rounding mulScale and divScale use round-half-up (biased toward positive). This provides an expected rounding error $\approx 0$ (unbiased) for random inputs and a worst-case per-operation error of $\pm 0.5$ ULP ($0.5e-18$).
Error Accumulation In getAllPrices, with $\approx 3 + 5n$ fixed-point operations (where $n$ is outcomes), for $n=100$, the expected accumulated error is $\approx 503 \cdot 0.25e-18 \approx 1.3e-16$. This is within the precision budget for practical use cases.
Taylor Series (exp) For $x \in [0, \ln(2))$, 18 terms provide error $&lt; (\ln(2))^{18} / 18! \approx 10^{-18}$. The early-exit at MIN_TERM = 100 raw units (1e-16 in 18-decimal) introduces negligible additional error.
Binary Search (ln) 59 iterations provide resolution of $\ln(2) / 2^{59} \approx 1.2e-18$, approximately 1 ULP, matching the fixed-point precision floor.
Cost Function Invariant Clamp The clamp if (cost < maxQ) cost = maxQ introduces at most maxQ - cost_exact error, bounded by the fixed-point truncation in the Log-Sum-Exp sum. In practice, this difference is less than $1e-12$ relative to maxQ for realistic market states.

Disclaimer: This reference is intended for developers. Always verify mathematical invariants in the context of your specific smart contract integration.

Back to Top