Skip to content

Float safety: Smarkets buildOrder toBasisPoints/toQuantityUnits float multiplication #664

@realfishsam

Description

@realfishsam

Risk Level

CRITICAL

Location

core/src/exchanges/smarkets/price.ts:20-35 (utility functions)
core/src/exchanges/smarkets/index.ts:316,321 (order building call site)

Code

// price.ts:20-22
export function toBasisPoints(probability: number): number {
  return Math.round(probability * BASIS_POINTS_SCALE);  // * 10000
}

// price.ts:34-36
export function toQuantityUnits(gbp: number): number {
  return Math.round(gbp * BASIS_POINTS_SCALE);  // * 10000
}

// index.ts:316,321 — both are sent directly to the Smarkets API
quantity: toQuantityUnits(params.amount),
body.price = toBasisPoints(params.price);

Problem

probability * 10000 and gbp * 10000 are IEEE 754 float multiplications before Math.round. Any input price or amount that is not exactly representable in binary floating point can produce a dirty intermediate (e.g. 0.3335 * 10000 = 3334.9999999999995) that Math.round then rounds the wrong way.

The integer results are the exact values sent to the Smarkets API as price (basis points, 0–10000) and quantity (1/10000 GBP units). A 1-unit error in price shifts the order by 1 basis point (0.01%); a 1-unit error in quantity is 0.0001 GBP off the requested size. Both values affect order placement.

Example Failure

// GBP quantity edge case
gbp = 0.10015
0.10015 * 10000 = 1001.4999999999999  (IEEE 754)
Math.round(1001.4999...) = 1001        // should be 1002 — order is 0.0001 GBP short

// Price basis-points edge case
probability = 0.05750
0.05750 * 10000 = 574.9999999999999   (IEEE 754)
Math.round(574.999...) = 575           // happens to be right here

probability = 0.10115
0.10115 * 10000 = 1011.4999999999999  (IEEE 754)
Math.round(1011.499...) = 1011         // should be 1012 — order placed at wrong price

Suggested Fix

Use integer arithmetic with decimal.js before multiplying:

import Decimal from 'decimal.js';
export function toBasisPoints(probability: number): number {
  return new Decimal(probability).times(BASIS_POINTS_SCALE).toDecimalPlaces(0, Decimal.ROUND_HALF_UP).toNumber();
}
export function toQuantityUnits(gbp: number): number {
  return new Decimal(gbp).times(BASIS_POINTS_SCALE).toDecimalPlaces(0, Decimal.ROUND_HALF_UP).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