Skip to content

Add native token fill support - #414

Merged
nahimterrazas merged 5 commits into
mainfrom
native-token-fill-support
Jul 9, 2026
Merged

Add native token fill support#414
nahimterrazas merged 5 commits into
mainfrom
native-token-fill-support

Conversation

@nahimterrazas

@nahimterrazas nahimterrazas commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Support native-token output fills by attaching msg.value in runtime and quote-time fill transaction builders.
  • Reject native tokens as inputs while allowing native outputs, including the Compact low-20-byte guard.
  • Route native balances, approvals, admin balance display, and destination balance checks through native-aware paths.
  • Price gas and native outputs by the chain native symbol with config-first resolution and known-chain fallback.
  • Add native metadata to shipped config examples and cover ERC20 input to native output fill/settle in an ignored e2e test.

Validation

  • cargo fmt --all -- --check
  • cargo clippy --all-features --all-targets -- -D warnings --allow deprecated
  • cargo check --all-targets
  • cargo test -p solver-config
  • cargo test -p solver-order
  • cargo test -p solver-core
  • cargo test -p solver-service
  • OIF_CONTRACTS_PATH=/Users/nahimdhaney/openzeppelin/oif-solver/oif-contracts cargo test -p solver-e2e-tests --test happy_e2e_native_fill_settle -- --ignored --test-threads=1 --nocapture

Summary by CodeRabbit

  • New Features

    • Added native ETH handling across quoting, balance reporting, order execution, and native fill settlement (including correct native gas/asset valuation and transaction value behavior).
  • Bug Fixes

    • Native assets are no longer treated like ERC-20s for approvals/balance checks; native inputs are rejected during quote validation and EIP-7683 order validation.
    • Improved overflow-safe native/USD conversions and tightened fail-closed behavior when native token metadata isn’t configured.
  • Chores

    • Updated example/demo/test configurations to include native ETH (and expanded mock pricing mappings like POL/BNB/AVAX); added native ETH end-to-end coverage.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: af94fe97-01e7-4d24-802d-5822bcdd752c

📥 Commits

Reviewing files that changed from the base of the PR and between 7703f3e and 050468e.

📒 Files selected for processing (1)
  • .github/workflows/e2e.yaml

📝 Walkthrough

Walkthrough

This PR adds native-token detection and native ETH handling across configs, token management, cost/profit, order validation, strategy execution, admin and quote APIs, plus an e2e native fill/settle test. It also extends default token name mappings for POL, BNB, and AVAX.

Changes

Native ETH token support

Layer / File(s) Summary
Native token utilities
crates/solver-types/src/utils/conversion.rs, crates/solver-types/src/utils/mod.rs, crates/solver-types/src/lib.rs
Adds is_native_token_id and is_native_address helpers and re-exports them.
Config files declare ETH
config/demo.json, config/example-broadcaster.json, config/example-hyperlane.json, config/non-seeded-networks-example.json, config/seed-overrides-kms-example.json, config/seed-overrides-mainnet.json, config/seed-overrides-testnet.json
Adds ETH token entries to network token lists.
Common token filtering
crates/solver-core/src/engine/context.rs
Filters native-address tokens from common-token lists and updates the test fixture.
TokenManager native handling
crates/solver-core/src/engine/token_manager.rs
Skips approvals for native tokens, adds a native approval error, routes native balances without a token address, and updates native detection in balance/withdraw paths.
Native gas and fill pricing
crates/solver-core/src/engine/cost_profit.rs
Adds native gas fallback resolution, updates fee conversions, sets fill transaction value for native outputs, hardens decimal conversion, and expands valuation tests.
Order validation and native fill value
crates/solver-order/src/implementations/standards/_7683.rs
Sets fill transaction value for native outputs and rejects orders with native inputs.
Native balance keying
crates/solver-order/src/implementations/strategies/simple.rs
Uses a native balance key for native outputs and updates execution messaging.
Admin balances use configured native metadata
crates/solver-service/src/apis/admin.rs
Builds native balance rows from configured native token metadata and skips native tokens in the ERC-20 loop.
Quote validation for native assets
crates/solver-service/src/apis/quote/mod.rs, crates/solver-service/src/apis/quote/validation.rs
Rejects native inputs, switches destination balance checks to native-aware lookup, and updates quote fixtures and tests.
Intent handler native token wiring
crates/solver-core/src/handlers/intent.rs
Adds a native ETH token to the intent-handler test setup.
E2E native fill and settle coverage
crates/solver-e2e-tests/src/lib.rs, crates/solver-e2e-tests/tests/happy_e2e_native_fill_settle.rs, .github/workflows/e2e.yaml
Adds ETH to the e2e seed override helper, adds a native ETH output test, and registers it in the e2e workflow.

