Skip to content

fix: add wall-clock time gate to H1 deviation-acceptance to block rapid dual-source manipulation - #363

Open
Ayomisco wants to merge 1 commit into
dcccrypto:mainfrom
Ayomisco:fix/keeper-12-deviation-accept-dual-source
Open

fix: add wall-clock time gate to H1 deviation-acceptance to block rapid dual-source manipulation#363
Ayomisco wants to merge 1 commit into
dcccrypto:mainfrom
Ayomisco:fix/keeper-12-deviation-accept-dual-source

Conversation

@Ayomisco

@Ayomisco Ayomisco commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Closes #362

What

Add a time gate to oracle.ts so that a deviated price requires both DEVIATION_ACCEPT_AFTER consecutive rejections AND at least ORACLE_DEVIATION_ACCEPT_AFTER_MS (default 5 minutes) of elapsed wall-clock time before acceptance.

Why

The count-only gate allowed a deviated price to be accepted after 5 * ORACLE_RATE_LIMIT_MS (~25 s). An attacker who moves a pool indexed by both DexScreener and Jupiter satisfies cross-source validation (both feeds agree on the manipulated price) and then triggers the count gate after 25 s.

A 5-minute time gate makes a sustained oracle drift distinguishable from a short-term manipulation. The count gate is kept to avoid accepting a fast-moving real price after a single cycle.

Changes

  • src/services/oracle.ts:
    • Added deviationFirstRejectedAt: Map<string, number> to track when each slab's current rejection streak started.
    • Added DEVIATION_ACCEPT_AFTER_MS static (default 5 * 60 * 1000, overridable via ORACLE_DEVIATION_ACCEPT_AFTER_MS env var).
    • On first rejection in a streak (consecutiveCount === 1), record now() in the map.
    • Both countGate and timeGate must be true before the price is accepted.
    • Clear deviationFirstRejectedAt entry when a price is accepted.

…id dual-source manipulation

The H1 bypass (DEVIATION_ACCEPT_AFTER=5) was count-only. An attacker who could
move a thin liquidity pool that both DexScreener and Jupiter index could push a
>30% deviated price: cross-source validation passes (both feeds agree within 10%),
then after 5 * ORACLE_RATE_LIMIT_MS (~25s) the count gate fires and the
manipulated price is pushed on-chain.

Add a second gate: the first consecutive rejection for a market must be at least
ORACLE_DEVIATION_ACCEPT_AFTER_MS old (default 5 minutes) before H1 fires. A
sustained legitimate move (>30% over 5+ minutes) still accepts; a 25-second
coordinated manipulation does not.

The time threshold is configurable via ORACLE_DEVIATION_ACCEPT_AFTER_MS for
markets with high expected volatility. Operators should set a value appropriate
to the expected natural price velocity of each market.
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@Ayomisco, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 22 minutes and 26 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6fcc003a-62c3-4214-adb8-7c8ef067fd44

📥 Commits

Reviewing files that changed from the base of the PR and between 8ee810d and a0a8d7e.

📒 Files selected for processing (1)
  • src/services/oracle.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@dcccrypto

Copy link
Copy Markdown
Owner

Independent verification — not an approval (QA/Security own that). Blocking finding: this PR turns two existing tests red, and they haven't been updated.

The fix itself looks right to me — I'll get to that — but CI would go red on merge.

The breakage

on main   → tests/services/oracle-deviation.test.ts   47 passed
on #363   → tests/services/oracle-deviation.test.ts    2 FAILED / 45 passed

Both failures are the existing H1 acceptance tests:

  • H1: accepts deviated price after 5 consecutive rejections (no permanent brick) (:337)
  • H1: resets rejection counter when a normal price is accepted (:369)

Both drive 5 consecutive rejections and assert the price is finally accepted. With the new DEVIATION_ACCEPT_AFTER_MS (5 min) wall-clock minimum, 5 fast rejections no longer satisfy the gate, so fetchPrice keeps returning null and expect(rAccept).not.toBeNull() fails.

That's the new guard working as designed — but it means these two tests encode the old count-only contract and need updating rather than the change being wrong.

They're worth updating, not deleting

The first one guards a real property: H1 must eventually accept, or a sustained legitimate move bricks the market permanently. That property still holds under this PR — it just now requires elapsed time as well as count. Deleting the test would drop a genuine safety guarantee; updating it keeps it and pins the new semantics.

This should be cheap: the clock is already injectable — oracle.ts:98-104 takes opts?.now. So the tests can construct the service with a controllable clock and advance it past DEVIATION_ACCEPT_AFTER_MS between the 5th rejection and the acceptance assertion, rather than relying on wall-clock time.

On the fix itself

Reads well, and three details I'd call out as good:

  • deviationFirstRejectedAt is deleted on acceptance, so a later streak starts a fresh window rather than inheriting a stale timestamp
  • the timestamp is set on consecutiveCount === 1, so the window measures the streak, not the process lifetime
  • the threshold is env-overridable (ORACLE_DEVIATION_ACCEPT_AFTER_MS), which is what makes the updated tests practical

One thing I could not verify

There is no test for the new gate itself. Once the two above are updated, the natural companion is: 5 rejections + clock advanced less than the threshold → still rejected; advanced past it → accepted. Without that, a later refactor could drop the time gate and the suite would stay green — the count-only behaviour this PR exists to remove.

Given the injectable clock, that's a handful of lines in the same file.

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.

[HIGH] oracle.ts: DEVIATION_ACCEPT_AFTER=5 count-only gate allows ~25s coordinated dual-source manipulation to push >30% deviated price

2 participants