Skip to content

feat: add signed context oracle support#439

Open
hardyjosh wants to merge 12 commits intomasterfrom
feat/oracle-signed-context-v2
Open

feat: add signed context oracle support#439
hardyjosh wants to merge 12 commits intomasterfrom
feat/oracle-signed-context-v2

Conversation

@hardyjosh
Copy link
Copy Markdown
Contributor

@hardyjosh hardyjosh commented Apr 12, 2026

Summary

Adds oracle signed context support to the solver, enabling orders that use off-chain oracle data (e.g., real-time equity prices from Alpaca) to be quoted and cleared on-chain.

Proven on Base mainnet

Router arb tx (oracle order ↔ Hydrex DEX pool):
https://basescan.org/tx/0x82c8beb40f07e3df0ea3b50736e0835fe4bf4f571de69e0e555236112fc02cf0

This transaction shows the solver:

  1. Fetching a signed oracle price for wtCOIN from st0x-oracle-server ($160.6/share, signed by the oracle signer)
  2. Quoting the on-chain oracle order with the signed context (order verifies signer, schema version, staleness, price bounds)
  3. Taking 0.08 wtCOIN from the order at the oracle price
  4. Routing through the Hydrex wtCOIN/USDC pool at ~$168/share
  5. Profiting from the $7.4/share price difference

What's new

Oracle module (src/oracle/):

  • extractOracleUrl() — extracts oracle URL from order meta by searching for the RaindexSignedContextOracleV1 CBOR magic number (0xff7a1507ba4419ca)
  • fetchSignedContext() — POSTs ABI-encoded (OrderV4, uint256, uint256, address) to the oracle endpoint, parses the JSON array response, validates the SignedContextV1 shape
  • Health tracking with cooloff: 3 consecutive failures → 5-minute cooloff per URL
  • 5-second hard timeout per request

Oracle integration in quote pipeline (src/oracle/fetch.ts):

  • Called during quoteSingleOrderV3() / quoteSingleOrderV4() before the on-chain quote
  • Injects the signed context into takeOrder.struct.signedContext so the on-chain quote call includes it
  • V4 orders only (oracle context is a V4/V6 feature)

Key fixes in this PR:

  1. ABI encoding: The oracle server (alloy/Rust) uses abi_decode() which expects a wrapping tuple, while viem's encodeAbiParameters produces separate-parameter encoding. Fixed by wrapping all fields in a single tuple parameter.
  2. Response parsing: Oracle servers return a JSON array of SignedContextV1 objects. The solver now unwraps response[0] before validation.
  3. signedContext threading: The clear3() calls in intra-orderbook mode were hardcoding aliceSignedContext=[], bobSignedContext=[]. Now threads through the actual signedContext from each order's takeOrder struct.

Order lifecycle with oracle context

Subgraph → extractOracleUrl(meta) → oracleUrl on Pair
         ↓
quoteSingleOrder() → fetchOracleContext(pair)
                    → POST to oracle URL with ABI-encoded order
                    → inject signedContext into takeOrder.struct
                    → on-chain quote2() with signed context
         ↓
findBestTrade() → router / intra / inter
               → calldata includes signedContext from takeOrder.struct

Test plan

  • Build: npm run build — clean
  • Solver discovers oracle-backed V6 orders from subgraph
  • Oracle URL extracted from CBOR meta (magic 0xff7a1507ba4419ca)
  • Oracle endpoint returns signed context with correct signer, schema v1, 3-element context
  • On-chain quote succeeds with oracle context (verified ratio matches oracle price)
  • Router arb tx on Base mainnetbasescan

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added oracle context signing and verification support for V4 orders.
    • Orders now support oracle metadata extraction and integration.
    • Implemented oracle health monitoring with automatic cooloff during service disruptions.
  • Improvements

    • Enhanced order quoting process with oracle context integration.
    • Improved fallback behavior when oracle services are temporarily unavailable.

Josh Hardy and others added 12 commits April 12, 2026 09:24
1. Add meta field to subgraph queries for order discovery
2. Create oracle module with:
   - extractOracleUrl() placeholder for meta parsing
   - fetchSignedContext() for batch oracle requests
   - Support for batch format (array of contexts)