Default token name mappings

Layer / File(s) Summary
Additional default mappings
crates/solver-pricing/src/lib.rs
Adds POL, BNB, and AVAX to the default symbol-to-name mapping table and updates the test.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
    participant OrderValidator as "_7683 validate_order"
    participant Strategy as "SimpleStrategy"
    participant TokenManager as "TokenManager"
    participant CostProfit as "cost_profit"
    participant AdminAPI as "admin balances"
    participant QuoteValidation as "quote validation"

    OrderValidator->>OrderValidator: reject native inputs
    Strategy->>TokenManager: check native balance with chain_id and None
    TokenManager-->>Strategy: native balance
    Strategy->>Strategy: execute using native balance key
    CostProfit->>CostProfit: convert native gas and fees to USD
    CostProfit->>CostProfit: set fill tx value for native output
    QuoteValidation->>TokenManager: native-compatible destination balance lookup
    AdminAPI->>AdminAPI: build native balance row from configured metadata
Loading

Possibly related PRs

Suggested reviewers: shahnami, tirumerla, pepebndc

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding native token fill support.
Description check ✅ Passed The description covers Summary and testing, but it omits the template's Checklist section and related issue reference.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch native-token-fill-support

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.

@nahimterrazas
nahimterrazas marked this pull request as ready for review July 6, 2026 19:04
@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
crates/solver-types/src/utils/conversion.rs (1)

30-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Misleading doc comment on is_native_address.

The comment says "True iff a solver address is exactly the 20-byte zero address," but the function is used generically for any token/config address (e.g. detecting native tokens in TokenManager, context.rs, admin balances), not specifically the solver's own address.

📝 Suggested doc fix
-/// True iff a solver address is exactly the 20-byte zero address.
+/// True iff the given address is exactly the 20-byte zero address (native token sentinel).
 pub fn is_native_address(addr: &Address) -> bool {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/solver-types/src/utils/conversion.rs` around lines 30 - 33, The doc
comment for is_native_address is misleading because the helper is used as a
generic zero-address check for token/config addresses, not just a solver
address. Update the comment on is_native_address in conversion.rs to describe it
as checking whether an Address is the 20-byte zero address and note its generic
native-token/native-address usage, so callers like TokenManager and context.rs
are accurately documented.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/solver-types/src/utils/conversion.rs`:
- Around line 30-33: The doc comment for is_native_address is misleading because
the helper is used as a generic zero-address check for token/config addresses,
not just a solver address. Update the comment on is_native_address in
conversion.rs to describe it as checking whether an Address is the 20-byte zero
address and note its generic native-token/native-address usage, so callers like
TokenManager and context.rs are accurately documented.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 94a44ca3-bfbb-4a11-a894-225f96e5dc01

📥 Commits

Reviewing files that changed from the base of the PR and between afb420e and 3bd93c0.

📒 Files selected for processing (22)
  • config/demo.json
  • config/example-broadcaster.json
  • config/example-hyperlane.json
  • config/non-seeded-networks-example.json
  • config/seed-overrides-kms-example.json
  • config/seed-overrides-mainnet.json
  • config/seed-overrides-testnet.json
  • crates/solver-core/src/engine/context.rs
  • crates/solver-core/src/engine/cost_profit.rs
  • crates/solver-core/src/engine/token_manager.rs
  • crates/solver-core/src/handlers/intent.rs
  • crates/solver-e2e-tests/src/lib.rs
  • crates/solver-e2e-tests/tests/happy_e2e_native_fill_settle.rs
  • crates/solver-order/src/implementations/standards/_7683.rs
  • crates/solver-order/src/implementations/strategies/simple.rs
  • crates/solver-pricing/src/lib.rs
  • crates/solver-service/src/apis/admin.rs
  • crates/solver-service/src/apis/quote/mod.rs
  • crates/solver-service/src/apis/quote/validation.rs
  • crates/solver-types/src/lib.rs
  • crates/solver-types/src/utils/conversion.rs
  • crates/solver-types/src/utils/mod.rs

@pepebndc

pepebndc commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Native output + Dutch-auction: msg.value may under-fund the fill

Verified the contract-side assumption against oif-contractsOutputSettlerBase._fill() handles native outputs correctly (clean if (token == 0) sendValue else safeTransferFrom branch, fill/fillOrderOutputs are payable, and _refundNativeExcess returns any over-sent value). No double-pull risk. 👍

One edge to confirm, though: the solver attaches msg.value = output.amount (crates/solver-order/src/implementations/standards/_7683.rs:393-399), but the contract pays out _resolveOutput()'s returned amount, which is not always output.amount:

  • LIMIT_ORDER / EXCLUSIVE_LIMIT_ORDER → resolved amount == output.amount (works exactly).
  • DUTCH_AUCTION / EXCLUSIVE_DUTCH_AUCTION → resolved amount is output.amount + slope · timeDiff, i.e. greater than output.amount (OutputSettlerSimple._resolveOutput / _dutchAuctionPrice).

For a native Dutch-auction output, the solver would attach only output.amount, the contract would attempt Address.sendValue(recipient, resolvedAmount) with a higher amount than it holds, and the fill would revert (insufficient balance). It fails closed — no fund loss — but native Dutch-auction fills would silently never succeed.

Question: is native + Dutch-auction an intended combination? If so, the msg.value for native outputs should be the resolved auction price rather than the base output.amount. The current e2e test only covers the limit-order path, so this wouldn't surface in CI.

@pepebndc

pepebndc commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Native pricing fallback is asymmetric: gas has it, output/input valuation doesn't

Verified in crates/solver-core/src/engine/cost_profit.rs. The known-chain native-symbol fallback is only wired into the gas valuation path, not the output/input valuation path:

  • Gasnative_base_units_to_usd (line ~2870) resolves the native symbol config-first, and on TokenManagerError::TokenNotSupported falls back to native_symbol_fallback(chain_id) (lines ~2895–2901). So gas pricing works on any well-known chain even without an explicit zero-address TokenConfig.
  • Output/input valuecalculate_inputs_usd_value (lines ~3145–3148) and calculate_outputs_usd_value (lines ~3182–3185) call get_token_info(chain_id, &token_address).await? directly. For a native output (zero-address asset) on a chain with no explicit zero-address TokenConfig, get_token_info returns TokenNotSupported, which propagates via ? and hard-errors — the native_symbol_fallback table is never consulted here.

Net effect is safe (fails closed — the order errors out rather than being mispriced), so this isn't a fund-safety bug. But it means the "config-first with known-chain fallback" behavior applies to gas only: to actually serve a native output, an operator must add a zero-address TokenConfig on that chain even if it's a well-known one the fallback table already covers.

Suggestion: either route native amounts in calculate_{inputs,outputs}_usd_value through native_base_units_to_usd (or an equivalent fallback) for symmetry, or document explicitly that a zero-address TokenConfig is required per-chain to serve native outputs. A regression test for "native output on a chain with no zero-address config" would pin whichever behavior is intended.

@pepebndc

pepebndc commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Non-18-decimal native tokens are not cleanly supported (gas leg couples to output decimals)

Traced the decimals handling across the changed paths. This PR effectively assumes an 18-decimal native token (all shipped configs, tests, and the fallback table assume 18). A chain whose native token isn't 18 decimals isn't safely supported. Details:

Works regardless of decimals:

  • The fill itself — msg.value = output.amount (crates/solver-order/src/implementations/standards/_7683.rs:393-399) attaches the raw base-unit amount with no decimals math.

Config-driven, honors non-18 if a zero-address TokenConfig is registered:

  • Output/input USD valuation — convert_raw_token_to_usd (crates/solver-core/src/engine/cost_profit.rs:3110-3116) divides the raw amount by 10^decimals from get_token_info.
  • Admin balance display — admin.rs (~1942-1945) uses token.decimals, defaulting to 18.

Assumes 18:

  • The fallback table native_symbol_fallback (cost_profit.rs:349-367) only lists chains documented as 18-decimal and its caller substitutes 18u8 unconditionally (comment at line ~347: "All listed chains use an 18-decimal native token, so callers assume 18").

The actual problem — the gas leg couples to the output-token decimals:
native_base_units_to_usd feeds the raw wei gas cost through the same configured decimals. On EVM, gas is always denominated in 1e18 wei regardless of a token's nominal decimals. So if an operator sets the native decimals to (say) 6 to match a genuinely 6-decimal native token, output valuation becomes correct but the gas leg is mispriced by 10^(18−6). The single decimals field is doing double duty — "decimals of the output amount" and "wei-per-native-unit for gas" — and those only coincide at 18.

Separately, the 10_i64.pow(decimals) overflow (raised elsewhere) means anything >= 19 breaks regardless, so 18 is effectively the only fully-safe value.

Suggestion: if 18-decimal-only is the intended scope, assert/validate it (reject a zero-address TokenConfig with decimals != 18, or at least document the assumption). If non-18 native chains are in scope, the gas-cost conversion needs to treat gas as wei (1e18) independently of the output token's nominal decimals.

@nahimterrazas

Copy link
Copy Markdown
Collaborator Author

@pepebndc pushed a follow-up. native output now errors clearly when the chain has no native token config (kept the gas fallback, it has to price every order incl erc20-only). documented the 18-dec native assumption + added a debug_assert, and fixed the i64 pow overflow (there was a second unguarded spot in convert_usd_to_token_amount too, both use checked i128 now). left the simple.rs check as is since the preflight already gates gas+value, just added a comment.

re dutch native: we reject dutch/exclusive outputs at validation, so it never reaches the fill builder. added a test to lock that.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

Inline comments:
In `@crates/solver-core/src/engine/cost_profit.rs`:
- Around line 3194-3196: The native valuation error messages in
CostProfitError::Calculation are exceeding Rust’s 100-column limit. Wrap the
long strings in cost_profit.rs by splitting the message across adjacent string
literals or formatting the format! call over multiple lines, and apply the same
fix to the other native valuation error block referenced in the same area so the
Rust formatting stays within rustfmt rules.
- Around line 2929-2934: The native gas pricing path in cost_profit.rs only uses
debug_assert_eq! to guard the 18-decimal assumption, so release builds can still
misprice when TokenConfig.decimals is not 18. Update the native gas conversion
logic around Self::convert_raw_token_to_usd to explicitly fail closed at runtime
for non-18 decimal native tokens (for example by returning an error or otherwise
aborting pricing for that config) instead of relying on debug assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 49e2dd28-0e9f-41c5-aeca-5df7dfaa790b

📥 Commits

Reviewing files that changed from the base of the PR and between 3bd93c0 and 7d549cd.

📒 Files selected for processing (2)
  • crates/solver-core/src/engine/cost_profit.rs
  • crates/solver-order/src/implementations/strategies/simple.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/solver-order/src/implementations/strategies/simple.rs

Comment thread crates/solver-core/src/engine/cost_profit.rs Outdated
Comment thread crates/solver-core/src/engine/cost_profit.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
crates/solver-core/src/engine/cost_profit.rs (1)

3148-3153: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: unify with the checked/try_ variant used in convert_usd_to_token_amount.

Here the > 28 guard makes 10_i128.pow(...) and from_i128_with_scale safe, but the sibling path at Line 3324 uses checked_pow + try_from_i128_with_scale to fail cleanly. Aligning both on the non-panicking variants would make the invariant self-documenting and robust to future edits that relax the guard. No behavioral change today.

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

In `@crates/solver-core/src/engine/cost_profit.rs` around lines 3148 - 3153, The
power/divisor construction in the token amount conversion path currently relies
on the `decimals <= 28` guard, but the sibling `convert_usd_to_token_amount`
uses checked/try-based APIs for safer failure behavior. Update the
`raw_amount_decimal / divisor` logic to use the same `checked_pow` and
`try_from_i128_with_scale` pattern as `convert_usd_to_token_amount`, so the
`cost_profit` conversion path stays self-documenting and remains robust if the
guard changes later.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/solver-core/src/engine/cost_profit.rs`:
- Around line 3148-3153: The power/divisor construction in the token amount
conversion path currently relies on the `decimals <= 28` guard, but the sibling
`convert_usd_to_token_amount` uses checked/try-based APIs for safer failure
behavior. Update the `raw_amount_decimal / divisor` logic to use the same
`checked_pow` and `try_from_i128_with_scale` pattern as
`convert_usd_to_token_amount`, so the `cost_profit` conversion path stays
self-documenting and remains robust if the guard changes later.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 29ec745d-9bba-42a0-a316-e9deecb8f0dd

📥 Commits

Reviewing files that changed from the base of the PR and between 7d549cd and 7703f3e.

📒 Files selected for processing (1)
  • crates/solver-core/src/engine/cost_profit.rs

@pepebndc pepebndc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

approved

nahimterrazas and others added 2 commits July 8, 2026 17:32
The PR adds the native-output e2e test but never wired it into CI, so
the headline feature's end-to-end coverage would not run on any PR.
Verified passing locally twice against real Anvil chains.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@nahimterrazas
nahimterrazas merged commit ecc1bb9 into main Jul 9, 2026
20 checks passed
@nahimterrazas
nahimterrazas deleted the native-token-fill-support branch July 9, 2026 00:04
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.

2 participants