Skip to content

feat(token-price-oracle): add multi-source price feeds - #1002

Merged
curryxbo merged 21 commits into
mainfrom
feat/977-chainlink-token-price-oracle
Aug 4, 2026
Merged

feat(token-price-oracle): add multi-source price feeds#1002
curryxbo merged 21 commits into
mainfrom
feat/977-chainlink-token-price-oracle

Conversation

@curryxbo

@curryxbo curryxbo commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add off-chain price feed adapters for Chainlink AggregatorV3, Pyth Hermes, Bitget, Binance, and OKX under the existing token-price-oracle fallback framework.
  • Validate source freshness/health where available: Chainlink round checks, Pyth publish time and optional confidence BPS checks, and full batch coverage before accepting a fallback source.
  • Document per-source configuration and keep L2TokenRegistry unchanged; all external data sources are consumed by the off-chain updater before writing existing priceRatio values.

Test plan

  • cd token-price-oracle && go test ./...
  • cd token-price-oracle && go vet ./...
  • Public data smoke tests (temporary, not committed): Chainlink, Pyth Hermes, Binance, and OKX returned live BTC/ETH prices.

Closes #977

Summary by CodeRabbit

  • New Features

    • Added Chainlink and Pyth Hermes price feed support.
    • Added Binance and OKX exchange feeds with configurable priority and fallback behavior.
    • Added configurable staleness, confidence, token mappings, and feed endpoints.
    • Devnet now pre-registers BTC, ETH, and BGB test tokens.
  • Bug Fixes

    • Incomplete batch responses are now detected and trigger fallback feeds.
  • Documentation

    • Expanded configuration examples and added a devnet token-oracle setup guide.
    • Clarified derivation confirmation and reorganization detection behavior.

@curryxbo
curryxbo requested a review from a team as a code owner June 22, 2026 11:41
@curryxbo
curryxbo requested review from dylanCai9 and removed request for a team June 22, 2026 11:41
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds Chainlink, Pyth Hermes, Binance, and OKX price feeds with configuration, validation, fallback integration, and documentation. Devnet genesis now pre-registers three test tokens. Derivation confirmation behavior now uses explicit confirmation settings without an implicit Layer1 override.

Changes

Multi-source Token Price Feeds

Layer / File(s) Summary
Feed configuration and CLI wiring
token-price-oracle/config/config.go, token-price-oracle/flags/flags.go
Adds provider types, Chainlink and Pyth settings, CEX mappings, CLI flags, environment mappings, and priority validation.
Chainlink and Pyth price retrieval
token-price-oracle/client/chainlink_feed.go, token-price-oracle/client/pyth_feed.go, token-price-oracle/client/*_test.go
Implements AggregatorV3 and Hermes clients with batch retrieval, freshness and confidence validation, numeric conversion, and unit tests.
Binance and OKX feed implementation
token-price-oracle/client/cex_feed.go, token-price-oracle/client/cex_feed_test.go
Adds cached ETH pricing, stablecoin handling, Binance and OKX HTTP retrieval, response validation, and tests.
Provider factory and fallback validation
token-price-oracle/updater/factory.go, token-price-oracle/updater/factory_test.go, token-price-oracle/client/price_feed.go
Wires providers into feed creation, redacts RPC logs, and rejects incomplete batch responses.
Operational examples and documentation
token-price-oracle/README.md, token-price-oracle/docker-compose.yml, token-price-oracle/env.example, token-price-oracle/local.sh
Documents feed priority, provider variables, compose settings, sample environments, startup flags, and the Chainlink client.

Devnet TokenRegistry Initialization

Layer / File(s) Summary
Genesis token pre-registration
ops/l2-genesis/morph-chain-ops/genesis/devnet_tokens.go, ops/l2-genesis/morph-chain-ops/genesis/devnet_tokens_test.go, ops/l2-genesis/morph-chain-ops/genesis/layer_two.go
Defines three devnet tokens, writes registry storage and supported-token indexes, validates the layout, and invokes registration during developer genesis.
Devnet setup and verification documentation
DEVNET_TOKENREGISTRY_SETUP.md
Documents token metadata, storage initialization, oracle configuration, startup commands, verification, and troubleshooting.

Derivation Confirmation Behavior

Layer / File(s) Summary
Confirmation defaults and reorg documentation
node/derivation/config.go, node/derivation/reorg.go, node/flags/flags.go
Documents confirmation modes and removes the implicit Layer1 finalized-confirmation override.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PriceUpdater
  participant ChainlinkPriceFeed
  participant AggregatorV3
  participant PythHermesPriceFeed
  participant HermesAPI
  participant CEXPriceFeed
  participant ExchangeAPI
  PriceUpdater->>ChainlinkPriceFeed: Request batch token prices
  ChainlinkPriceFeed->>AggregatorV3: Read round data and decimals
  AggregatorV3-->>ChainlinkPriceFeed: Return feed values
  PriceUpdater->>PythHermesPriceFeed: Request batch token prices
  PythHermesPriceFeed->>HermesAPI: Request parsed price IDs
  HermesAPI-->>PythHermesPriceFeed: Return price and confidence data
  PriceUpdater->>CEXPriceFeed: Request exchange prices
  CEXPriceFeed->>ExchangeAPI: Fetch ticker prices
  ExchangeAPI-->>CEXPriceFeed: Return ticker data
Loading

Possibly related PRs

  • morph-l2/morph#809: Extends the token-price-oracle components and feed configuration introduced there.
  • morph-l2/morph#812: Relates to L2TokenRegistry balance-slot storage and token registration semantics.

Suggested reviewers: dylancai9, twcctop, kukoomomo

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning It implements [#977] multi-source off-chain feeds, fallback, Chainlink/Pyth validation, and configuration, but omits heartbeat, anomaly detection, circuit breakers, TWAP, metrics, and hot reload. Implement or explicitly defer the missing [#977] requirements, including heartbeat, aggregation, anomaly and circuit-breaker controls, TWAP, metrics, and configuration hot reload.
Out of Scope Changes check ⚠️ Warning Changes to node/derivation comments and node/flags help text are unrelated to [#977] and to the token-price-oracle implementation. Remove the unrelated node/derivation changes or move them to a separate pull request.
Docstring Coverage ⚠️ Warning Docstring coverage is 31.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: adding multiple price-feed sources to the token-price-oracle.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/977-chainlink-token-price-oracle

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
token-price-oracle/client/chainlink_feed_test.go (1)

63-69: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Single-case test with potential precision loss in TestChainlinkAnswerToFloat.

The test verifies only one conversion scenario (123456789000 → 1234.56789) and relies on Float64() conversion, which truncates to ~17 significant digits. The implementation uses big.Float.SetPrec(256) for high-precision arithmetic, but the test doesn't validate precision preservation or edge cases:

  • decimals = 0 (no scaling)
  • decimals = 18 (extreme precision, common for ERC-20 tokens)
  • Large answer values where Float64 conversion may lose precision
  • Answer = 1 with various decimal positions

Adding parameterized test cases for different decimal values would increase confidence in correct price scaling.

📋 Suggested parameterized test cases
func TestChainlinkAnswerToFloat(t *testing.T) {
	tests := []struct {
		name      string
		answer    *big.Int
		decimals  uint8
		expected  float64
	}{
		{
			name:     "standard 8 decimals",
			answer:   big.NewInt(123456789000),
			decimals: 8,
			expected: 1234.56789,
		},
		{
			name:     "no decimals",
			answer:   big.NewInt(2000),
			decimals: 0,
			expected: 2000,
		},
		{
			name:     "18 decimals (ERC-20 standard)",
			answer:   big.NewInt(1_000_000_000_000_000_000), // 1 token with 18 decimals
			decimals: 18,
			expected: 1.0,
		},
		{
			name:     "single unit",
			answer:   big.NewInt(1),
			decimals: 8,
			expected: 0.00000001,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			price := chainlinkAnswerToFloat(tt.answer, tt.decimals)
			got, _ := price.Float64()
			if got != tt.expected {
				t.Fatalf("chainlinkAnswerToFloat(%s, %d) = %v, want %v", 
					tt.answer.String(), tt.decimals, got, tt.expected)
			}
		})
	}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@token-price-oracle/client/chainlink_feed_test.go` around lines 63 - 69, The
TestChainlinkAnswerToFloat function only tests a single case and does not
validate the chainlinkAnswerToFloat implementation across edge cases and
different decimal positions. Refactor TestChainlinkAnswerToFloat into a
parameterized test by creating a test cases table with fields for test name,
answer value as *big.Int, decimals as uint8, and expected float64 result.
Iterate through the test cases using t.Run() and verify the
chainlinkAnswerToFloat function correctly handles scenarios including:
decimals=0 (no scaling), decimals=18 (ERC-20 standard), large answer values, and
answer=1 with various decimal positions. This ensures the high-precision
arithmetic in the implementation is properly validated across different scaling
scenarios.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@token-price-oracle/client/chainlink_feed_test.go`:
- Around line 9-61: The TestValidateChainlinkRound test is missing coverage for
critical validation paths in validateChainlinkRound: future timestamp
validation, nil parameter checks, and staleness boundary conditions. Add four
new test cases to the tests table: one for future updatedAt values beyond the
maxStaleness window, separate cases for nil values in each of the four input
parameters (answer, updatedAt, roundID, answeredInRound), and one for the exact
staleness boundary condition where updatedAt is exactly maxStaleness in the
past, with appropriate wantErr values for each.

In `@token-price-oracle/client/chainlink_feed.go`:
- Around line 243-245: The future timestamp validation in the condition check at
line 243 is too permissive because it allows timestamps up to now plus
maxStaleness in the future. Change the condition to reject any updatedAt
timestamp that is after the current time (now), rather than allowing it to be up
to maxStaleness into the future. Replace now.Add(maxStaleness) with just now in
the After() comparison to ensure future-dated timestamps are properly rejected.

In `@token-price-oracle/updater/factory.go`:
- Around line 118-121: The log.Info statement at line 120 logs the raw
ChainlinkRPC URL directly, which exposes potentially sensitive API keys or
authentication credentials. Create a helper function called redactRPCForLog that
parses the RPC URL and returns only the scheme and host portion (e.g.,
"https://example.com"), stripping out any credentials or path-based API keys.
Then update the log.Info call to use the redactRPCForLog function on
cfg.ChainlinkRPC before logging it in the "rpc" field.

---

Nitpick comments:
In `@token-price-oracle/client/chainlink_feed_test.go`:
- Around line 63-69: The TestChainlinkAnswerToFloat function only tests a single
case and does not validate the chainlinkAnswerToFloat implementation across edge
cases and different decimal positions. Refactor TestChainlinkAnswerToFloat into
a parameterized test by creating a test cases table with fields for test name,
answer value as *big.Int, decimals as uint8, and expected float64 result.
Iterate through the test cases using t.Run() and verify the
chainlinkAnswerToFloat function correctly handles scenarios including:
decimals=0 (no scaling), decimals=18 (ERC-20 standard), large answer values, and
answer=1 with various decimal positions. This ensures the high-precision
arithmetic in the implementation is properly validated across different scaling
scenarios.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 238ff072-6b3c-4840-af5f-4bf64cbaa274

📥 Commits

Reviewing files that changed from the base of the PR and between 99bc207 and 2e4be7d.

📒 Files selected for processing (10)
  • token-price-oracle/README.md
  • token-price-oracle/client/chainlink_feed.go
  • token-price-oracle/client/chainlink_feed_test.go
  • token-price-oracle/client/price_feed.go
  • token-price-oracle/config/config.go
  • token-price-oracle/docker-compose.yml
  • token-price-oracle/env.example
  • token-price-oracle/flags/flags.go
  • token-price-oracle/local.sh
  • token-price-oracle/updater/factory.go

Comment thread token-price-oracle/client/chainlink_feed_test.go
Comment thread token-price-oracle/client/chainlink_feed.go Outdated
Comment thread token-price-oracle/updater/factory.go
@curryxbo curryxbo changed the title feat(token-price-oracle): add Chainlink price feed feat(token-price-oracle): add multi-source price feeds Jun 22, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
token-price-oracle/client/pyth_feed.go (1)

21-31: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unused write-lock: mu is only ever RLocked.

tokenPriceIDs is populated once in the constructor and never mutated afterward, yet mu is read-locked in GetTokenPrice/GetBatchTokenPrices. There is no Lock() call anywhere in the file, so the sync.RWMutex is vestigial and could mislead future maintainers into assuming mutation support exists.

Also applies to: 69-72, 109-122

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@token-price-oracle/client/pyth_feed.go` around lines 21 - 31, The
sync.RWMutex field mu is never write-locked and protects immutable state only.
Remove mu from PythHermesPriceFeed, eliminate the corresponding RLock/RUnlock
calls in GetTokenPrice and GetBatchTokenPrices, and retain the existing read
behavior without synchronization.
token-price-oracle/client/cex_feed.go (1)

96-115: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

GetBatchTokenPrices issues one sequential HTTP request per token instead of a batch call.

Both Binance (GET /api/v3/ticker/price with no symbol returns all tickers) and OKX (GET /api/v5/market/ticker with instType) support fetching multiple prices in a single call. The current implementation does N sequential round-trips per update cycle, which scales poorly and increases exposure to per-exchange rate limits as the number of mapped tokens grows.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@token-price-oracle/client/cex_feed.go` around lines 96 - 115, Update
CEXPriceFeed.GetBatchTokenPrices to fetch all requested token prices through a
single exchange batch request instead of calling GetTokenPrice once per token.
Use the Binance and OKX batch ticker APIs, map returned symbols to the requested
tokenIDs, preserve the existing ETH-price update and per-token skip behavior,
and return the assembled prices map.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@token-price-oracle/client/cex_feed.go`:
- Around line 62-94: Update CEXPriceFeed.GetTokenPrice to lazily initialize
ethPrice when it has not yet been set, fetching the ETH price through the
existing CEX price-fetching path instead of returning the “ETH price not
initialized” error. Reuse the same initialization and synchronization behavior
as GetBatchTokenPrices, while preserving the existing token lookup, mapped-price
fetch, and return flow.

In `@token-price-oracle/local.sh`:
- Line 31: Update the commented Pyth API-key argument in the example command to
expand the documented TOKEN_PRICE_ORACLE_PYTH_API_KEY variable instead of
PYTH_API_KEY.

In `@token-price-oracle/README.md`:
- Around line 74-76: Update the README documentation for
TOKEN_PRICE_ORACLE_PRICE_FEED_PRIORITY to consistently mark it as optional and
include its default value bitget, matching the default assigned in flags.go;
apply the same correction to the other occurrence and leave the CLI behavior
unchanged.

---

Nitpick comments:
In `@token-price-oracle/client/cex_feed.go`:
- Around line 96-115: Update CEXPriceFeed.GetBatchTokenPrices to fetch all
requested token prices through a single exchange batch request instead of
calling GetTokenPrice once per token. Use the Binance and OKX batch ticker APIs,
map returned symbols to the requested tokenIDs, preserve the existing ETH-price
update and per-token skip behavior, and return the assembled prices map.

In `@token-price-oracle/client/pyth_feed.go`:
- Around line 21-31: The sync.RWMutex field mu is never write-locked and
protects immutable state only. Remove mu from PythHermesPriceFeed, eliminate the
corresponding RLock/RUnlock calls in GetTokenPrice and GetBatchTokenPrices, and
retain the existing read behavior without synchronization.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2f1accfd-350f-4a5f-a35c-52ff0f91f2ce

📥 Commits

Reviewing files that changed from the base of the PR and between 2e4be7d and a5a77e7.

📒 Files selected for processing (11)
  • token-price-oracle/README.md
  • token-price-oracle/client/cex_feed.go
  • token-price-oracle/client/cex_feed_test.go
  • token-price-oracle/client/pyth_feed.go
  • token-price-oracle/client/pyth_feed_test.go
  • token-price-oracle/config/config.go
  • token-price-oracle/docker-compose.yml
  • token-price-oracle/env.example
  • token-price-oracle/flags/flags.go
  • token-price-oracle/local.sh
  • token-price-oracle/updater/factory.go

Comment thread token-price-oracle/client/cex_feed.go
Comment thread token-price-oracle/local.sh Outdated
Comment thread token-price-oracle/README.md Outdated
curryxbo and others added 3 commits July 25, 2026 11:16
- Add devnet_tokens.go with BTC/ETH/BGB test token definitions
- Automatically register tokens in genesis for devnet environment
- Enable token-price-oracle to work out-of-the-box without manual setup
- Tokens: BTC (ID=1), ETH (ID=2), BGB (ID=3)
- Supports multi-source price feeds: Chainlink, Pyth, Bitget, OKX

This ensures complete end-to-end flow from genesis generation to
price oracle operation in devnet.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- Complete setup guide for devnet token pre-registration
- Configuration examples for token-price-oracle
- Troubleshooting and extension recommendations
- Final test summary covering all PRs (1021, 1023, 1002)

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Summary of completed tasks:
- PR1021 testing (beacon fallback)
- PR1023 testing (layer1-verify metrics)
- PR1002 testing with devnet TokenRegistry implementation
- Complete documentation and troubleshooting guides

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.

Tip: disable this comment in your organization's Code Review settings.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@DEVNET_TOKENREGISTRY_SETUP.md`:
- Around line 137-147: Update the token-price-oracle docker run command in
DEVNET_TOKENREGISTRY_SETUP.md to assign the explicit container name
token-price-oracle, matching the existing docker logs command. Keep the
remaining run options unchanged.
- Line 54: Replace the concrete TOKEN_PRICE_ORACLE_PRIVATE_KEY value in the
setup documentation with a clearly marked placeholder, and explicitly state that
the key must never be used outside an isolated local devnet. Keep the
surrounding setup instructions intact while making the devnet-only restriction
unmistakable.

In `@ops/l2-genesis/morph-chain-ops/genesis/devnet_tokens.go`:
- Around line 122-126: The devnet activation flow and its documentation must
agree on whether tokens are immediately updateable. In
ops/l2-genesis/morph-chain-ops/genesis/devnet_tokens.go lines 122-126,
deliberately retain or revise the inactive token initialization and test the
intended behavior; in DEVNET_TOKENREGISTRY_SETUP.md lines 5 and 127-141, remove
the unconditional out-of-the-box update claim or state prerequisites, and place
activation/allowlist setup before oracle startup; in FINAL_TEST_SUMMARY.md lines
131-151 and 274-285, mark transaction logs and complete-loop/merge-readiness
claims as conditional until activation and allowlisting are verified.
- Around line 101-130: Update setTokenInfo to encode token.BalanceSlot as
BalanceSlot plus one when non-zero, while preserving zero as zero, matching
L2TokenRegistry’s getTokenInfo and getTokenInfoByAddress contract. Add a
regression test covering getTokenInfo, reverse lookup, getAllTokenIDs,
getSupportedTokenList, and zero/non-zero balance-slot storage encoding.

In `@TODAY_WORK_SUMMARY.md`:
- Around line 187-201: Reconcile all validation reports to use the pending
devnet RPC checks as the authoritative status: in TODAY_WORK_SUMMARY.md lines
187-201 retain pending states, lines 94-113 label unverified logs as expected
output, lines 263-277 avoid claiming the full loop is complete, and lines
301-309 update the conclusion and merge recommendation; uncheck unrerun
validations in DEVNET_TOKENREGISTRY_SETUP.md lines 159-166 and align the
coverage matrix with actual evidence in FINAL_TEST_SUMMARY.md lines 171-185.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6845cc95-9f3e-478e-9365-074215486c8f

📥 Commits

Reviewing files that changed from the base of the PR and between a5a77e7 and b1be127.

📒 Files selected for processing (5)
  • DEVNET_TOKENREGISTRY_SETUP.md
  • FINAL_TEST_SUMMARY.md
  • TODAY_WORK_SUMMARY.md
  • ops/l2-genesis/morph-chain-ops/genesis/devnet_tokens.go
  • ops/l2-genesis/morph-chain-ops/genesis/layer_two.go

Comment thread DEVNET_TOKENREGISTRY_SETUP.md Outdated
Comment thread token-price-oracle/DEVNET_SETUP.md
Comment thread ops/l2-genesis/morph-chain-ops/genesis/devnet_tokens.go Outdated
Comment thread ops/l2-genesis/morph-chain-ops/genesis/devnet_tokens.go
Comment thread TODAY_WORK_SUMMARY.md Outdated
Harden feed validation and secret handling, align devnet storage with the contract, and replace unverified Chinese reports with concise English setup guidance.

Co-authored-by: Cursor <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
DEVNET_TOKENREGISTRY_SETUP.md (1)

76-92: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Make partial provider coverage explicit.

For the configured [1,2,3] batch, Chainlink and Pyth mappings omit token ID 3, while only Bitget and OKX cover all three tokens. Since incomplete provider responses are rejected before fallback acceptance, Chainlink and Pyth cannot serve this batch independently. Document that CEX feeds are required for token 3, or provide complete mappings/remove partial providers from the example priority.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@DEVNET_TOKENREGISTRY_SETUP.md` around lines 76 - 92, Update the provider
configuration example around TOKEN_PRICE_ORACLE_PRICE_FEED_PRIORITY and the
Chainlink/Pyth mappings to explicitly account for token ID 3 being unavailable
from those providers. Document that Bitget and OKX are required for token 3, or
alternatively provide complete Chainlink and Pyth mappings or remove the partial
providers from the priority list.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@DEVNET_TOKENREGISTRY_SETUP.md`:
- Around line 112-117: Update the setup instructions before the
token-price-oracle docker run command to explicitly create devnet.env from the
documented environment values, and state the working directory or use an
unambiguous path so Docker can locate it. Keep the existing docker run
configuration unchanged aside from the env-file path if needed.

---

Nitpick comments:
In `@DEVNET_TOKENREGISTRY_SETUP.md`:
- Around line 76-92: Update the provider configuration example around
TOKEN_PRICE_ORACLE_PRICE_FEED_PRIORITY and the Chainlink/Pyth mappings to
explicitly account for token ID 3 being unavailable from those providers.
Document that Bitget and OKX are required for token 3, or alternatively provide
complete Chainlink and Pyth mappings or remove the partial providers from the
priority list.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7733fb92-3fd0-481e-a1d4-6ceda3959894

📥 Commits

Reviewing files that changed from the base of the PR and between b1be127 and 4174e3d.

📒 Files selected for processing (11)
  • DEVNET_TOKENREGISTRY_SETUP.md
  • ops/l2-genesis/morph-chain-ops/genesis/devnet_tokens.go
  • ops/l2-genesis/morph-chain-ops/genesis/devnet_tokens_test.go
  • token-price-oracle/README.md
  • token-price-oracle/client/cex_feed.go
  • token-price-oracle/client/cex_feed_test.go
  • token-price-oracle/client/chainlink_feed.go
  • token-price-oracle/client/chainlink_feed_test.go
  • token-price-oracle/local.sh
  • token-price-oracle/updater/factory.go
  • token-price-oracle/updater/factory_test.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • token-price-oracle/local.sh
  • token-price-oracle/client/chainlink_feed_test.go
  • token-price-oracle/updater/factory.go
  • ops/l2-genesis/morph-chain-ops/genesis/devnet_tokens.go
  • token-price-oracle/README.md
  • token-price-oracle/client/cex_feed.go

Comment thread token-price-oracle/DEVNET_SETUP.md

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.

Tip: disable this comment in your organization's Code Review settings.

corey and others added 2 commits July 31, 2026 17:30
Deriving from L1 finalized made a validator's own chain trail L1 by roughly
two epochs, which delayed batch-divergence alerts by the same amount and
collapsed the L2 safe and finalized tags onto a single value. It also broke
startup on an L1 with no finalized block yet, since the first-run startHeight
default reads at the configured depth.

Validators now share the fixed-depth default and its reorg detector with every
other node type; deployments that want a consensus-backed read set
--derivation.confirmations explicitly.

Co-authored-by: Cursor <[email protected]>
… feeds

BitgetSDKPriceFeed rejected a standalone GetTokenPrice call whenever the cached
ETH leg had never been primed, while CEXPriceFeed fetched it on demand. One
interface method with two preconditions meant a caller's failure mode depended
on which feed happened to be configured.

Bitget now self-initializes like the Binance/OKX feed, matching Chainlink and
Pyth which fetch both legs on every call. The batch path is unchanged: it primes
ETH before the loop so the self-init branch never fires, keeping a cycle at N+1
requests against rate-limited endpoints rather than 2N.

Co-authored-by: Cursor <[email protected]>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.

Tip: disable this comment in your organization's Code Review settings.

@panos-xyz panos-xyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I completed a correctness, integration, and spec review of the current head. CI is green, but the issues below include merge-blocking state-layout, fallback, and availability defects that are not covered by the current tests.

Additional spec-level gaps that are not anchored to changed lines:

  • Issue #977 requires a heartbeat that forces an on-chain update despite a below-threshold deviation; source maxStaleness does not implement that behavior.
  • Per-token/per-source metrics and the P0 risk/degradation policy are still missing.
  • Genesis mutation and derivation-finality changes are outside #977's explicit off-chain-only scope and should be split into separately reviewed PRs.
  • The existing unresolved devnet.env documentation comment remains valid.

Please resolve the P1 findings before merge and keep #977 open (or narrow the closing semantics) until its P0 requirements are complete.

}

// Pre-register test tokens in TokenRegistry for devnet
if err := SetDevnetTestTokens(db); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This writes devnet-only registry state into every generated network genesis. The generic genesis l2 command calls BuildL2DeveloperGenesis for any deploy config, including mainnet, Hoodi, QA, and testnet configs where fundDevAccounts is false. Because this call is unconditional, regenerating one of those genesis files reserves token IDs 1-3, installs test addresses, and changes the state root. Gate this behind an explicit devnet setting (at minimum config.FundDevAccounts) and add a regression test proving that a non-dev config leaves the registry empty.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1bab5a9. SetDevnetTestTokens now sits inside the same config.FundDevAccounts gate as the dev accounts, so mainnet and testnet genesis are untouched.

{
TokenID: 2,
TokenAddress: common.HexToAddress("0x5300000000000000000000000000000000000011"), // L2WETH predeploy address
BalanceSlot: common.BigToHash(big.NewInt(3)), // WETH standard balance slot

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The L2WETH balance slot is incorrect. bindings/bindings/wrappedether_more.go places _balances at slot 0; slot 3 is _name. After token ID 2 is activated, fee accounting reads keccak256(user, 3), so deposited WETH at keccak256(user, 0) appears absent and WETH-paid transactions fail. The current model also uses a zero hash to mean 'no balance slot', so it cannot represent an actual slot of zero. Add a separate NeedBalanceSlot/HasBalanceSlot field and encode actual slot 0 as stored value 1, or intentionally use the EVM-call path, then cover a WETH fee-payment flow in a genesis-level test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1bab5a9. L2WETH now registers slot 0.

You were right that the encoding needed a separate flag: slot 0 is a real balance slot, so a zero hash could not distinguish it from "no slot". DevnetTestToken gained a NeedBalanceSlot field mirroring the parameter L2TokenRegistry.registerToken already takes, and setTokenInfo now follows _toStoredBalanceSlot exactly.

The existing storage test had the same ambiguity baked into its assertion, so it was corrected as well, and a new test pins the WETH entry to slot 0 with the flag set.

return []DevnetTestToken{
{
TokenID: 1,
TokenAddress: common.HexToAddress("0x0000000000000000000000000000000000000001"), // Mock BTC address

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: These are precompile addresses, not mock ERC-20 contracts. Address 0x01 executes ECRECOVER and 0x03 executes RIPEMD160. The setup guide later activates all three IDs, after which balanceOf/transfer calls for BTC and BGB return invalid or hash-derived data and cannot settle fee-token transactions. Deploy real mock ERC-20 bytecode at non-precompile addresses (or do not advertise/activate these IDs) and add one fee-payment integration test per advertised token.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1bab5a9. The placeholder addresses moved to 0x1111...1111 and 0x3333...3333, clear of the 0x01-0x0a precompile range, and a test asserts the registered addresses stay above 0xff so this cannot regress. The setup guide was updated to match and now says the addresses are placeholders with no deployed contract.

Comment thread token-price-oracle/client/price_feed.go Outdated
for tokenID, price := range prices {
for _, tokenID := range tokenIDs {
price, exists := prices[tokenID]
if !exists {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Batch fallback is all-or-nothing and behaves differently when only one feed is configured. Every feed receives the full active-token set, and an incomplete map is discarded, so complementary mappings across providers can never succeed (for example, the documented Chainlink/Pyth mappings cannot serve active ID 3). Conversely, createFallbackPriceFeed returns a single feed directly, bypassing this completeness validation; CEX feeds can then return a partial or empty map with a nil error and the updater can record a successful cycle. Implement per-token fallback/merging (querying each provider only for unresolved supported IDs) and enforce the same completeness contract for both one-feed and multi-feed configurations.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Splitting this into the two parts.

Single-feed bypass — fixed in 1bab5a9. createFallbackPriceFeed no longer returns feeds[0] directly, so one-feed and multi-feed configurations share the same completeness contract. FallbackPriceFeed already handles a single feed correctly, so this was just deleting the special case.

Worth recording that the impact was a bit worse than a recorded success: an empty batch reaches the "no prices need updating" branch, which also calls LastSuccessfulUpdateTimestamp.Set(now). A total feed outage therefore refreshed the very metric that would have detected it.

Per-token merging across providers — agreed, but out of scope here. Querying each provider only for unresolved IDs changes both the PriceFeed batch contract and the updater's notion of a successful cycle, and it needs its own tests around partial resolution. I would rather not grow this PR further; I will track it as a follow-up and link it here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on the per-token part — you were right that this belongs in this PR, and my earlier "out of scope" call was wrong. Fixed in 7d18942.

Tracing it end to end, this was worse than a missing enhancement. The updater passes the full active set to the fallback feed, and ChainlinkPriceFeed/PythHermesPriceFeed returned an error on the first token absent from their mapping, so one unmapped active token failed those feeds for every token, every cycle. The oracle-first design silently degraded to CEX-only. The devnet configuration in this PR reproduces it: genesis registers tokens 1-3 while the documented Chainlink and Pyth mappings cover only 1 and 2, so once all three are active neither oracle feed is ever used.

The fix has two halves. Chainlink and Pyth now omit tokens they have no mapping for instead of failing the batch, which is the behaviour the CEX feeds already had, so the contract is now consistent across providers; genuine fetch failures still error. FallbackPriceFeed.GetBatchTokenPrices then resolves across feeds in priority order, passing each feed only the tokens still unresolved.

On the completeness contract, I went with partial resolution rather than all-or-nothing: a token no provider can serve no longer blocks updates for every other token. It is reported by its absence from the returned map, and the updater logs it and exports a new unresolved_tokens gauge plus an unresolved_token error counter, so a token going stale is visible rather than silent. A cycle that resolves nothing at all is still an error, which preserves the guarantee from the single-feed fix above.

Tests cover the merge (higher-priority feed serves what it can, the next feed is asked only for the remainder), an unservable token yielding a partial result, a failing feed not shrinking the next feed's request, and the empty-result case still erroring.

Comment thread node/derivation/config.go
// derivation cursor on hash mismatch so a deeper reorg is recoverable.
// Operators wanting strict no-reorg-possible reads can still set
// --derivation.confirmations=-3 (rpc.FinalizedBlockNumber).
// Applies to every verify mode. Layer1 validators derive their whole

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Making layer1 verification use the 10-block default can permanently stall a validator after a valid deep reorg. The reorg handler only rewinds the L1 cursor; reorg.go explicitly states that it does not roll back L2. If a >10-block reorg replaces a committed batch with different content, re-derivation reuses the old L2 blocks, root verification fails, and subsequent polls keep retrying the same failure. Preserve finalized-by-default behavior for layer1 mode until L2 rollback exists, or implement and integration-test changed-batch reorg recovery. This consensus-critical policy change should also be split from the oracle PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged, and raised again in your latest review. I am not going to argue the deep-reorg scenario inside this PR: it deserves its own discussion rather than being settled as a side effect of a price-feed change, so I will follow up on it separately.

Comment thread token-price-oracle/client/pyth_feed.go Outdated
if price.PublishTime <= 0 {
return fmt.Errorf("publish_time must be positive")
}
if published.After(now.Add(maxStaleness)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Future-dated prices are accepted for the full staleness window. With the one-hour default, a publish time almost one hour in the future passes this check and remains valid for almost two hours. This undermines the claimed freshness validation. Reject timestamps after now or after a small, explicit clock-skew allowance, matching the hardened Chainlink path, and add a future-timestamp regression test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1bab5a9. validatePythPrice now rejects any publish time after the local clock rather than allowing the full staleness window, which matches validateChainlinkRound. Added a table case for a future-dated publish time.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amended in e974620: the check now allows 30s of clock skew rather than rejecting anything ahead of the local clock.

Matching validateChainlinkRound exactly turned out to be wrong for this feed. updatedAt there is an L1 block timestamp and is always in the past by the time it is read, so zero tolerance is safe. Pyth's publish_time is the publisher's near-real-time wall clock at second granularity, so a host whose clock drifts behind the publisher would have rejected every Pyth price and taken the whole feed down. The 30s bound still fixes the reported defect, which was accepting anything inside the staleness window — nearly an hour at the default. Tests cover both sides of the bound.

if priceStr == "" {
return nil, fmt.Errorf("no price data returned for symbol %s", symbol)
}
price, err := strconv.ParseFloat(priceStr, 64)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: strconv.ParseFloat accepts NaN and infinities. NaN passes the current <= 0 test and big.NewFloat(NaN) panics; positive infinity later causes conversion to an integer ratio to return nil and can panic the updater. The same issue exists in the fixed stablecoin parser. Reject math.IsNaN/math.IsInf and non-positive values before constructing big.Float, then test that malformed exchange data falls through to the next provider instead of crashing the process.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1bab5a9. Both parsers now go through a shared newFinitePositiveFloat guard that rejects NaN, infinities and non-positive values before big.NewFloat.

Two things worth flagging beyond the original report. Bitget carries its own copies of both parsers rather than reusing the CEX ones, and its ticker parser had no positivity check at all, so it accepted zero and negatives as well; both now use the shared guard. Separately, ParseFloat("1e400") does return an out-of-range error, so overflow via decimal literal was already rejected.

The test asserts rejection at the parser level for NaN, ±Inf, zero and negatives. I did not add an end-to-end test that a malformed ticker falls through to the next provider; the parser returns an error and FallbackPriceFeed already advances on error, but say the word if you would like that covered explicitly.

contract := bind.NewBoundContract(feedAddress, parsedChainlinkAggregatorABI, c.caller, nil, nil)

var roundData []interface{}
if err := contract.Call(&bind.CallOpts{Context: ctx}, &roundData, "latestRoundData"); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Chainlink calls have no bounded per-call timeout. ethclient.Dial uses an HTTP client without a request timeout, and these calls inherit the service's long-lived context. A stalled high-priority RPC can therefore block the updater indefinitely, preventing fallback from ever being attempted. Wrap each feed operation in a bounded child context (or inject a timed HTTP/RPC client) and add a hanging-RPC fallback test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1bab5a9. Each eth_call now runs under its own 10s deadline, covering both latestRoundData and decimals.

I put the deadline on the call context rather than on the dialed HTTP client so it applies regardless of transport, since NewChainlinkPriceFeedWithCaller can be handed any bind.ContractCaller.

export TOKEN_PRICE_ORACLE_TOKEN_MAPPING_BITGET="1:BTCUSDT,2:ETHUSDT"

# Optional: prefer oracle feeds first, fallback to CEX sources
export TOKEN_PRICE_ORACLE_PRICE_FEED_PRIORITY="chainlink,pyth,bitget,binance,okx"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This quick-start configuration cannot start as written. The priority enables Binance and OKX, but the example provides neither provider's required token mapping. LoadConfig rejects the configuration before startup. Limit the priority list to the feeds configured in this snippet, or include complete Binance and OKX mappings/base configuration.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1bab5a9. The quick start now includes the Binance and OKX mappings, which brings it in line with the example further down the same README that already had them.

Comment thread DEVNET_TOKENREGISTRY_SETUP.md Outdated

```bash
export TOKEN_PRICE_ORACLE_L2_ETH_RPC=http://localhost:8545
export TOKEN_PRICE_ORACLE_L2_TOKEN_REGISTRY_ADDRESS=0x5300000000000000000000000000000000000021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The documented registry-address and token-ID controls are ignored. There are no corresponding flags/config fields: the service always binds the predeploy address and always reads every supported token ID from the contract. This is especially misleading because it hides the full-batch coverage failure. Either implement both options end-to-end with tests, or remove these exports and document the actual fixed behavior.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1bab5a9. Both exports are gone from the devnet guide.

They were not only in the new doc: env.example and docker-compose.yml carried the same two dead variables from before this PR, so those were cleaned up in the same commit rather than leaving two more copies of the same misdirection. env.example now states that the registry address is fixed at its predeploy and that token IDs always come from the contract.

corey and others added 3 commits August 3, 2026 16:59
…d docs

Require a Pyth API key whenever the pyth feed is enabled, since Hermes rejects
unauthenticated requests from 2026-08-18. Bound the Pyth exponent magnitude so a
malformed expo cannot ask big.Int for 10^2147483648, and reject publish times
ahead of the local clock instead of accepting the whole staleness window.

Reject NaN and infinities before constructing a big.Float: they pass a bare
`<= 0` test, and NaN panics while an Inf survives until Float.Int returns nil.
Bitget had the same parser twice, one of which had no positivity check at all.

Give Chainlink calls their own deadline. The service context has none and the
RPC transport imposes none, so a hung endpoint would stall the updater forever
and the fallback feeds would never be reached.

Wrap a single configured feed in FallbackPriceFeed rather than returning it
directly. That bypassed the completeness check, letting a partial or empty CEX
batch through as success, which the updater then recorded as a successful cycle
and used to advance the last-successful-update timestamp.

Gate the devnet token pre-registration on fundDevAccounts so it no longer writes
into mainnet and testnet genesis, correct the L2WETH balance slot from 3 (_name)
to 0 (_balances) via an explicit NeedBalanceSlot flag, since a zero slot is a
real slot and cannot double as "no slot", and move the placeholder token
addresses out of the precompile range.

Fix the quick-start config, which enabled Binance and OKX without their
mappings, and drop the documented registry-address and token-ID controls, which
have no corresponding flags.

Co-authored-by: Cursor <[email protected]>
… all-or-nothing

FallbackPriceFeed discarded a feed's entire response when it was missing any one
requested token, and Chainlink and Pyth failed a whole batch on the first token
absent from their mapping. Together that meant a provider covering part of the
active set could never contribute: with any active token missing from the oracle
mappings, Chainlink and Pyth failed every cycle and every token silently came
from a CEX. The devnet configuration this repo documents hits exactly that, since
genesis registers three tokens while the oracle mappings cover two.

Feeds now omit tokens they have no mapping for rather than failing the batch,
which is what the CEX feeds already did, and the fallback layer resolves across
feeds in priority order, passing each one only the tokens still unresolved.

A token that no feed can price no longer blocks the rest of the cycle; it is
reported by its absence from the returned map, and the updater surfaces it
through a warning and the new unresolved_tokens gauge. A cycle that resolves
nothing at all is still an error, so a total feed outage cannot be recorded as a
successful update.

Co-authored-by: Cursor <[email protected]>
Rejecting any publish time ahead of the local clock was too strict for this feed.
The Chainlink path reads an L1 block timestamp, which is always in the past, but
publish_time is the publisher's near-real-time wall clock at second granularity,
so host clock drift would have turned into a total Pyth outage. Allow 30s of skew
while still refusing prices dated far into the future, which was the actual defect:
the previous check accepted anything inside the staleness window, nearly an hour
at the default.

Co-authored-by: Cursor <[email protected]>
@panos-xyz

Copy link
Copy Markdown
Contributor

Code review

Found 4 issues:

  1. local.sh maps the pre-registered devnet token IDs to the wrong assets. The setup document defines ID 1 as BTC, ID 2 as ETH, and ID 3 as BGB, but the script submits BGB, BTC, and a $1.0 stablecoin price respectively. Following the documented devnet setup therefore writes incorrect prices for all three tokens.

--price-threshold 100 \
--price-feed-priority bitget \
--token-mapping-bitget "1:BGBUSDT,2:BTCUSDT,3:\$1.0" \
--bitget-api-base-url https://api.bitget.com \
--log-level info \

  1. The Pyth feed rejects an empty API key immediately, even though the adjacent comment says Hermes authentication is required only from 2026-08-18. Before that cutover, an otherwise valid unauthenticated Pyth configuration is rejected during startup (also independently by config validation), preventing the oracle from starting.

if maxStaleness <= 0 {
return nil, fmt.Errorf("pyth max staleness must be positive")
}
// Hermes requires authentication from 2026-08-18 onwards, on both the current
// and the upgraded endpoint, so an unauthenticated feed is not a usable config.
apiKey = strings.TrimSpace(apiKey)
if apiKey == "" {
return nil, fmt.Errorf("pyth price feed requires --pyth-api-key")
}

  1. The Docker command in the new devnet setup cannot be run as documented. It passes --env-file devnet.env, but the PR does not provide that file or a command to create it; the only example is token-price-oracle/env.example, which documents copying to .env. Docker exits before creating the oracle container when devnet.env is absent.

cd token-price-oracle
./build/bin/token-price-oracle
```
Or run the container with an explicit name:
```bash
docker run -d \
--name token-price-oracle \
--network docker_default \
--env-file devnet.env \
morph/token-price-oracle:latest
docker logs -f token-price-oracle

  1. Removing the Layer1-specific finalized confirmation override changes the default for Layer1 verification nodes to 10-block-old, potentially reorgable L1 data. If a pre-finality reorg changes an already-processed commit batch, the reorg recovery path rewinds L1 cursor/database state but does not roll back already-produced L2 blocks, so replacement derivation can diverge and stop until manual intervention.

// DefaultConfig returns the default derivation configuration.
func DefaultConfig() *Config {
return &Config{
L1: &types.L1Config{
// Fixed-depth (latest-N) confirmations rather than the SafeBlockNumber
// tag: 10 blocks (~2 min on mainnet) keeps lag low, and the always-on
// L1 reorg detector (SPEC-005 §4.7.6 in reorg.go) rewinds the
// derivation cursor on hash mismatch so a deeper reorg is recoverable.
// Applies to every verify mode. Layer1 validators derive their whole
// chain from L1, so a deeper default translates directly into head
// lag and into delayed batch-divergence alerts; deployments that
// want a consensus-backed read instead set
// --derivation.confirmations=-4 (safe) or -3 (finalized) explicitly.
Confirmations: DefaultConfirmations,
},

🤖 Generated with Claude Code

The devnet tokens were registered with 10^(18-decimals), which double-applies
the decimals adjustment the oracle already makes on its own. That collapses to
a scale of 1 for the two 18-decimal tokens, and since priceRatio is truncated
to a uint256, any token cheaper than ETH then stores a ratio of 0. BGB was
dropped every cycle with only a "Skipping zero price" warning and
calculateTokenAmount reverted for it, which defeats the one token in the set
that exercises a CEX-specific feed.

Scale cancels out of calculateTokenAmount, so its only role is preserving
significant digits, and 10^decimals is the convention L2TokenRegistry's own
tests use. It also makes the stored ratio equal 10^18 * tokenPrice/ethPrice for
every token regardless of decimals.

Also drop BGB from the documented OKX mapping, since OKX does not list it.

Co-authored-by: Cursor <[email protected]>
corey and others added 4 commits August 3, 2026 19:40
The restart recipe reads as a full reset, but docker compose down leaves named
volumes in place, so L1 resumes from its old chain and the redeployed L1
contracts land on different addresses each run.

Co-authored-by: Cursor <[email protected]>
The guide listed shell exports and then passed --env-file devnet.env, a file it
never created, so the documented container command could not start.

Co-authored-by: Cursor <[email protected]>
The script predates the pre-registered devnet tokens and still mapped ID 1 to
BGB, ID 2 to BTC and ID 3 to a fixed $1.0. Genesis registers ID 1 as BTC, ID 2
as ETH and ID 3 as BGB, so running the documented devnet flow wrote a wrong
price to all three.

Co-authored-by: Cursor <[email protected]>
…re the cutover

The comment stated the 2026-08-18 date and then rejected an empty key, which
reads as a contradiction while unauthenticated requests still work. Say why: an
updater configured today outlives the cutover, so failing at startup beats every
request failing later with nothing changed on our side.

Co-authored-by: Cursor <[email protected]>
@curryxbo

curryxbo commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — going through these in order.

1. local.sh token IDs — agreed, fixed in 09c0a6c. The mapping is now 1:BTCUSDT,2:ETHUSDT,3:BGBUSDT, matching genesis. Worth noting where this came from: the script predates the pre-registered tokens (it arrived with #809/#826), so its mapping was arbitrary until this PR gave IDs 1-3 a fixed meaning. Since it points at localhost:8545 with the devnet owner key it is effectively the devnet script, so it now carries a comment pointing at DEVNET_TOKENREGISTRY_SETUP.md. The $1.0 stablecoin syntax is still shown in the comment block, under ID 4.

2. Pyth API key before the cutover — keeping it required, but the comment was the problem. You are right that unauthenticated requests still work until 2026-08-18, and the comment stated the date and then rejected an empty key without bridging the two. Rewritten in 5d60d5a to give the actual reason: an updater configured today keeps running past the cutover, at which point every Hermes request starts failing with nothing having changed on our side. Refusing to start is the earlier and louder failure than a mid-flight outage, and the cost of the strict check is one free API key. This is also the same direction as your earlier comment that the key cannot stay optional. Happy to gate it on the date instead if you would rather not block the next two weeks. I also fixed a stale // Optional Pyth Hermes API key on the config field.

3. devnet.env — agreed, fixed in ede8cca. The guide now generates the file from token-price-oracle/ and passes --env-file ./devnet.env. It also notes that --env-file takes KEY=value lines rather than shell exports, which was the second reason the documented command could not work, and that the file holds a private key so it must stay out of version control.

4. Layer1 confirmations — acknowledged, and I am not going to argue it here. The deep-reorg path you describe is a fair concern and deserves better than a note buried in this PR, so I would rather track it separately than bundle the discussion into a price-feed change. I will follow up on it outside this review.

Separately, this branch has now been through a full devnet integration run, which turned up one defect in the devnet additions and confirmed the rest. Details in the following comment.

@curryxbo

curryxbo commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Devnet integration run

Ran the branch end to end on a local devnet (fresh genesis, L1 + L2 + oracle) rather than relying on unit tests alone. It found one defect in the devnet additions and confirmed the rest.

Defect found: the pre-registered tokens used the wrong scale — fixed in 9c5e25b.

devnet_tokens.go registered scale = 10^(18-decimals), which double-applies the decimals adjustment the updater already makes in calculatePriceRatioWithInfo. That collapses to scale = 1 for the two 18-decimal tokens, and since priceRatio is truncated to a uint256, anything cheaper than ETH stores 0. BGB fetched fine (1.5954 USD) but was dropped every cycle with only a Skipping zero price warning, and calculateTokenAmount(3, ...) reverted InvalidPrice — so the one token in the set that exercises a CEX-specific feed was the one that never worked. BTC survived only because its wrong value was still numerically large.

Scale cancels out of calculateTokenAmount, so its only role is preserving significant digits. Now 10^decimals, matching how L2TokenRegistry.t.sol registers USDC (1e6) and DAI (1e18), which makes the stored ratio equal 10^18 * tokenPrice/ethPrice for every token regardless of decimals. Added a test that walks the formula for the cheapest token so a scale that truncates to zero fails in CI.

Verified on-chain after the fix:

result
priceRatio 1 / 2 / 3 3.399e19 / 1e18 / 8.606e14, each equal to 10^18 * Pt/Pe
calculateTokenAmount(3, 1 ETH) 1162.0 BGB, matching 1845.49/1.5882
Skipping zero price 0 occurrences

Review fixes confirmed against a running chain:

  • Genesis gate — regenerating with only fundDevAccounts flipped leaves the registry with its own 5 slots instead of 27; all 22 token slots disappear, no shared slot differs.
  • WETH balance slotgetTokenInfo(2) returns balanceSlot=0x00, hasBalanceSlot=true, while tokens 1 and 3 return 0x00, false. Real slot 0 and "no slot" are now distinguishable.
  • Precompile addresses — tokens 1 and 3 sit at 0x1111…/0x3333… with no code, and getTokenIdByAddress reverts TokenNotFound for 0x01 and 0x03.
  • Cross-provider merge — with Binance mapping IDs 1-2 and Bitget mapping 1-3, three consecutive cycles logged Binance requested=3 resolved=2 then Bitget requested=1 resolved=1. The second feed is asked only for what is still unresolved.
  • Single-feed validation — with Binance alone, the fallback_price_feed warning still fires and unresolved_tokens reports 1 while IDs 1-2 keep updating. Before the fix createFallbackPriceFeed returned feeds[0] and this check did not exist.
  • Chainlink per-call timeout — against a socket that accepts and never responds, the feed errored context deadline exceeded 10.006s after creation and fell through to Binance, which then received the full requested=3. A failing feed does not shrink the next feed’s request.
  • Pyth key — rejected at config load with no network call made.

… root

Every other document in the repo lives beside the component it describes, with
README.md the only file at the root. The guide is about running the oracle
against a devnet, so it belongs with the oracle; genesis pre-registration is
step zero rather than the subject.

Co-authored-by: Cursor <[email protected]>
@curryxbo
curryxbo merged commit fc629c0 into main Aug 4, 2026
15 checks passed
@curryxbo
curryxbo deleted the feat/977-chainlink-token-price-oracle branch August 4, 2026 03:00
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.

Token Price Oracle enhancement: multi-source aggregation, freshness/heartbeat, anomaly detection

2 participants