3. Wire oracle into quoting logic:
   - Extract oracle URL from order meta before quote2
   - Fetch signed context and inject into takeOrder struct
   - Graceful fallback on oracle failures
4. Ensure signed context flows through to takeOrdersConfig

The solver now automatically fetches oracle data for orders that
specify an oracle-url, enabling external data integration.
- Fix ethers v6 → v5 APIs (defaultAbiCoder, arrayify)
- Use ABI.Orderbook.V5.OrderV4 constant instead of hardcoded tuple string
- Add 5s timeout on oracle fetch via AbortController
- Validate SignedContextV1 shape on each response entry
- Extract fetchOracleContext helper to deduplicate quote logic
- Remove noisy console.warn from stub extractOracleUrl
- Type OracleOrderRequest.order properly instead of any
Replace ethers.utils.defaultAbiCoder/arrayify with viem's
encodeAbiParameters/hexToBytes. Use proper viem ABI parameter
definitions instead of string-based encoding.
- Up to 2 retries with exponential backoff (500ms, 1s)
- After 3 consecutive failures, oracle URL enters 5min cooloff
- During cooloff, requests to that URL are skipped immediately
- Cooloff resets on first successful response
- Invalid responses (bad shape, wrong length) also count as failures
- All configurable via module constants
No retries, no delays in the loop. Single attempt with 5s timeout —
if it fails, record the failure and move on. After 3 consecutive
failures the URL enters a 5min cooloff where it's skipped immediately
(no network call at all). This way one bad oracle can't block the
processing of other orders.
…ager

Extract oracle cooloff tracking from module-level singleton into an
OracleManager class. Instance lives on OrderManager, threaded through
to quote functions. This makes it properly scoped to the solver
instance lifecycle and testable.

- OracleManager class in src/oracle/manager.ts
- fetchSignedContext takes OracleManager as parameter
- OrderManager creates and owns the OracleManager instance
- OracleManager is optional in quote functions for backward compat
Follow codebase conventions:
- Oracle health map lives on SharedState.oracleHealth
- fetchOracleContext is a standalone fn with this: SharedState,
  called via .call(state) like processOrder/findBestTrade
- Health helpers (isInCooloff, recordOracleSuccess/Failure) are
  plain exported functions operating on the health map
- No new classes, no module-level singletons
- quoteSingleOrder receives SharedState to thread through
- fetchSignedContext returns Result<SignedContextV1[], string>
- fetchOracleContext returns Result<void, string>
- Callers check .isErr() instead of try/catch
- Follows codebase convention for error handling
…terface

- OracleOrderRequest.order uses Order.V3 | Order.V4 from order/types
- OracleOrderRequest.counterparty typed as 0x
- Drop custom SignedContextV1 interface — signed context is already
  typed as any[] on TakeOrderV3/V4, and the response validation
  ensures the right shape at runtime
- fetchSignedContext returns Result<any[], string> matching the
  existing signedContext field type
- extractOracleUrl: implement CBOR meta parsing (was a stub returning null)
- Add oracleUrl field to PairBase, thread through V3/V4 fromArgs
- Add meta field to SgOrder type (already in subgraph query)
- fetchOracleContext: use pair.oracleUrl instead of dead (pair as any).meta
- Switch from batch to single request encoding to match oracle server spec
- Restrict oracle requests to V4 orders only
- Strip internal type discriminant before ABI encoding
Two fixes for oracle-backed orders:

1. src/oracle/index.ts: The oracle server returns a JSON **array** of
   SignedContextV1 objects (per the upstream rain.orderbook spec).
   The previous code validated response.signer directly on the parsed
   JSON, which fails on arrays (typeof array === "object" but
   array.signer is undefined). Now unwraps the first element of the
   array before validating.

2. src/core/modes/intra/simulation.ts: The clear3() calls for both
   V3 and V4 order codepaths hardcoded aliceSignedContext=[] and
   bobSignedContext=[]. This means oracle-backed orders always
   reverted on-chain because the signed context was never passed to
   the verifier. Now threads through the actual signedContext from
   each order's takeOrder struct (populated during quoting by
   fetchOracleContext).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
alloy's `(T1, T2, T3, T4).abi_encode()` produces the encoding of a
*single wrapping tuple* (first word is an offset to the tuple content),
while viem's `encodeAbiParameters([T1, T2, T3, T4], [...])` produces
the encoding of *four separate parameters* (first word is the offset
to the first dynamic parameter). These are different ABI layouts.

The oracle server uses alloy's `abi_decode()` which expects the wrapped
form. Fix: encode all four fields as components of a single tuple
parameter instead of four top-level parameters.

Also:
- Skip bounty task bytecode compilation in intra-orderbook mode when
  gasCoveragePercentage="0" (the task is never included in the withdraw
  call anyway, and computing it requires a dispair address which may
  not be available for V6 orderbooks whose registry doesn't expose
  I_INTERPRETER/I_STORE directly).
- Make resolveVersionContracts resilient to V6 deployers that don't
  expose I_INTERPRETER/I_STORE (falls back to using the deployer
  address itself, with a warning).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 12, 2026

Walkthrough

Implements oracle-signed context integration for orders. Adds oracle URL extraction from metadata, health/cooloff tracking for oracle reliability, and conditional signed context fetching. Updates the simulator to handle oracle tasks based on gas coverage and includes oracle context in calldata. Extends GraphQL queries and type definitions to support oracle metadata.

Changes

Cohort / File(s) Summary
Oracle Integration Module
src/oracle/index.ts, src/oracle/fetch.ts
New oracle module implementing URL extraction via CBOR parsing, health/cooloff tracking with consecutive failure counts and timeout management, and fetchSignedContext for POST requests with ABI-encoded parameters, timeout control, and response validation. New async fetchOracleContext function conditionally fetches and injects signed context into order details.
Order Type Extensions
src/order/types/index.ts, src/order/types/v3.ts, src/order/types/v4.ts
Added optional oracleUrl field to PairBase type and derived extraction from order metadata in V3 and V4 pair factories using extractOracleUrl.
Order Processing Pipeline
src/order/index.ts, src/order/quote.ts
Added optional state parameter to quoteSingleOrder and V3/V4 variants; introduced pre-quote oracle health step calling fetchOracleContext when state is provided, with error logging but continued execution.
Simulator & State
src/core/modes/intra/simulation.ts, src/state/index.ts, src/state/contracts.ts
Conditional oracle task construction based on gasCoveragePercentage === "0" with zero-address placeholder; updated calldata to use signedContext from order details. Added oracleHealth tracking map to SharedState. Modified contract resolution fallback to use dispair address when interfaces unavailable.
Subgraph Extensions
src/subgraph/query.ts, src/subgraph/types.ts
Added meta field to GraphQL order selections and SgOrder type to propagate oracle metadata from chain.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant OrderManager
    participant QuoteService
    participant OracleService
    participant SharedState
    participant Simulator

    Client->>OrderManager: quoteOrder()
    OrderManager->>QuoteService: quoteSingleOrder(orderDetails, client, state)
    
    alt state provided
        QuoteService->>OracleService: fetchOracleContext(orderDetails)
        
        alt oracleUrl present and V4 order
            OracleService->>OracleService: Check cooloff status
            alt not in cooloff
                OracleService->>OracleService: POST to oracle with ABI-encoded request
                OracleService->>OracleService: Validate response & record success/failure
                OracleService->>SharedState: Update oracleHealth map
                OracleService->>orderDetails: Inject signedContext into takeOrder
            else in cooloff
                OracleService->>OracleService: Skip oracle call
            end
        end
        
        alt error fetching context
            QuoteService->>QuoteService: Log warning, continue
        end
    end
    
    QuoteService->>QuoteService: Call viemClient.call() for quote
    QuoteService->>Client: Return quote result
    
    Client->>Simulator: setTransactionData()
    Simulator->>Simulator: Build oracle task based on gasCoveragePercentage
    alt gasCoveragePercentage === "0"
        Simulator->>Simulator: Use zero-address placeholder task
    else
        Simulator->>Simulator: Compile oracle task on-chain
    end
    Simulator->>Simulator: Construct calldata with signedContext from orderDetails
    Simulator->>Client: Return transaction data
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.85% 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 'feat: add signed context oracle support' is clear, specific, and directly summarizes the main change—adding oracle signed context functionality to enable orders relying on off-chain oracle data to be quoted and cleared on-chain.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/oracle-signed-context-v2

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/oracle/index.ts`:
- Around line 197-203: The fetch to the untrusted oracle URL should be guarded
to mitigate SSRF: add a URL validation step (e.g., an isAllowedOracleUrl helper)
and call it before the try/fetch in the code around the fetch(...) call in
src/oracle/index.ts; ensure the validator enforces https only, rejects hostnames
resolving to private/local ranges (localhost, 127.*, 10.*, 172.16-31.*,
192.168.*) and/or checks against a configured allowlist of trusted oracle
domains, and if validation fails throw or return an error instead of performing
the POST to the untrusted URL.
- Around line 73-81: isInCooloff currently mutates OracleHealthMap by resetting
state.cooloffUntil to 0 when expired; change it to be a pure predicate that only
reads state and returns true/false, remove the assignment, and add a new helper
clearExpiredCooloff(healthMap: OracleHealthMap, url: string) that checks the
same expiry and resets cooloffUntil when needed; update callers (e.g., places
that call isInCooloff) to call clearExpiredCooloff where they previously relied
on the side effect, or ensure recordOracleSuccess handles the reset on success.
- Around line 172-176: fetchSignedContext currently returns Result<any, string>;
change its signature to return Promise<Result<SignedContextV1Response, string>>
and use the concrete SignedContextV1/SignedContextV1Response types (alongside
existing OracleOrderRequest and OracleHealthMap) so callers get proper typing.
In the response-validation block, replace the generic Result.ok(...) with
Result.ok(item as SignedContextV1Response) (or construct a
SignedContextV1Response from item) and ensure any validation function references
SignedContextV1 to guarantee the shape before casting.
- Around line 233-242: The current SignedContextV1 check in the validation block
(before calling recordOracleFailure and returning Result.err) is too permissive;
update the validation in the same block to (1) verify signer matches an Ethereum
address regex (0x + 40 hex chars), (2) verify signature is hex (0x-prefixed hex
string of appropriate length, e.g., 130 chars for 65-byte signature or accept
generic 0x + even-length hex), and (3) verify each element of context is a
bytes32 hex string (0x + 64 hex chars); if any check fails call
recordOracleFailure(healthMap, url) and return Result.err with a clear message.
Use the existing symbols (recordOracleFailure, Result.err) and keep validation
deterministic and concise so malformed oracle responses are rejected early.

In `@src/order/quote.ts`:
- Around line 41-46: In quoteSingleOrderV3 remove the unnecessary V4-only oracle
fetch: delete the conditional block that calls fetchOracleContext.call(state,
orderDetails) and inspects oracleResult (the if (state) { const oracleResult =
await fetchOracleContext.call(state, orderDetails); if (oracleResult.isErr()) {
console.warn(...) } } block) because fetchOracleContext returns
Result.ok(undefined) for non-V4 orders; also clean up any now-unused
identifiers/imports (state, fetchOracleContext, oracleResult) in that file.

In `@src/state/contracts.ts`:
- Around line 97-112: The fallback that assigns addresses.dispair to interpreter
and store should only run for Rain V6; update the conditional around the
existing block that checks !interpreter || !store to also verify the runtime
registry/version equals 6 (e.g., check a variable like rainVersion === 6 or
registryVersion === 6), so for v4/v5 the original read failure surfaces instead
of silently falling back. Keep the same warning text and assignments to
addresses.dispair, interpreter, and store inside the now-scoped V6-only branch.

In `@src/state/index.ts`:
- Around line 227-228: The oracleHealth Map currently stores entries
indefinitely (oracleHealth: Map<string, { consecutiveFailures: number;
cooloffUntil: number }>) which can grow unbounded; add a lastAccessed timestamp
to the value shape (e.g., { consecutiveFailures, cooloffUntil, lastAccessed })
and update it whenever you read/write an entry, then implement a periodic
cleanup task (e.g., in the same module or state initializer) that iterates
oracleHealth and deletes entries that have been healthy for a long time or not
accessed for a TTL (or whose lastAccessed is older than threshold);
alternatively provide an LRU-eviction wrapper around oracleHealth to cap
size—ensure all code that touches oracleHealth (lookups in the health-check
logic) updates lastAccessed so stale entries can be removed.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 18170229-1aa5-4166-a891-a826e54af0c7

📥 Commits

Reviewing files that changed from the base of the PR and between 4928590 and 907a90e.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (12)
  • src/core/modes/intra/simulation.ts
  • src/oracle/fetch.ts
  • src/oracle/index.ts
  • src/order/index.ts
  • src/order/quote.ts
  • src/order/types/index.ts
  • src/order/types/v3.ts
  • src/order/types/v4.ts
  • src/state/contracts.ts
  • src/state/index.ts
  • src/subgraph/query.ts
  • src/subgraph/types.ts

Comment on lines +73 to +81
export function isInCooloff(healthMap: OracleHealthMap, url: string): boolean {
const state = healthMap.get(url);
if (!state || state.cooloffUntil === 0) return false;
if (Date.now() >= state.cooloffUntil) {
state.cooloffUntil = 0;
return false;
}
return true;
}
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.

🧹 Nitpick | 🔵 Trivial

Side effect in read function: isInCooloff mutates state.

The function resets cooloffUntil to 0 when the cooloff period has expired (line 77). This side effect during a read operation can cause issues in concurrent scenarios and violates the principle of least surprise for a function named with is prefix (implying a pure predicate).

♻️ Suggested refactor to separate concerns
 export function isInCooloff(healthMap: OracleHealthMap, url: string): boolean {
     const state = healthMap.get(url);
     if (!state || state.cooloffUntil === 0) return false;
-    if (Date.now() >= state.cooloffUntil) {
-        state.cooloffUntil = 0;
-        return false;
-    }
-    return true;
+    return Date.now() < state.cooloffUntil;
 }
+
+export function clearExpiredCooloff(healthMap: OracleHealthMap, url: string): void {
+    const state = healthMap.get(url);
+    if (state && state.cooloffUntil !== 0 && Date.now() >= state.cooloffUntil) {
+        state.cooloffUntil = 0;
+    }
+}

Then call clearExpiredCooloff explicitly where needed, or let recordOracleSuccess handle the reset naturally on the next successful call.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function isInCooloff(healthMap: OracleHealthMap, url: string): boolean {
const state = healthMap.get(url);
if (!state || state.cooloffUntil === 0) return false;
if (Date.now() >= state.cooloffUntil) {
state.cooloffUntil = 0;
return false;
}
return true;
}
export function isInCooloff(healthMap: OracleHealthMap, url: string): boolean {
const state = healthMap.get(url);
if (!state || state.cooloffUntil === 0) return false;
return Date.now() < state.cooloffUntil;
}
export function clearExpiredCooloff(healthMap: OracleHealthMap, url: string): void {
const state = healthMap.get(url);
if (state && state.cooloffUntil !== 0 && Date.now() >= state.cooloffUntil) {
state.cooloffUntil = 0;
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/oracle/index.ts` around lines 73 - 81, isInCooloff currently mutates
OracleHealthMap by resetting state.cooloffUntil to 0 when expired; change it to
be a pure predicate that only reads state and returns true/false, remove the
assignment, and add a new helper clearExpiredCooloff(healthMap: OracleHealthMap,
url: string) that checks the same expiry and resets cooloffUntil when needed;
update callers (e.g., places that call isInCooloff) to call clearExpiredCooloff
where they previously relied on the side effect, or ensure recordOracleSuccess
handles the reset on success.

