fix(oracle): make supply-oracle rate-limit per-time, not per-call#3
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
The Layer 3 rate-limit in
SupplyOracle.commitclamps how far the free-float factor can move per call, butcommitis permissionless and has no cooldown. Each call readsprev = committed[token]from storage and writes it back withblock.timestamp. Nothing stops a caller from invokingcommitrepeatedly in a single block: each call reads the freshly writtenprevand advances anothermaxFactorDeltaBpstoward the median, so the factor reaches the median inceil(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
minCommitIntervalcooldown (default 1 hour) is enforced against the storedcommitted[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 oftencommitis called, which restores the per-time guarantee.prevonce at the top ofcommitand reuses it for both the cooldown check and the clamp.minCommitIntervalis added tosetParamswith validation (nonzero, and at mostmaxCommitAgeso a token cannot be cooled out past its hard-stale ceiling) and to theParamsSetevent.SupplyOracle_CommitTooSoon(token, nextAllowedAt).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
minCommitIntervalandmaxFactorDeltaBpstogether; 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 revertsCommitTooSoon, 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.