Skip to content

Move the bucket to Rain Floats, and reject negative values #115

Closed

Description

@thedavidmeister

The orchestrator in S01-Issuer/st0x.deploy meters mint caps with this library. That metering is moving from raw token amounts to values, and the value arrives as a Rain Float, so the bucket needs to work in Floats rather than in fixed point.

What changes

LeakyBucket currently holds uint256 checkpoint, uint256 capacity, uint256 leakRate, with the level packed as (level << 64) | timestamp and LEAKY_BUCKET_LEVEL_MAX = type(uint256).max >> 64. Level, capacity and leak rate become Rain Floats.

The arithmetic has a direct equivalent: add, sub, mul and the comparisons all exist in LibDecimalFloat, and division by zero reverts rather than producing a NaN.

Prevent negatives

A uint256 level could not be negative by construction. A Rain Float carries a signed int224 coefficient, so the migration introduces the possibility. The library itself should reject negative values rather than leaving each consumer to guard its own inputs — a consumer computing a fill from an external price has no other backstop if that price arrives negative.

Zero is already rejected and stays rejected.

Two slots is acceptable

The packed (level << 64) | timestamp word does not survive: a Float needs all 32 bytes, so the checkpoint becomes two slots and costs an extra cold SLOAD/SSTORE per operation. The consumer has confirmed that is acceptable — its mints are large enough that the difference is immaterial.

Things to be careful about

Comparisons must go through LibDecimalFloat. A Float's exponent occupies the high 32 bits as a two's-complement int32, so a negative exponent reads as an enormous unsigned value. A raw bytes32 or uint256 comparison on a level against a capacity orders them incorrectly while looking entirely plausible on review.

Accumulation can round where integer arithmetic was exact. A bucket applies level - drain + fill repeatedly, and packLossy shrinks the coefficient when it exceeds int224. A systematic rounding bias in one direction is a slow drift in the limit, so the rounding direction should be chosen so the error is always conservative — the bucket fills at least as fast as the true value.

Where this came from

S01-Issuer/st0x.attest SPEC.md items 27 to 30: https://github.com/S01-Issuer/st0x.attest/blob/main/SPEC.md

Activity

  1. thedavidmeister commented on Sep 24, 2026

    @thedavidmeister
    ContributorAuthor

    Rounding is decided. SPEC.md item 15: the bucket rounds so that it fills at least as fast as the true value.

    So where Float accumulation across level - drain + fill has to round, it rounds against the caller: the level is never understated and the drain is never overstated. A systematic bias the other way is a rate limit that drifts loose over thousands of operations with nothing catching it.

  2. thedavidmeister commented on Oct 4, 2026

    @thedavidmeister
    ContributorAuthor

    Closing as complete. Floats landed in #116 and negative values are rejected by #123. The one remaining item was directed rounding, and that is ruled not a problem: rounding direction does not accumulate in the bucket. That supersedes the earlier rounding note on this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions