Technical Documentation for the LS-LMSR Mathematics Library
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.
- Library Architecture
- System Constants
- Error Catalogue
- Fixed-Point Arithmetic
- Core LS-LMSR Functions
- Mathematical Primitives
- Utility Functions
- Validation Helpers
- Numerical Stability & Precision
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.
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]
Compiler Rationale:
- Determinism: Strict pragma ensures consistent bytecode across environments.
- Safety: Built-in checked arithmetic prevents silent overflows.
- Optimization: Explicit
uncheckedblocks are used for loop increments where bounds are guaranteed (MAX_OUTCOMES = 100).
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.
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().
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.
}The library adheres to 18-decimal fixed-point conventions:
1.0is represented as1e18 = 10000000000000000000.5is represented as5e172.5is represented as25e17
- Addition/Subtraction: Standard
+and-(no scaling needed). - Multiplication (
mulScale):(a * b)in 18-decimal produces a 36-decimal value. Divide bySCALEto return to 18-decimal:(a * b) / SCALE. - Division (
divScale): To divideabybin fixed-point:(a * SCALE) / b.
Unlike standard Solidity truncation, LSMath uses round-half-up (also called round-to-nearest) rather than truncation:
mulScale:(a * b + SCALE/2) / SCALEdivScale:(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.
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.
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:
- Validate inputs: Checks
alphabounds,quantities.length, andMAX_OUTCOMES. - Accumulate
Σqᵢ: Sums quantities with post-addition overflow detection (if (sumQ < qi) revert ArithmeticOverflow();). - Compute
b:b = (alpha * sumQ) / SCALE. - Clamp to minimum 1: If
bis 0, it is clamped to1 weito 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.
Signature: function costFunction(uint256[] memory quantities, uint256 alpha) internal pure returns (uint256 cost)
Implements the LS-LMSR cost identity:
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.
Signature: function getAllPrices(uint256[] memory quantities, uint256 alpha) internal pure returns (uint256[] memory prices)
Calculates the price of all outcomes in a single pass.
- Liquidity: Calls
liquidityParameterto getbandsumQ. - Exponentials: Calculates
exp(q_i/b)for each outcome using the Log-Sum-Exp trick. - Normalization: Divides each exponential by the sum of all exponentials to get the price.
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.
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).
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.
The exponential function uses a combination of range reduction and Taylor series expansion.
-
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))$ . -
Taylor Expansion: Approximates
$e^r$ using 18 terms:$$e^r \approx 1 + r + \frac{r^2}{2!} + \frac{r^3}{3!} + \dots$$ -
Reconstruction: The result is shifted left by
$k$ bits to restore the original scale.
The natural logarithm uses a bit-by-bit iterative squaring algorithm (binary search in the log domain).
-
Normalization: Scales the input into the range
$[1, 2)$ by tracking the number of bit-shifts (intPart). - 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.
-
Precision: Achieves
$\approx 1.2 \times 10^{-18}$ resolution, matching the fixed-point floor.
These functions provide the backbone for fixed-point arithmetic with overflow protection and round-half-up logic.
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
costCurrentandcostInitialusingcostFunction. - Finds
maxQ(maximum quantity). - Handles edge cases where
costCurrent < maxQ(theoretically impossible, practically guarded). - Returns
0whenC(q) - max(qᵢ) ≥ C(q₀)(market maker is in outcome-independent profit).
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
int256arithmetic forrevenuecalculation because it can be negative. - Returns
trueforhasProfitifrevenue > 0, along with theprofitamount.
These functions provide pre-validation checks, allowing consuming contracts to handle invalid states gracefully without necessarily reverting.
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.
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.
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:
Why this matters: Because MAX_OUTCOMES = 100 and each term uint256.
Implementation Detail for Negative-Shift Branch:
When ratio < maxRatio, we need SCALE² = 1e36 because exp(diff) is already in 18-decimal. Dividing by it gives (SCALE²) / (e^diff · SCALE) = SCALE / e^diff, which is
| Component | Precision / Error |
|---|---|
| Fixed-Point Floor |
|
| Rounding |
mulScale and divScale use round-half-up (biased toward positive). This provides an expected rounding error |
| Error Accumulation | In getAllPrices, with |
Taylor Series (exp) |
For MIN_TERM = 100 raw units (1e-16 in 18-decimal) introduces negligible additional error. |
Binary Search (ln) |
59 iterations provide resolution of |
| 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 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.