Comment on lines +172 to +176
export async function fetchSignedContext(
url: string,
request: OracleOrderRequest,
healthMap: OracleHealthMap,
): Promise<Result<any, string>> {
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.

🧹 Nitpick | 🔵 Trivial

Return type uses any — consider a stricter type.

fetchSignedContext returns Result<any, string>, but the validated response has a known shape matching SignedContextV1. Using any bypasses type safety and allows misuse at call sites.

♻️ Define and use a typed return
+export interface SignedContextV1Response {
+    signer: string;
+    context: string[];
+    signature: string;
+}
+
 export async function fetchSignedContext(
     url: string,
     request: OracleOrderRequest,
     healthMap: OracleHealthMap,
-): Promise<Result<any, string>> {
+): Promise<Result<SignedContextV1Response, string>> {

Then update the validation block to return the typed object:

return Result.ok(item as SignedContextV1Response);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/oracle/index.ts` around lines 172 - 176, fetchSignedContext currently
returns Result<any, string>; change its signature to return
Promise<Result<SignedContextV1Response, string>> and use the concrete
SignedContextV1/SignedContextV1Response types (alongside existing
OracleOrderRequest and OracleHealthMap) so callers get proper typing. In the
response-validation block, replace the generic Result.ok(...) with
Result.ok(item as SignedContextV1Response) (or construct a
SignedContextV1Response from item) and ensure any validation function references
SignedContextV1 to guarantee the shape before casting.

Comment on lines +197 to +203
try {
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/octet-stream" },
body,
signal: controller.signal,
});
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.

⚠️ Potential issue | 🟡 Minor

Consider validating the oracle URL to mitigate SSRF risks.

The url parameter comes from on-chain metadata (extractOracleUrl), but malicious orders could embed URLs targeting internal services. While the impact depends on deployment environment, consider:

  • Restricting to HTTPS only
  • Blocking private IP ranges (localhost, 10.x, 172.16-31.x, 192.168.x)
  • Using an allowlist of known oracle domains if applicable
🛡️ Example URL validation
function isAllowedOracleUrl(url: string): boolean {
    try {
        const parsed = new URL(url);
        if (parsed.protocol !== 'https:') return false;
        // Block private IPs - basic check
        const host = parsed.hostname;
        if (host === 'localhost' || host.startsWith('127.') || 
            host.startsWith('10.') || host.startsWith('192.168.')) {
            return false;
        }
        return true;
    } catch {
        return false;
    }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/oracle/index.ts` around lines 197 - 203, The fetch to the untrusted
oracle URL should be guarded to mitigate SSRF: add a URL validation step (e.g.,
an isAllowedOracleUrl helper) and call it before the try/fetch in the code
around the fetch(...) call in src/oracle/index.ts; ensure the validator enforces
https only, rejects hostnames resolving to private/local ranges (localhost,
127.*, 10.*, 172.16-31.*, 192.168.*) and/or checks against a configured
allowlist of trusted oracle domains, and if validation fails throw or return an
error instead of performing the POST to the untrusted URL.

Comment on lines +233 to +242
if (
typeof item !== "object" ||
item === null ||
typeof (item as any).signer !== "string" ||
!Array.isArray((item as any).context) ||
typeof (item as any).signature !== "string"
) {
recordOracleFailure(healthMap, url);
return Result.err("Oracle response is not a valid SignedContextV1");
}
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.

🧹 Nitpick | 🔵 Trivial

Validation could be stricter for security-critical data.

The validation only checks that signer and signature are strings, and context is an array. For oracle-signed context that will be used on-chain:

  • signer should be a valid Ethereum address (0x + 40 hex chars)
  • signature should be valid hex
  • context items should be valid bytes32 values

While the on-chain verifier will ultimately reject malformed data, early validation prevents wasted gas and provides clearer error messages.

🛡️ Optional stricter validation
+const isValidAddress = (s: string): boolean => /^0x[a-fA-F0-9]{40}$/.test(s);
+const isValidHex = (s: string): boolean => /^0x[a-fA-F0-9]*$/.test(s);
+const isValidBytes32 = (s: string): boolean => /^0x[a-fA-F0-9]{64}$/.test(s);
+
     // Validate shape of single SignedContextV1
     if (
         typeof item !== "object" ||
         item === null ||
-        typeof (item as any).signer !== "string" ||
-        !Array.isArray((item as any).context) ||
-        typeof (item as any).signature !== "string"
+        !isValidAddress((item as any).signer) ||
+        !Array.isArray((item as any).context) ||
+        !(item as any).context.every(isValidBytes32) ||
+        !isValidHex((item as any).signature)
     ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/oracle/index.ts` around lines 233 - 242, The current SignedContextV1
check in the validation block (before calling recordOracleFailure and returning
Result.err) is too permissive; update the validation in the same block to (1)
verify signer matches an Ethereum address regex (0x + 40 hex chars), (2) verify
signature is hex (0x-prefixed hex string of appropriate length, e.g., 130 chars
for 65-byte signature or accept generic 0x + even-length hex), and (3) verify
each element of context is a bytes32 hex string (0x + 64 hex chars); if any
check fails call recordOracleFailure(healthMap, url) and return Result.err with
a clear message. Use the existing symbols (recordOracleFailure, Result.err) and
keep validation deterministic and concise so malformed oracle responses are
rejected early.

Comment on lines +41 to +46
if (state) {
const oracleResult = await fetchOracleContext.call(state, orderDetails);
if (oracleResult.isErr()) {
console.warn("Failed to fetch oracle context:", oracleResult.error);
}
}
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.

🧹 Nitpick | 🔵 Trivial

Unnecessary oracle fetch call for V3 orders.

fetchOracleContext is called in quoteSingleOrderV3, but it immediately returns Result.ok(undefined) for non-V4 orders (see src/oracle/fetch.ts line 22). While harmless, this adds a function call overhead for every V3 quote.

Consider removing this block from quoteSingleOrderV3 since oracle context is V4-only:

♻️ Suggested change
 export async function quoteSingleOrderV3(
     orderDetails: Pair,
     viemClient: PublicClient,
     state?: SharedState,
     blockNumber?: bigint,
     gas?: bigint,
 ) {
-    if (state) {
-        const oracleResult = await fetchOracleContext.call(state, orderDetails);
-        if (oracleResult.isErr()) {
-            console.warn("Failed to fetch oracle context:", oracleResult.error);
-        }
-    }
-
     const { data } = await viemClient
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (state) {
const oracleResult = await fetchOracleContext.call(state, orderDetails);
if (oracleResult.isErr()) {
console.warn("Failed to fetch oracle context:", oracleResult.error);
}
}
export async function quoteSingleOrderV3(
orderDetails: Pair,
viemClient: PublicClient,
state?: SharedState,
blockNumber?: bigint,
gas?: bigint,
) {
const { data } = await viemClient
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/order/quote.ts` around lines 41 - 46, In quoteSingleOrderV3 remove the
unnecessary V4-only oracle fetch: delete the conditional block that calls
fetchOracleContext.call(state, orderDetails) and inspects oracleResult (the if
(state) { const oracleResult = await fetchOracleContext.call(state,
orderDetails); if (oracleResult.isErr()) { console.warn(...) } } block) because
fetchOracleContext returns Result.ok(undefined) for non-V4 orders; also clean up
any now-unused identifiers/imports (state, fetchOracleContext, oracleResult) in
that file.

Comment on lines +97 to 112
// In Rain V6, the "deployer" address from the rainlang registry may
// actually be the parser, which doesn't expose I_INTERPRETER/I_STORE.
// Fall back to using the dispair address itself for all three fields —
// the actual interpreter/store will be taken from the order's evaluable
// struct at execution time. This allows intra-orderbook clearing
// (which doesn't need the task deployer) to work without a "real"
// deployer address.
if (!interpreter || !store) {
console.warn(
`Could not read interpreter/store from dispair ${addresses.dispair} — ` +
`using fallback. Task bytecode generation will fail; set gasCoveragePercentage="0" ` +
`to skip bounty tasks.`,
);
interpreter = addresses.dispair;
store = addresses.dispair;
}
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.

⚠️ Potential issue | 🟠 Major

Scope the deployer-as-interpreter/store fallback to V6 only.

Line 104 applies fallback for all versions, but the rationale is V6-specific. For v4/v5, this can silently convert a bad read/config into invalid interpreter/store addresses and defer failure to simulation/execution.

💡 Proposed fix
-    if (!interpreter || !store) {
+    if (!interpreter || !store) {
+        if (version !== "v6") {
+            return undefined;
+        }
         console.warn(
             `Could not read interpreter/store from dispair ${addresses.dispair} — ` +
                 `using fallback. Task bytecode generation will fail; set gasCoveragePercentage="0" ` +
                 `to skip bounty tasks.`,
         );
         interpreter = addresses.dispair;
         store = addresses.dispair;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// In Rain V6, the "deployer" address from the rainlang registry may
// actually be the parser, which doesn't expose I_INTERPRETER/I_STORE.
// Fall back to using the dispair address itself for all three fields —
// the actual interpreter/store will be taken from the order's evaluable
// struct at execution time. This allows intra-orderbook clearing
// (which doesn't need the task deployer) to work without a "real"
// deployer address.
if (!interpreter || !store) {
console.warn(
`Could not read interpreter/store from dispair ${addresses.dispair} — ` +
`using fallback. Task bytecode generation will fail; set gasCoveragePercentage="0" ` +
`to skip bounty tasks.`,
);
interpreter = addresses.dispair;
store = addresses.dispair;
}
// In Rain V6, the "deployer" address from the rainlang registry may
// actually be the parser, which doesn't expose I_INTERPRETER/I_STORE.
// Fall back to using the dispair address itself for all three fields —
// the actual interpreter/store will be taken from the order's evaluable
// struct at execution time. This allows intra-orderbook clearing
// (which doesn't need the task deployer) to work without a "real"
// deployer address.
if (!interpreter || !store) {
if (version !== "v6") {
return undefined;
}
console.warn(
`Could not read interpreter/store from dispair ${addresses.dispair} — ` +
`using fallback. Task bytecode generation will fail; set gasCoveragePercentage="0" ` +
`to skip bounty tasks.`,
);
interpreter = addresses.dispair;
store = addresses.dispair;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/state/contracts.ts` around lines 97 - 112, The fallback that assigns
addresses.dispair to interpreter and store should only run for Rain V6; update
the conditional around the existing block that checks !interpreter || !store to
also verify the runtime registry/version equals 6 (e.g., check a variable like
rainVersion === 6 or registryVersion === 6), so for v4/v5 the original read
failure surfaces instead of silently falling back. Keep the same warning text
and assignments to addresses.dispair, interpreter, and store inside the
now-scoped V6-only branch.

Comment on lines +227 to +228
/** Oracle endpoint health tracking for cooloff */
oracleHealth: Map<string, { consecutiveFailures: number; cooloffUntil: number }> = new Map();
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.

🧹 Nitpick | 🔵 Trivial

Consider adding eviction for stale oracle health entries.

The oracleHealth map will grow unboundedly as new oracle URLs are encountered. Over long runtimes with many different oracle endpoints, this could lead to memory growth.

Consider periodically evicting entries that have been healthy for extended periods or haven't been accessed recently.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/state/index.ts` around lines 227 - 228, The oracleHealth Map currently
stores entries indefinitely (oracleHealth: Map<string, { consecutiveFailures:
number; cooloffUntil: number }>) which can grow unbounded; add a lastAccessed
timestamp to the value shape (e.g., { consecutiveFailures, cooloffUntil,
lastAccessed }) and update it whenever you read/write an entry, then implement a
periodic cleanup task (e.g., in the same module or state initializer) that
iterates oracleHealth and deletes entries that have been healthy for a long time
or not accessed for a TTL (or whose lastAccessed is older than threshold);
alternatively provide an LRU-eviction wrapper around oracleHealth to cap
size—ensure all code that touches oracleHealth (lookups in the health-check
logic) updates lastAccessed so stale entries can be removed.

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