Skip to content

SIG-755: multi-asset oracle infrastructure#78

Merged
worjs merged 2 commits into
mainfrom
feat/SIG-755-multi-asset-oracle
May 11, 2026
Merged

SIG-755: multi-asset oracle infrastructure#78
worjs merged 2 commits into
mainfrom
feat/SIG-755-multi-asset-oracle

Conversation

@worjs
Copy link
Copy Markdown
Contributor

@worjs worjs commented May 11, 2026

Context

Jira: SIG-755 (parent epic SIG-740 multi-asset oracle migration).

Moves oracle configuration from protocol-wide globals to per-market values so future non-BTC markets can be added without redeploying. This PR is the contract track; sibling repos (sdk, server, admin, subgraph, app) follow in separate issues.

Decisions

  • Market struct gains three trailing fields feedId, feedDecimals, tickScale (append-only). MarketOracleConfig is a new tuple introduced beside Market in ISignalsCore and is the 11th createMarket argument.
  • _toSettlementTick is extracted to contracts/lib/OracleTickLib.sol as the single source of truth. The hard-coded 1_000_000 divisor in the old copies is replaced with market.tickScale.
  • Global redstoneFeedId / redstoneFeedDecimals storage is preserved at the same slots, renamed to private __deprecated_* with @custom:oz-renamed-from. The public getters disappear as a consequence.
  • setRedstoneConfig is retained but slimmed to (uint64 maxSampleDistance, uint64 futureTolerance). Per-asset values now live on each market.
  • reinitializeV2() (reinitializer(2) onlyOwner) backfills existing markets with hard-coded BTC defaults (feedId="BTC", feedDecimals=8, tickScale=1_000_000) and emits OracleConfigBackfilled(marketCount). Markets with numBins == 0 are skipped.
  • Per-market setMarketOracleConfig setter is intentionally NOT added in this PR — deferred to a future operations issue.
  • PrepareSafeUpgrade switches the upgrade plan from a single upgradeToAndCall to a Safe MultiSend (operation=1 DelegateCall) bundling upgradeToAndCall(newImpl, reinitializeV2()) followed by setModules(...). The multiSendAddress is loaded from script/config/deploy-<env>.json and cross-checked against the Safe SDK's known network address.
  • Default MODULES for PrepareSafeUpgrade changes from trade,lifecycle,vault to lifecycle,oracle, with a hard requirement that both be present for the SIG-755 cutover.

Impact

ABI surface changes that downstream repos consume:

  • v1-subgraph: MarketCreated event gains three trailing fields (bytes32 feedId, uint8 feedDecimals, uint64 tickScale). New event OracleConfigBackfilled(uint256 marketCount). OracleConfigUpdated event arg list is reduced from 4 to 2 (drops feedId, feedDecimals).
  • v1-sdk / v1-server / v1-admin / signals-app: createMarket signature gains an 11th argument MarketOracleConfig. setRedstoneConfig signature drops feedId and feedDecimals (now 2-arg). The redstoneFeedId() and redstoneFeedDecimals() view getters are removed — callers must read getMarket(marketId).feedId / .feedDecimals instead. Market struct gains 3 trailing fields, shifting any positional ABI decoders. New errors InvalidOracleConfig, OracleConfigMissing(uint256).
  • Regenerated ABIs in abis/*.json and storage layouts in storage-snapshots/*.json are the source of truth for inline-ABI updates in sibling repos.

No sibling PRs are open yet (search returned none). Sibling updates land in follow-up issues against this PR's published ABIs.

Risk Areas

  • Storage layout (SignalsCoreStorage.sol): redstoneFeedId / redstoneFeedDecimals slot retention via @custom:oz-renamed-from, new Market trailing fields. Storage diff CI gate applies.
  • Upgrade flow (PrepareSafeUpgrade.s.sol, scripts/ops/safe.ts): change from direct upgradeToAndCall to MultiSend DelegateCall. simulateTx now branches on operation and uses simulateAndRevert for delegatecalls; new bespoke decoder decodeSimulateAndRevert handles Safe's compact revert payload.
  • reinitializeV2(): iterates 1..nextMarketId writing to existing market storage. The numBins == 0 skip is the only filter; if any non-BTC fixture market exists it will be silently mislabeled as BTC. Production state is BTC-only at this revision.
  • Tick conversion centralization (OracleTickLib.sol): both module copies of _toSettlementTick are replaced. Behavioral parity for tickScale=1_000_000 is the key invariant.
  • Oracle submit/finalize guards: new OracleConfigMissing(marketId) reverts at submitSettlementSample, primary settlement finalize, and finalizeSecondarySettlement when feedId == bytes32(0) or tickScale == 0. Markets that miss the backfill cannot settle.
  • Fork test helper (test/foundry/fork/base/ForkBaseTest.sol): bypasses currentBatchId and _batchAggregations slot writes directly to repair a live dev-fork drift where processed-batch flags are ahead of currentBatchId. Scope is fork test setup only.
  • abi.encodeWithSignature selector in SignalsCore.createMarket delegation: the string-encoded signature "createMarket(...,(bytes32,uint8,uint64))" must match the actual lifecycle module function exactly for the delegatecall selector to resolve.

Findings

PrepareSafeUpgrade previously hard-coded a per-tx layout in safe-upgrade-copy/ files and a 6-step Safe UI flow. Filenames and ordering have changed; any external runbook referencing the old 01-to.txt … 06-calldata-upgradeToAndCall.txt layout needs an update outside this repo.

- Per-market oracle config (feedId, feedDecimals, tickScale) appended to Market struct
- createMarket signature accepts MarketOracleConfig, validates per-market config
- _toSettlementTick extracted to OracleTickLib for single source of truth
- Global oracle asset config storage-zombified (private + @Custom:oz-renamed-from)
- setRedstoneConfig signature slimmed to protocol-level sample timing (2-param)
- reinitializeV2 backfills all existing BTC markets atomically
- New errors: InvalidOracleConfig, OracleConfigMissing(uint256)
- Script + deploy config + TypeScript types updated to remove global feed keys
- Safe MultiSend support added to PrepareSafeUpgrade.s.sol and scripts/ops/safe.ts
Copy link
Copy Markdown

@signals-reviewer signals-reviewer Bot left a comment

Choose a reason for hiding this comment

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

Summary

Moves oracle configuration from protocol-wide globals to per-market values. Market struct gains three trailing fields (feedId, feedDecimals, tickScale), createMarket accepts a new MarketOracleConfig tuple, _toSettlementTick is extracted to OracleTickLib.sol, global feed storage is zombified with @custom:oz-renamed-from, reinitializeV2() backfills existing BTC markets, setRedstoneConfig is slimmed to 2 params, and PrepareSafeUpgrade switches to Safe MultiSend. 54 files changed, +1523 / -690.

Cross-PR Context

Sibling PRs (SIG-755)

  • No sibling PRs found. The PR description states sibling repos (sdk, server, admin, subgraph, app) follow in separate issues against published ABIs. This is acceptable because v1-contract is the upstream source of truth and downstream repos consume the regenerated ABIs.

Impact Map Check

  • v1-subgraph: MarketCreated event gains 3 trailing fields, OracleConfigUpdated event args reduced from 4 to 2, new OracleConfigBackfilled event. Subgraph update required but correctly deferred to follow-up issue.
  • v1-sdk / v1-server / v1-admin / signals-app: createMarket signature change, setRedstoneConfig signature change, redstoneFeedId()/redstoneFeedDecimals() getters removed, Market struct expanded. All deferred to follow-up issues.

Recent Review Patterns

  • PR#75 (SIG-743, storage layout CI): flagged build-info caching and missing UUPS-discovery test. Neither pattern appears in this PR.

Issues

  • 🟡 [Robustness]: CreateMarket.s.sol hard-codes bytes32("BTC"), feedDecimals: 8, tickScale: 1_000_000 (line 177) and does not read oracle config from the market JSON template, while the template (market-template.json) has no oracle config fields either. (script/ops/CreateMarket.s.sol:177)

    Scenario: When operations needs to create a non-BTC market (the whole purpose of this multi-asset migration), they must remember to modify CreateMarket.s.sol source code rather than updating a config file. This breaks the existing config-driven pattern where all market parameters come from JSON. If someone copies the template and fills in ETH market params but doesn't touch the script, the market gets BTC oracle config.
    Evidence: Verified market-template.json has no feedId/feedDecimals/tickScale fields. Verified CreateMarket.s.sol line 177 hard-codes BTC values. The PR description says "per-market setMarketOracleConfig setter is intentionally NOT added", but CreateMarket.s.sol is the creation path, not the setter path. Adding oracle.feedId, oracle.feedDecimals, oracle.tickScale to the JSON template and reading them in the script would maintain the config-driven pattern without requiring a setter.

Suggestions

  • [Test Coverage]: No test verifies that OracleTickLib.toSettlementTick handles tickScale = 0 (division by zero). The guard is in the callers (require(market.tickScale != 0)), but if a future caller forgets the guard, the EVM would revert with a generic panic rather than a meaningful error. (contracts/lib/OracleTickLib.sol:18)

    Scenario: A future function calls OracleTickLib.toSettlementTick without checking tickScale != 0. The EVM panics on division by zero (Panic(0x12)) instead of reverting with OracleConfigMissing. Currently all 3 callers have the guard, so this is defense-in-depth rather than a live bug.
    Evidence: Verified all 3 call sites have the tickScale != 0 guard before calling the library. The test OracleTickLib.t.sol tests BTC scale parity, custom scale, and fuzz parity, but never tests the zero-tickScale path. Adding a test that verifies the EVM panic on tickScale=0 (or adding a require inside the library) would document the invariant.

Verdict

REQUEST_CHANGES: the CreateMarket.s.sol hard-coded oracle config issue is blocking. The ops script should read oracle configuration from the market JSON template to maintain the config-driven deployment pattern. The test coverage suggestion is also blocking per review rules.

Copy link
Copy Markdown

@signals-reviewer signals-reviewer Bot left a comment

Choose a reason for hiding this comment

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

Summary

Moves oracle configuration from protocol-wide globals to per-market values. Market struct gains three trailing fields (feedId, feedDecimals, tickScale), createMarket accepts a new MarketOracleConfig tuple, _toSettlementTick is extracted to OracleTickLib.sol, global feed storage is zombified with @custom:oz-renamed-from, reinitializeV2() backfills existing BTC markets, setRedstoneConfig is slimmed to 2 params, and PrepareSafeUpgrade switches to Safe MultiSend. 54 files changed, +1523 / -690.

Cross-PR Context

Sibling PRs (SIG-755)

  • No sibling PRs found. The PR description states sibling repos (sdk, server, admin, subgraph, app) follow in separate issues against published ABIs. This is acceptable because v1-contract is the upstream source of truth and downstream repos consume the regenerated ABIs.

Impact Map Check

  • v1-subgraph: MarketCreated event gains 3 trailing fields, OracleConfigUpdated event args reduced from 4 to 2, new OracleConfigBackfilled event. Subgraph update required but correctly deferred to follow-up issue.
  • v1-sdk / v1-server / v1-admin / signals-app: createMarket signature change, setRedstoneConfig signature change, redstoneFeedId()/redstoneFeedDecimals() getters removed, Market struct expanded. All deferred to follow-up issues.

Recent Review Patterns

  • PR#75 (SIG-743, storage layout CI): flagged build-info caching and missing UUPS-discovery test. Neither pattern appears in this PR.

Issues

  • 🟡 [Robustness]: CreateMarket.s.sol hard-codes bytes32("BTC"), feedDecimals: 8, tickScale: 1_000_000 (line 177) and does not read oracle config from the market JSON template, while the template (market-template.json) has no oracle config fields either. (script/ops/CreateMarket.s.sol:177)

    Scenario: When operations needs to create a non-BTC market (the whole purpose of this multi-asset migration), they must remember to modify CreateMarket.s.sol source code rather than updating a config file. This breaks the existing config-driven pattern where all market parameters come from JSON. If someone copies the template and fills in ETH market params but doesn't touch the script, the market gets BTC oracle config.
    Evidence: Verified market-template.json has no feedId/feedDecimals/tickScale fields. Verified CreateMarket.s.sol line 177 hard-codes BTC values. The PR description says "per-market setMarketOracleConfig setter is intentionally NOT added", but CreateMarket.s.sol is the creation path, not the setter path. Adding oracle.feedId, oracle.feedDecimals, oracle.tickScale to the JSON template and reading them in the script would maintain the config-driven pattern without requiring a setter.

Suggestions

  • [Test Coverage]: No test verifies that OracleTickLib.toSettlementTick handles tickScale = 0 (division by zero). The guard is in the callers (require(market.tickScale != 0)), but if a future caller forgets the guard, the EVM would revert with a generic panic rather than a meaningful error. (contracts/lib/OracleTickLib.sol:18)

    Scenario: A future function calls OracleTickLib.toSettlementTick without checking tickScale != 0. The EVM panics on division by zero (Panic(0x12)) instead of reverting with OracleConfigMissing. Currently all 3 callers have the guard, so this is defense-in-depth rather than a live bug.
    Evidence: Verified all 3 call sites have the tickScale != 0 guard before calling the library. The test OracleTickLib.t.sol tests BTC scale parity, custom scale, and fuzz parity, but never tests the zero-tickScale path. Adding a test that verifies the EVM panic on tickScale=0 (or adding a require inside the library) would document the invariant.

Verdict

REQUEST_CHANGES: the CreateMarket.s.sol hard-coded oracle config issue is blocking. The ops script should read oracle configuration from the market JSON template to maintain the config-driven deployment pattern. The test coverage suggestion is also blocking per review rules.

Copy link
Copy Markdown
Contributor Author

@worjs worjs left a comment

Choose a reason for hiding this comment

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

Summary

Moves oracle configuration from protocol-wide globals to per-market values. Market struct gains three trailing fields (feedId, feedDecimals, tickScale), createMarket accepts a new MarketOracleConfig tuple, _toSettlementTick is extracted to OracleTickLib.sol, global feed storage is zombified with @custom:oz-renamed-from, reinitializeV2() backfills existing BTC markets, setRedstoneConfig is slimmed to 2 params, and PrepareSafeUpgrade switches to Safe MultiSend. 54 files changed, +1523 / -690.

Cross-PR Context

Sibling PRs (SIG-755)

  • No sibling PRs found. The PR description states sibling repos (sdk, server, admin, subgraph, app) follow in separate issues against published ABIs. This is acceptable because v1-contract is the upstream source of truth and downstream repos consume the regenerated ABIs.

Impact Map Check

  • v1-subgraph: MarketCreated event gains 3 trailing fields, OracleConfigUpdated event args reduced from 4 to 2, new OracleConfigBackfilled event. Subgraph update required but correctly deferred to follow-up issue.
  • v1-sdk / v1-server / v1-admin / signals-app: createMarket signature change, setRedstoneConfig signature change, redstoneFeedId()/redstoneFeedDecimals() getters removed, Market struct expanded. All deferred to follow-up issues.

Recent Review Patterns

  • PR#75 (SIG-743, storage layout CI): flagged build-info caching and missing UUPS-discovery test. Neither pattern appears in this PR.

Issues

  • 🟡 [Robustness]: CreateMarket.s.sol hard-codes bytes32("BTC"), feedDecimals: 8, tickScale: 1_000_000 (line 177) and does not read oracle config from the market JSON template, while the template (market-template.json) has no oracle config fields either. (script/ops/CreateMarket.s.sol:177)

    Scenario: When operations needs to create a non-BTC market (the whole purpose of this multi-asset migration), they must remember to modify CreateMarket.s.sol source code rather than updating a config file. This breaks the existing config-driven pattern where all market parameters come from JSON. If someone copies the template and fills in ETH market params but doesn't touch the script, the market gets BTC oracle config.
    Evidence: Verified market-template.json has no feedId/feedDecimals/tickScale fields. Verified CreateMarket.s.sol line 177 hard-codes BTC values. The PR description says "per-market setMarketOracleConfig setter is intentionally NOT added", but CreateMarket.s.sol is the creation path, not the setter path. Adding oracle.feedId, oracle.feedDecimals, oracle.tickScale to the JSON template and reading them in the script would maintain the config-driven pattern without requiring a setter.

Suggestions

  • [Test Coverage]: No test verifies that OracleTickLib.toSettlementTick handles tickScale = 0 (division by zero). The guard is in the callers (require(market.tickScale != 0)), but if a future caller forgets the guard, the EVM would revert with a generic panic rather than a meaningful error. (contracts/lib/OracleTickLib.sol:18)

    Scenario: A future function calls OracleTickLib.toSettlementTick without checking tickScale != 0. The EVM panics on division by zero (Panic(0x12)) instead of reverting with OracleConfigMissing. Currently all 3 callers have the guard, so this is defense-in-depth rather than a live bug.
    Evidence: Verified all 3 call sites have the tickScale != 0 guard before calling the library. The test OracleTickLib.t.sol tests BTC scale parity, custom scale, and fuzz parity, but never tests the zero-tickScale path. Adding a test that verifies the EVM panic on tickScale=0 (or adding a require inside the library) would document the invariant.

Verdict

REQUEST_CHANGES: the CreateMarket.s.sol hard-coded oracle config issue is blocking. The ops script should read oracle configuration from the market JSON template to maintain the config-driven deployment pattern. The test coverage suggestion is also blocking per review rules.

@worjs
Copy link
Copy Markdown
Contributor Author

worjs commented May 11, 2026

Response to review (round 2)

Thanks for the careful review. Round 2 applies the test coverage suggestion as c18c5de. The CreateMarket.s.sol concern I respectfully dispute on technical grounds, with a 4-step argument below.

Applied — OracleTickLib zero-tickScale invariant test (commit c18c5de)

Added test_zeroTickScaleCausesEvmPanic() in test/foundry/unit/lib/OracleTickLib.t.sol. The test sets market.tickScale = 0 through the harness and asserts the library reverts (EVM Panic 0x12). This documents the lib's reliance on caller-side guards without introducing a lib-internal check.

Lib-internal require(tickScale != 0) was deliberately not added. The library has no marketId in scope, so it cannot produce the meaningful OracleConfigMissing(marketId) error that the three production call sites (submitSettlementSample, finalizePrimarySettlement, finalizeSecondarySettlement) emit. Pushing the guard into the lib would either swallow the marketId-tagged revert or duplicate the check; both make the failure surface worse than the current state.

Disputed — CreateMarket.s.sol BTC oracle hard-code

The review frames bytes32("BTC"), 8, 1_000_000 at L177 as "breaking the config-driven pattern" with a "non-BTC ETH-params footgun". I believe the framing is incorrect, on the following grounds.

1. There is no "config-driven multi-asset pattern" at HEAD for this PR to break.

The operational pipeline hard-codes BTC at multiple layers; this script is one consistent piece of it, not an outlier:

  • v1-server/src/config/configuration.ts:7-11REDSTONE_DATA_FEED = 'BTC' global env var. The cron that drives createMarket reads this. Plan section 7.2 explicitly preserves it: "그 외 그대로: configuration.ts:7-11REDSTONE_DATA_FEED='BTC' 글로벌 config, Redstone submitter, Binance service, ATR/anchor 계산, OG copy".
  • v1-server/src/modules/markets/signals-core-operator.service.ts:415 — single Redstone adapter dataFeed 'BTC'. Per epic appendix: "Multi-asset 운영 시 동적화 필요".
  • v1-integration/tests/helpers/redstone.js:4-6DATA_FEED_ID="BTC", FEED_DECIMALS=8. Plan section 7.6 keeps these.
  • v1-contract/test/foundry/base/RedstoneHelper.sol:14bytes32 internal constant DATA_FEED_ID = bytes32("BTC"), FEED_DECIMALS = 8 hard-coded.
  • signals-app — UI copy, OG image template, chart pipelines all BTC-specific (plan section 7.5).
  • Pre-SIG-755 script/config/deploy-*.json had redstone.feedId = "BTC" at the global scope. SIG-755 removes those keys, and the same BTC values move into the script. No config knob is removed; the location moves.

The pipeline is BTC-only by design at every layer. Asking only this script to look "asset-agnostic" produces a single misleading entrypoint feeding a stack that interprets every market as BTC.

2. The "operator fills in ETH ticks → BTC oracle silently stamped" footgun is mechanically impossible.

Walking the call path:

  1. Operator copies template, edits minTick / maxTick / tickSpacing / numBins for ETH range. Submits.
  2. Script reads JSON, calls createMarket(..., MarketOracleConfig({feedId: bytes32("BTC"), feedDecimals: 8, tickScale: 1_000_000})).
  3. MarketLifecycleModule validates oracle config (D8 of plan: feedId != bytes32(0), feedDecimals in (0, 18], tickScale > 0). All pass.
  4. Market is stamped with BTC oracle config.
  5. Settlement: v1-server cron submits Redstone payload via signals-core-operator.service.ts:415 with dataFeed 'BTC'. Contract reads market.feedId == bytes32("BTC"), calls getOracleNumericValueFromTxMsg(market.feedId) → returns BTC price. Settlement applies BTC price (e.g. 100000_000000) to an ETH-shaped tick range → clamps to maxTick.
  6. Loud failure: subgraph-derived assetSymbol = "BTC", OG image still BTC template, settlement tick visibly absurd vs. tick range. Anyone watching catches it from the first market creation.

Now compare the change reviewer prefers (operator fills in oracle.feedId = "ETH" in JSON):

  1. Same operator path, JSON has ETH oracle config.
  2. Contract validates (feedDecimals != 0, tickScale != 0, feedId != bytes32(0)) — passes.
  3. Market is stamped with ETH oracle config.
  4. Settlement: v1-server cron still calls Redstone with dataFeed 'BTC'. Contract reads market.feedId == bytes32("ETH"). getOracleNumericValueFromTxMsg("ETH")revert (no ETH price in the BTC payload).
  5. Silent failure: settlement permanently stuck. Operator manually markSettlementFailed + finalizeSecondarySettlement days later, with cron-level errors that look like oracle outages.

Hard-coded BTC is a load-bearing assumption announcement: "this script only produces BTC markets". The reviewer's preferred change replaces that with "this script produces whatever you ask for, but downstream only knows BTC", which is strictly more dangerous.

3. Audit narrative is worse with the JSON fields added now.

SIG-740 epic funds one audit covering both BTC operation and future multi-asset infrastructure. If the ops template advertises oracle.feedId / feedDecimals / tickScale fields while the rest of the stack hard-codes BTC, an auditor reading the template reasonably asks "what happens when I set this to ETH?" — and gets to (2) above. The audit narrative becomes "contract supports multi-asset but operational template lies about it". The honest narrative ("ops is BTC-only until post-audit, contract supports more for future") only holds when the template matches operational reality.

4. The future-PR ownership is concrete, not "scope hand-waving".

When multi-asset operations actually starts (post-audit decision per epic Section 13 Open Decisions D1), the same coordinated PR must:

  1. Add oracle.{feedId,feedDecimals,tickScale} to JSON template and per-env market configs.
  2. Make CreateMarket.s.sol read them.
  3. Add setMarketOracleConfig(uint256, MarketOracleConfig) (per plan Non-goals, intentionally NOT added now).
  4. Update v1-server configuration.ts REDSTONE_DATA_FEED to per-market lookup.
  5. Update v1-server Redstone submitter for dynamic asset selection.
  6. Update v1-subgraph indexer for non-BTC assetSymbol derivation (conceptually covered in SIG-760).
  7. Update OG image generation for non-BTC assets.
  8. Update alerting for non-BTC price feeds.

Splitting (1) into this PR while leaving (2-8) for the future PR actively creates a misleading interim state. The natural boundary is "all of {1..8} together, after audit, as one coordinated multi-asset enable PR" — exactly what plan SIG-740 lists as non-goals "다른 자산의 실제 운영 시작" and "Per-market Redstone feed routing의 실전 사용".

Summary

  • Item 2 (test): applied as c18c5de. Lib-internal guard intentionally not added per the marketId-tagged error rationale.
  • Item 1 (CreateMarket.s.sol BTC hard-code): disputed. Keeping the hard-code is safer than adding the JSON fields, given the rest of the pipeline is BTC-only at HEAD and stays BTC-only after this PR. The proposed change would introduce a footgun (settlement-time silent revert disguised as oracle outage), not remove one.

Requesting re-review.

Copy link
Copy Markdown

@signals-reviewer signals-reviewer Bot left a comment

Choose a reason for hiding this comment

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

Re-review Summary

1 new commit since last review (c18c5de: OracleTickLib zero-tickScale invariant test). 1 file changed.

Cross-PR Context

Sibling PRs (SIG-755)

  • No sibling PRs found. v1-contract is the upstream source of truth; downstream repos (v1-subgraph, v1-sdk, v1-server, v1-admin, signals-app) will consume the regenerated ABIs in follow-up issues under the SIG-740 epic. This is acceptable.

Impact Map Check

  • v1-subgraph: MarketCreated event gains 3 trailing fields, OracleConfigUpdated reduced from 4 to 2 args, new OracleConfigBackfilled event. Deferred to sibling issue.
  • v1-sdk / v1-server / v1-admin / signals-app: createMarket signature change, setRedstoneConfig 4-to-2 arg reduction, removed redstoneFeedId()/redstoneFeedDecimals() getters, Market struct expanded. Deferred to sibling issues.

Previous Items

  • [Resolved]: OracleTickLib zero-tickScale test coverage. Resolved by c18c5de adding test_zeroTickScaleCausesEvmPanic() in test/foundry/unit/lib/OracleTickLib.t.sol. The test documents the EVM Panic(0x12) behavior when tickScale = 0, confirming the library relies on caller-side guards. The deliberate omission of a lib-internal require is well-reasoned: the library lacks marketId scope, so it cannot produce the more informative OracleConfigMissing(marketId) error that the three production call sites emit.

  • 🔄 [Accepted]: CreateMarket.s.sol BTC oracle hard-code. The author's rebuttal meets the rigor standard with a 4-point argument backed by cross-repo evidence:

    1. Verified BTC is hard-coded at every operational layer: v1-server/src/config/configuration.ts:8 (REDSTONE_DATA_FEED = 'BTC'), v1-integration/tests/helpers/redstone.js:4 (DATA_FEED_ID = "BTC"), test/foundry/base/RedstoneHelper.sol:14 (DATA_FEED_ID = bytes32("BTC")). No config-driven multi-asset pattern exists to break.
    2. The "operator fills in ETH ticks, BTC oracle silently stamped" scenario is not silent: settlement tick would be visually absurd (BTC price ~100000 applied to ETH-range ticks), and subgraph-derived assetSymbol still reads "BTC". The reverse scenario (JSON-driven ETH oracle config with BTC-only server cron) produces a worse failure: getOracleNumericValueFromTxMsg("ETH") reverts on the BTC Redstone payload, permanently blocking settlement.
    3. Multi-asset enablement is a coordinated cross-stack change (8 items across 6+ repos), and premature template exposure creates a misleading interim state.
    4. The epic (SIG-740 Section 13 Open Decisions D1) explicitly defers multi-asset operations to post-audit.

New Issues

No new issues found. Independent analysis of all 54 changed files confirms:

  • Storage layout: redstoneFeedId/redstoneFeedDecimals slot retention via @custom:oz-renamed-from is correct (types unchanged: bytes32 and uint8). The 3 new Market struct fields (feedId, feedDecimals, tickScale) are appended after deltaEt, which is safe for mapping-stored structs.
  • reinitializeV2(): Loop bounds 1..nextMarketId are correct (nextMarketId is the next-to-assign value via ++nextMarketId). numBins == 0 skip correctly filters uninitialized market slots. reinitializer(2) prevents re-execution. Test coverage (test_reinitializeV2BackfillsExistingMarkets, test_reinitializeV2CannotRunTwice) is adequate.
  • OracleTickLib centralization: The library's toSettlementTick is behaviorally identical to the old _toSettlementTick when tickScale = 1_000_000 (the BTC default). The fuzz test testFuzz_btcScaleParityWithLegacyAlgorithm exhaustively validates this equivalence.
  • Oracle guards: OracleConfigMissing(marketId) reverts are placed at all 3 entry points (submitSettlementSample, finalizePrimarySettlement, finalizeSecondarySettlement) before calling OracleTickLib. Test coverage for all 3 guards exists.
  • abi.encodeWithSignature selector: The string "createMarket(int256,int256,int256,uint64,uint64,uint64,uint32,uint256,address,address,(bytes32,uint8,uint64))" correctly represents MarketOracleConfig as its ABI tuple type.
  • Safe MultiSend encoding: _encodeMultiSendTx uses uint8(0) (CALL) for both sub-transactions, which is correct because they target the coreProxy. The outer Safe transaction uses operation=1 (DELEGATECALL to MultiSend), and the multiSendAddress is cross-checked against the Safe SDK known network address at runtime.
  • simulateAndRevert decoder: decodeSimulateAndRevert in safe.ts correctly handles both ABI-encoded and compact revert payload formats, with proper length checks.
  • build-redstone-submit-calldata.ts: market.feedId ?? market[24] fallback is correct (feedId is the 25th field, index 24 in 0-based tuple decoding).
  • Fork test helper: _alignCurrentBatchToDevForkState correctly handles the dev-fork drift by scanning forward from currentBatchId and writing the aligned value via vm.store. FORK_SKIP_V2_UPGRADE=true for test:fork:advisory:prod is correct since prod hasn't been upgraded yet.

Verdict

APPROVE: all previous items are Resolved (1) or Accepted (1). Zero Unresolved items. No new issues found.

Copy link
Copy Markdown

@signals-reviewer signals-reviewer Bot left a comment

Choose a reason for hiding this comment

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

Re-review Summary

1 new commit since last review (c18c5de: OracleTickLib zero-tickScale invariant test). 1 file changed.

Cross-PR Context

Sibling PRs (SIG-755)

  • No sibling PRs found. v1-contract is the upstream source of truth; downstream repos (v1-subgraph, v1-sdk, v1-server, v1-admin, signals-app) will consume the regenerated ABIs in follow-up issues under the SIG-740 epic. This is acceptable.

Impact Map Check

  • v1-subgraph: MarketCreated event gains 3 trailing fields, OracleConfigUpdated reduced from 4 to 2 args, new OracleConfigBackfilled event. Deferred to sibling issue.
  • v1-sdk / v1-server / v1-admin / signals-app: createMarket signature change, setRedstoneConfig 4-to-2 arg reduction, removed redstoneFeedId()/redstoneFeedDecimals() getters, Market struct expanded. Deferred to sibling issues.

Previous Items

  • [Resolved]: OracleTickLib zero-tickScale test coverage. Resolved by c18c5de adding test_zeroTickScaleCausesEvmPanic() in test/foundry/unit/lib/OracleTickLib.t.sol. The test documents the EVM Panic(0x12) behavior when tickScale = 0, confirming the library relies on caller-side guards. The deliberate omission of a lib-internal require is well-reasoned: the library lacks marketId scope, so it cannot produce the more informative OracleConfigMissing(marketId) error that the three production call sites emit.

  • 🔄 [Accepted]: CreateMarket.s.sol BTC oracle hard-code. The author's rebuttal meets the rigor standard with a 4-point argument backed by cross-repo evidence:

    1. Verified BTC is hard-coded at every operational layer: v1-server/src/config/configuration.ts:8 (REDSTONE_DATA_FEED = 'BTC'), v1-integration/tests/helpers/redstone.js:4 (DATA_FEED_ID = "BTC"), test/foundry/base/RedstoneHelper.sol:14 (DATA_FEED_ID = bytes32("BTC")). No config-driven multi-asset pattern exists to break.
    2. The "operator fills in ETH ticks, BTC oracle silently stamped" scenario is not silent: settlement tick would be visually absurd (BTC price ~100000 applied to ETH-range ticks), and subgraph-derived assetSymbol still reads "BTC". The reverse scenario (JSON-driven ETH oracle config with BTC-only server cron) produces a worse failure: getOracleNumericValueFromTxMsg("ETH") reverts on the BTC Redstone payload, permanently blocking settlement.
    3. Multi-asset enablement is a coordinated cross-stack change (8 items across 6+ repos), and premature template exposure creates a misleading interim state.
    4. The epic (SIG-740 Section 13 Open Decisions D1) explicitly defers multi-asset operations to post-audit.

New Issues

No new issues found. Independent analysis of all 54 changed files confirms:

  • Storage layout: redstoneFeedId/redstoneFeedDecimals slot retention via @custom:oz-renamed-from is correct (types unchanged: bytes32 and uint8). The 3 new Market struct fields (feedId, feedDecimals, tickScale) are appended after deltaEt, which is safe for mapping-stored structs.
  • reinitializeV2(): Loop bounds 1..nextMarketId are correct (nextMarketId is the next-to-assign value via ++nextMarketId). numBins == 0 skip correctly filters uninitialized market slots. reinitializer(2) prevents re-execution. Test coverage adequate.
  • OracleTickLib centralization: Behaviorally identical to old _toSettlementTick when tickScale = 1_000_000. Fuzz test validates equivalence.
  • Oracle guards: OracleConfigMissing(marketId) at all 3 entry points. Test coverage for all 3 exists.
  • ABI selector: createMarket(...,(bytes32,uint8,uint64)) correctly encodes MarketOracleConfig tuple.
  • Safe MultiSend: _encodeMultiSendTx uses uint8(0) CALL for sub-txs (correct). Outer tx uses operation=1 DELEGATECALL. multiSendAddress cross-checked at runtime.
  • simulateAndRevert decoder: Handles both ABI-encoded and compact revert payloads correctly.
  • build-redstone-submit-calldata.ts: market[24] fallback is correct (feedId is 25th field, 0-indexed).
  • Fork test helper: _alignCurrentBatchToDevForkState handles dev-fork drift correctly. FORK_SKIP_V2_UPGRADE=true for advisory:prod is correct.

Verdict

APPROVE: all previous items are Resolved (1) or Accepted (1). Zero Unresolved items. No new issues found.

Copy link
Copy Markdown

@signals-reviewer signals-reviewer Bot left a comment

Choose a reason for hiding this comment

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

Re-review Summary

1 new commit since last review (c18c5de: OracleTickLib zero-tickScale invariant test). 1 file changed.

Cross-PR Context

Sibling PRs (SIG-755)

  • No sibling PRs found. v1-contract is the upstream source of truth; downstream repos (v1-subgraph, v1-sdk, v1-server, v1-admin, signals-app) will consume the regenerated ABIs in follow-up issues under the SIG-740 epic. This is acceptable.

Impact Map Check

  • v1-subgraph: MarketCreated event gains 3 trailing fields, OracleConfigUpdated reduced from 4 to 2 args, new OracleConfigBackfilled event. Deferred to sibling issue.
  • v1-sdk / v1-server / v1-admin / signals-app: createMarket signature change, setRedstoneConfig 4-to-2 arg reduction, removed redstoneFeedId()/redstoneFeedDecimals() getters, Market struct expanded. Deferred to sibling issues.

Previous Items

  • [Resolved]: OracleTickLib zero-tickScale test coverage. Resolved by c18c5de adding test_zeroTickScaleCausesEvmPanic() in test/foundry/unit/lib/OracleTickLib.t.sol. The test documents the EVM Panic(0x12) behavior when tickScale = 0, confirming the library relies on caller-side guards. The deliberate omission of a lib-internal require is well-reasoned: the library lacks marketId scope, so it cannot produce the more informative OracleConfigMissing(marketId) error that the three production call sites emit.

  • 🔄 [Accepted]: CreateMarket.s.sol BTC oracle hard-code. The author's rebuttal meets the rigor standard with a 4-point argument backed by cross-repo evidence:

    1. Verified BTC is hard-coded at every operational layer: v1-server/src/config/configuration.ts:8 (REDSTONE_DATA_FEED = 'BTC'), v1-integration/tests/helpers/redstone.js:4 (DATA_FEED_ID = "BTC"), test/foundry/base/RedstoneHelper.sol:14 (DATA_FEED_ID = bytes32("BTC")). No config-driven multi-asset pattern exists to break.
    2. The "operator fills in ETH ticks, BTC oracle silently stamped" scenario is not silent: settlement tick would be visually absurd (BTC price ~100000 applied to ETH-range ticks), and subgraph-derived assetSymbol still reads "BTC". The reverse scenario (JSON-driven ETH oracle config with BTC-only server cron) produces a worse failure: getOracleNumericValueFromTxMsg("ETH") reverts on the BTC Redstone payload, permanently blocking settlement.
    3. Multi-asset enablement is a coordinated cross-stack change (8 items across 6+ repos), and premature template exposure creates a misleading interim state.
    4. The epic (SIG-740 Section 13 Open Decisions D1) explicitly defers multi-asset operations to post-audit.

New Issues

No new issues found. Independent analysis of all 54 changed files confirms:

  • Storage layout: redstoneFeedId/redstoneFeedDecimals slot retention via @custom:oz-renamed-from is correct (types unchanged: bytes32 and uint8). The 3 new Market struct fields (feedId, feedDecimals, tickScale) are appended after deltaEt, which is safe for mapping-stored structs.
  • reinitializeV2(): Loop bounds 1..nextMarketId are correct (nextMarketId is the next-to-assign value via ++nextMarketId). numBins == 0 skip correctly filters uninitialized market slots. reinitializer(2) prevents re-execution. Test coverage adequate.
  • OracleTickLib centralization: Behaviorally identical to old _toSettlementTick when tickScale = 1_000_000. Fuzz test validates equivalence.
  • Oracle guards: OracleConfigMissing(marketId) at all 3 entry points. Test coverage for all 3 exists.
  • ABI selector: createMarket(...,(bytes32,uint8,uint64)) correctly encodes MarketOracleConfig tuple.
  • Safe MultiSend: _encodeMultiSendTx uses uint8(0) CALL for sub-txs (correct). Outer tx uses operation=1 DELEGATECALL. multiSendAddress cross-checked at runtime.
  • simulateAndRevert decoder: Handles both ABI-encoded and compact revert payloads correctly.
  • build-redstone-submit-calldata.ts: market[24] fallback is correct (feedId is 25th field, 0-indexed).
  • Fork test helper: _alignCurrentBatchToDevForkState handles dev-fork drift correctly. FORK_SKIP_V2_UPGRADE=true for advisory:prod is correct.

Verdict

APPROVE: all previous items are Resolved (1) or Accepted (1). Zero Unresolved items. No new issues found.

@worjs worjs merged commit 8062872 into main May 11, 2026
16 checks passed
@worjs worjs deleted the feat/SIG-755-multi-asset-oracle branch May 11, 2026 02:56
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