Skip to content

fix(oracle): make supply-oracle rate-limit per-time, not per-call#3

Merged
jayeshy14 merged 1 commit into
mainfrom
fix/supply-oracle-commit-cooldown
Jun 16, 2026
Merged

fix(oracle): make supply-oracle rate-limit per-time, not per-call#3
jayeshy14 merged 1 commit into
mainfrom
fix/supply-oracle-commit-cooldown

Conversation

@jayeshy14

Copy link
Copy Markdown
Owner

The bug

The Layer 3 rate-limit in SupplyOracle.commit clamps how far the free-float factor can move per call, but commit is permissionless and has no cooldown. Each call reads prev = committed[token] from storage and writes it back with block.timestamp. Nothing stops a caller from invoking commit repeatedly in a single block: each call reads the freshly written prev and advances another maxFactorDeltaBps toward the median, so the factor reaches the median in ceil(distance / maxStep) back-to-back calls inside one block.

That defeats the stated purpose of the rate-limit, whose own comment frames it as "a malicious spike cannot move the index more than one step before a human can react." The human-reaction window collapses from several commit intervals to a single block. The clamp was never actually a rate limit, because nothing enforced time.

Credit to the finding: this was caught by adversarial review of the containment layer.

The fix

Make the rate-limit per-time. A minCommitInterval cooldown (default 1 hour) is enforced against the stored committed[token].timestamp: a successful commit cannot follow the previous one for the same token until the interval elapses. The factor therefore moves at most one clamped step per interval regardless of how often commit is called, which restores the per-time guarantee.

  • Reads prev once at the top of commit and reuses it for both the cooldown check and the clamp.
  • minCommitInterval is added to setParams with validation (nonzero, and at most maxCommitAge so a token cannot be cooled out past its hard-stale ceiling) and to the ParamsSet event.
  • Adds SupplyOracle_CommitTooSoon(token, nextAllowedAt).
  • The Layer 3 NatSpec now states plainly that the cooldown is load-bearing.

Tradeoff

A genuinely large but correct supply change now tracks deliberately slowly, one clamped step per interval, which is the intended "buy security with latency" posture for a slow-moving input. The owner should size minCommitInterval and maxFactorDeltaBps together; the defaults give roughly a 10% move per hour, about seven hours to halve a factor, which is a reasonable guardian-reaction window.

Testing

Adds test_RateLimit_PerTimeNotPerCommit_BlocksSameBlockWalk, which reproduces the single-block walk: it asserts the first commit moves exactly one clamped step, that a second commit in the same block reverts CommitTooSoon, and that the next step lands only after the cooldown elapses. All existing rate-limit and divergence tests still pass, confirming the default interval is compatible with the suite. 76 tests pass.

The Layer 3 rate-limit clamped the factor's move per commit, but commit is
permissionless with no cooldown, so the clamp was not actually a rate limit.
A caller could invoke commit repeatedly in a single block, each call reading
the freshly written prev factor and advancing another maxFactorDeltaBps toward
the median, walking the factor all the way to the median in ceil(distance /
maxStep) back-to-back calls inside one block. That collapses the human-reaction
window the clamp exists to preserve from several commit intervals to a single
block, defeating the stated containment guarantee.

Fix: add a minCommitInterval cooldown (default 1 hour) enforced against the
stored committed[token].timestamp. A successful commit cannot follow the
previous one for the same token until the interval elapses, so the factor
moves at most one clamped step per interval no matter how often commit is
called. The single prev read is reused for both the cooldown check and the
clamp. minCommitInterval is added to setParams with validation (nonzero and at
most maxCommitAge) and to the ParamsSet event, and the Layer 3 NatSpec now
states the cooldown is load-bearing.

Adds test_RateLimit_PerTimeNotPerCommit_BlocksSameBlockWalk, which reproduces
the single-block walk and asserts a second same-block commit reverts
CommitTooSoon and the next step lands only after the cooldown. 76 tests pass.
@jayeshy14 jayeshy14 merged commit baee4b3 into main Jun 16, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant