Skip to content

Float safety: Polymarket US roundToTickSize double Math.round float band-aid #295

@realfishsam

Description

@realfishsam

Risk Level

MEDIUM

Location

core/src/exchanges/polymarket_us/price.ts:52-55

Code

export function roundToTickSize(price: number, tickSize: number = POLYMARKET_US_TICK_SIZE): number {
    const ticks = Math.round(price / tickSize);
    const rounded = ticks * tickSize;
    const scale = Math.pow(10, POLYMARKET_US_PRICE_DECIMALS);
    return Math.round(rounded * scale) / scale;
}

Problem

This function applies two separate Math.round calls — the second one exists specifically to clean up float drift introduced by the first. The comment in the source even says "Re-rounds to avoid floating-point drift." This is a documented band-aid acknowledging the underlying problem.

price / tickSize is a float division. ticks * tickSize reconstructs the rounded price but via float multiplication, which can produce a dirty result like 0.5499999999999999 instead of 0.55. The second Math.round(rounded * scale) / scale corrects most cases but fails if the dirty intermediate happens to round the wrong way.

This function is on the critical path for every Polymarket US order — it produces the price sent to the order intent API.

Example Failure

price = 0.5501, tickSize = 0.001, POLYMARKET_US_PRICE_DECIMALS = 3
ticks = Math.round(0.5501 / 0.001) = Math.round(550.0999...) = 550
rounded = 550 * 0.001 = 0.5499999999999999  (IEEE 754 drift!)
scale = 1000
Math.round(0.5499999999999999 * 1000) / 1000
= Math.round(549.9999999999999) / 1000
= 549 / 1000
= 0.549  ← WRONG, should be 0.550

Suggested Fix

Use decimal.js to avoid the need for double-rounding entirely:

import Decimal from 'decimal.js';
export function roundToTickSize(price: number, tickSize: number = POLYMARKET_US_TICK_SIZE): number {
    return new Decimal(price)
        .dividedBy(tickSize)
        .toDecimalPlaces(0, Decimal.ROUND_HALF_UP)
        .times(tickSize)
        .toDecimalPlaces(POLYMARKET_US_PRICE_DECIMALS)
        .toNumber();
}

Found by automated float safety audit

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions