Skip to content

feat: Implement validation for cross-chain output oracles #307

Open
nahimterrazas wants to merge 3 commits intomainfrom
h03-fix-plan
Open

feat: Implement validation for cross-chain output oracles #307
nahimterrazas wants to merge 3 commits intomainfrom
h03-fix-plan

Conversation

@nahimterrazas
Copy link
Copy Markdown
Collaborator

@nahimterrazas nahimterrazas commented Mar 4, 2026

Summary by CodeRabbit

  • Bug Fixes
    • Strengthened cross-chain validation to reject all-zero oracles and oracles with non-zero upper bytes; now requires exact destination address match and reports output index and destination chain in errors.
  • Tests
    • Expanded test coverage for multi-output/multi-chain flows, adding cases for zero and dirty-upper-byte oracle values and index-aware assertions.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 4, 2026

📝 Walkthrough

Walkthrough

Added defense-in-depth oracle validations for EIP-7683 cross-chain outputs: reject all-zero oracles, reject non-zero upper 12 bytes, and require exact lower-20-byte oracle address matches supported destination routes across fill, claim, and validate flows; tests and fixtures updated accordingly.

Changes

Cohort / File(s) Summary
Core validation
crates/solver-order/src/implementations/standards/_7683.rs
Added index-aware checks in validate_order and tightened validations in generate_fill_transaction and generate_claim_transaction: reject oracle == [0u8;32], reject non-zero oracle[..12], and require exact match of oracle[12..32] to supported route oracle address.
Tests & fixtures
crates/solver-order/tests/*, crates/solver-order/src/test_utils/*, crates/solver-order/.../fixtures/*
Canonicalized test oracle bytes32 (zeroed upper 12 bytes, lower 20 bytes = address); added tests asserting rejection of zero or dirty-upper-byte cross-chain oracles in fill/claim flows and updated validate_order tests and builders.
Test adjustments for multi-output
crates/solver-order/tests/.../arbitrum_multi_output_*
Adjusted multi-output (Arbitrum) test setup to use clean lower-20-byte oracle encodings and updated incompatible-oracle scenarios to be “incompatible but clean.”

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • shahnami

Poem

🐰🔎 I hop through bytes both low and high,
I sniff the upper bits and bid zeros goodbye.
Lower twenty’s tidy, exact as a charm,
Cross-chain orders checked, no messy alarm.
🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is entirely empty, missing all required template sections including Summary, Testing Process, and the Checklist. Add a comprehensive description following the template with: a summary of changes, explanation of the testing process, and completion of the required checklist items.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: implementing validation for cross-chain output oracles, which is the core focus of the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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 h03-fix-plan
📝 Coding Plan
  • Generate coding plan for human review comments

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

@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: 1

🧹 Nitpick comments (1)
crates/solver-order/src/implementations/standards/_7683.rs (1)

655-659: Use output_index in all output-route error branches for consistent diagnostics.

You already enumerate outputs; including index in unsupported-route/incompatible-oracle errors will make multi-output debugging much easier.

Also applies to: 686-690

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

In `@crates/solver-order/src/implementations/standards/_7683.rs` around lines 655
- 659, The error messages for unsupported routes and incompatible-oracle
branches currently reference origin_chain, dest_chain and input_oracle but omit
the output index; update the two error branches that return
OrderError::ValidationFailed (the branch checking
supported_destinations.contains(&dest_chain) and the similar branch around lines
686-690) to include the output_index variable in the formatted message (e.g.
"output {output_index}: route from chain {origin_chain} to chain {dest_chain}
..." and similarly for the incompatible-oracle message) so diagnostics
consistently show which output failed; keep the existing context (input_oracle,
origin_chain, dest_chain) and insert output_index into the format strings.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/solver-order/src/implementations/standards/_7683.rs`:
- Around line 317-329: The current validation only checks that cross-chain
output oracles are non-zero and have zero upper bytes but does not verify the
oracle is actually supported for the destination chain; add a route-membership
check after the existing canonical checks to ensure the oracle corresponds to a
known/registered route for output.chain_id (e.g. call a routing lookup like
is_oracle_valid_for_chain(output.oracle, output.chain_id) or consult the
RouteRegistry/AllowedOracles for that chain), and return
OrderError::ValidationFailed with a clear message ("unsupported oracle for
destination chain {chain_id}") if the oracle is not a member; apply the same
additional check in the other analogous block (the fill/claim validation at the
other location) so both places enforce route membership.

---

Nitpick comments:
In `@crates/solver-order/src/implementations/standards/_7683.rs`:
- Around line 655-659: The error messages for unsupported routes and
incompatible-oracle branches currently reference origin_chain, dest_chain and
input_oracle but omit the output index; update the two error branches that
return OrderError::ValidationFailed (the branch checking
supported_destinations.contains(&dest_chain) and the similar branch around lines
686-690) to include the output_index variable in the formatted message (e.g.
"output {output_index}: route from chain {origin_chain} to chain {dest_chain}
..." and similarly for the incompatible-oracle message) so diagnostics
consistently show which output failed; keep the existing context (input_oracle,
origin_chain, dest_chain) and insert output_index into the format strings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0136949f-09f7-4eff-9696-7f23488c105c

📥 Commits

Reviewing files that changed from the base of the PR and between f8543a1 and 44953ed.

📒 Files selected for processing (1)
  • crates/solver-order/src/implementations/standards/_7683.rs

Comment on lines +317 to +329
// Defense in depth: cross-chain outputs must have a canonical non-zero oracle.
if output.oracle == [0u8; 32] {
return Err(OrderError::ValidationFailed(format!(
"Zero oracle not allowed for cross-chain output on chain {}",
output.chain_id.to::<u64>()
)));
}
if output.oracle[..12].iter().any(|&byte| byte != 0) {
return Err(OrderError::ValidationFailed(format!(
"Output oracle has dirty upper bytes for cross-chain output on chain {}",
output.chain_id.to::<u64>()
)));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add route-membership validation in fill/claim (not just zero/dirty-byte checks).

These blocks validate canonical encoding, but they still accept a canonical unsupported output oracle for a destination chain. That leaves a defense-in-depth gap in transaction generation paths.

Proposed patch direction
@@ async fn generate_fill_transaction(...)
-        if output.oracle[..12].iter().any(|&byte| byte != 0) {
+        if output.oracle[..12].iter().any(|&byte| byte != 0) {
             return Err(OrderError::ValidationFailed(format!(
                 "Output oracle has dirty upper bytes for cross-chain output on chain {}",
                 output.chain_id.to::<u64>()
             )));
         }
+        let dest_chain_id = output.chain_id.to::<u64>();
+        let output_oracle_address = &output.oracle[12..32];
+        let has_supported_output_oracle = self.oracle_routes.supported_routes.values().any(|outs| {
+            outs.iter().any(|supported| {
+                supported.chain_id == dest_chain_id
+                    && supported.oracle.0.as_slice() == output_oracle_address
+            })
+        });
+        if !has_supported_output_oracle {
+            return Err(OrderError::ValidationFailed(format!(
+                "Output oracle is not supported for destination chain {}",
+                dest_chain_id
+            )));
+        }

@@ async fn generate_claim_transaction(...)
                 if output.oracle[..12].iter().any(|&byte| byte != 0) {
                     return Err(OrderError::ValidationFailed(format!(
                         "Output oracle has dirty upper bytes for cross-chain output on chain {}",
                         output.chain_id.to::<u64>()
                     )));
                 }
+                let dest_chain_id = output.chain_id.to::<u64>();
+                let output_oracle_address = &output.oracle[12..32];
+                let has_supported_output_oracle = self.oracle_routes.supported_routes.values().any(|outs| {
+                    outs.iter().any(|supported| {
+                        supported.chain_id == dest_chain_id
+                            && supported.oracle.0.as_slice() == output_oracle_address
+                    })
+                });
+                if !has_supported_output_oracle {
+                    return Err(OrderError::ValidationFailed(format!(
+                        "Output oracle is not supported for destination chain {}",
+                        dest_chain_id
+                    )));
+                }
             }

Also applies to: 444-458

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

In `@crates/solver-order/src/implementations/standards/_7683.rs` around lines 317
- 329, The current validation only checks that cross-chain output oracles are
non-zero and have zero upper bytes but does not verify the oracle is actually
supported for the destination chain; add a route-membership check after the
existing canonical checks to ensure the oracle corresponds to a known/registered
route for output.chain_id (e.g. call a routing lookup like
is_oracle_valid_for_chain(output.oracle, output.chain_id) or consult the
RouteRegistry/AllowedOracles for that chain), and return
OrderError::ValidationFailed with a clear message ("unsupported oracle for
destination chain {chain_id}") if the oracle is not a member; apply the same
additional check in the other analogous block (the fill/claim validation at the
other location) so both places enforce route membership.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 4, 2026

Codecov Report

❌ Patch coverage is 96.29630% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...olver-order/src/implementations/standards/_7683.rs 96.2% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

@nahimterrazas nahimterrazas self-assigned this Mar 5, 2026
Copy link
Copy Markdown

@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.

♻️ Duplicate comments (1)
crates/solver-order/src/implementations/standards/_7683.rs (1)

317-329: ⚠️ Potential issue | 🟠 Major

Enforce supported-route membership in fill/claim, not only canonical encoding.

Line 317 and Line 444 only validate canonical bytes32 shape (non-zero + clean upper 12 bytes). A clean but unsupported oracle for the destination chain still passes these tx-generation paths, so the defense-in-depth gap remains.

🔧 Proposed patch direction
@@ async fn generate_fill_transaction(
-        // Defense in depth: cross-chain outputs must have a canonical non-zero oracle.
+        // Defense in depth: cross-chain outputs must have a canonical non-zero oracle.
+        let dest_chain_id = output.chain_id.to::<u64>();
         if output.oracle == [0u8; 32] {
             return Err(OrderError::ValidationFailed(format!(
                 "Zero oracle not allowed for cross-chain output on chain {}",
-                output.chain_id.to::<u64>()
+                dest_chain_id
             )));
         }
         if output.oracle[..12].iter().any(|&byte| byte != 0) {
             return Err(OrderError::ValidationFailed(format!(
                 "Output oracle has dirty upper bytes for cross-chain output on chain {}",
-                output.chain_id.to::<u64>()
+                dest_chain_id
             )));
         }
+        let output_oracle_address = &output.oracle[12..32];
+        let has_supported_output_oracle =
+            self.oracle_routes.supported_routes.values().any(|outs| {
+                outs.iter().any(|supported| {
+                    supported.chain_id == dest_chain_id
+                        && supported.oracle.0.as_slice() == output_oracle_address
+                })
+            });
+        if !has_supported_output_oracle {
+            return Err(OrderError::ValidationFailed(format!(
+                "Output oracle is not supported for destination chain {}",
+                dest_chain_id
+            )));
+        }
@@
-        let dest_chain_id = output.chain_id.to::<u64>();
+        // dest_chain_id already derived above

@@ async fn generate_claim_transaction(
                 if output.oracle[..12].iter().any(|&byte| byte != 0) {
                     return Err(OrderError::ValidationFailed(format!(
                         "Output oracle has dirty upper bytes for cross-chain output on chain {}",
                         output.chain_id.to::<u64>()
                     )));
                 }
+                let dest_chain_id = output.chain_id.to::<u64>();
+                let output_oracle_address = &output.oracle[12..32];
+                let has_supported_output_oracle =
+                    self.oracle_routes.supported_routes.values().any(|outs| {
+                        outs.iter().any(|supported| {
+                            supported.chain_id == dest_chain_id
+                                && supported.oracle.0.as_slice() == output_oracle_address
+                        })
+                    });
+                if !has_supported_output_oracle {
+                    return Err(OrderError::ValidationFailed(format!(
+                        "Output oracle is not supported for destination chain {}",
+                        dest_chain_id
+                    )));
+                }
             }

Also applies to: 444-458

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

In `@crates/solver-order/src/implementations/standards/_7683.rs` around lines 317
- 329, The current checks only assert canonical oracle shape (output.oracle not
all-zero and upper 12 bytes zero) but do not ensure the oracle is a supported
destination route; update both validation sites that inspect output.oracle (the
blocks returning OrderError::ValidationFailed for zero oracle and dirty upper
bytes) to also verify membership in the supported-route registry (e.g., call the
route table or SupportedRoutes::contains/get for the destination chain_id and/or
oracle bytes). If the oracle is not present in the supported routes, return
OrderError::ValidationFailed with a clear message including output.chain_id and
the oracle identifier; apply this same membership check at the second validation
site handling cross-chain outputs so unsupported-but-canonical oracles are
rejected.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@crates/solver-order/src/implementations/standards/_7683.rs`:
- Around line 317-329: The current checks only assert canonical oracle shape
(output.oracle not all-zero and upper 12 bytes zero) but do not ensure the
oracle is a supported destination route; update both validation sites that
inspect output.oracle (the blocks returning OrderError::ValidationFailed for
zero oracle and dirty upper bytes) to also verify membership in the
supported-route registry (e.g., call the route table or
SupportedRoutes::contains/get for the destination chain_id and/or oracle bytes).
If the oracle is not present in the supported routes, return
OrderError::ValidationFailed with a clear message including output.chain_id and
the oracle identifier; apply this same membership check at the second validation
site handling cross-chain outputs so unsupported-but-canonical oracles are
rejected.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a9876b0c-2d7e-4ad9-8e12-b77b00c890ae

📥 Commits

Reviewing files that changed from the base of the PR and between 44953ed and cd55098.

📒 Files selected for processing (1)
  • crates/solver-order/src/implementations/standards/_7683.rs

Copy link
Copy Markdown

@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.

♻️ Duplicate comments (1)
crates/solver-order/src/implementations/standards/_7683.rs (1)

321-333: ⚠️ Potential issue | 🟠 Major

Still missing supported-route validation in fill/claim.

These checks harden the bytes32 encoding, but they still allow a clean oracle whose lower 20 bytes are not registered for output.chain_id. validate_order() already rejects that at Lines 679-686, and it matters here because validate_and_create_order() can preserve parsable intent_data instead of rebuilding order.data.outputs from the validated StandardOrder. Please reuse the same route-membership check here, ideally through a shared helper so these paths stay in sync.

Patch direction
-        if output.oracle == [0u8; 32] {
-            return Err(...);
-        }
-        if output.oracle[..12].iter().any(|&byte| byte != 0) {
-            return Err(...);
-        }
+        self.validate_cross_chain_output_oracle(output.chain_id.to::<u64>(), &output.oracle)?;

Have validate_cross_chain_output_oracle(...) include the same zero-oracle, dirty-upper-byte, and supported-route checks used by validate_order().

Also applies to: 448-462

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

In `@crates/solver-order/src/implementations/standards/_7683.rs` around lines 321
- 333, The cross-chain oracle check must also verify that the oracle (lower 20
bytes) is a supported route for output.chain_id as validate_order() does; modify
validate_cross_chain_output_oracle(...) to perform the same three checks
(non-zero oracle, clean upper 12 bytes, and membership of the lower-20-byte
address in the supported routes for the given chain_id) so
validate_and_create_order() and validate_order() share identical validation
logic; reuse or extract the route-membership logic into a helper (called from
validate_cross_chain_output_oracle and validate_order) so the behavior for
StandardOrder route validation stays in sync.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@crates/solver-order/src/implementations/standards/_7683.rs`:
- Around line 321-333: The cross-chain oracle check must also verify that the
oracle (lower 20 bytes) is a supported route for output.chain_id as
validate_order() does; modify validate_cross_chain_output_oracle(...) to perform
the same three checks (non-zero oracle, clean upper 12 bytes, and membership of
the lower-20-byte address in the supported routes for the given chain_id) so
validate_and_create_order() and validate_order() share identical validation
logic; reuse or extract the route-membership logic into a helper (called from
validate_cross_chain_output_oracle and validate_order) so the behavior for
StandardOrder route validation stays in sync.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e1acb734-9b23-4c84-b352-eb10d3f43fd8

📥 Commits

Reviewing files that changed from the base of the PR and between cd55098 and 0b421a6.

📒 Files selected for processing (1)
  • crates/solver-order/src/implementations/standards/_7683.rs

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.

3 participants