Skip to content

feat: Enhance EIP-712 support with token versioning#294

Open
nahimterrazas wants to merge 3 commits intomainfrom
286-oif-3009-v0-quotes-missing-version-field-in-eip712domain
Open

feat: Enhance EIP-712 support with token versioning#294
nahimterrazas wants to merge 3 commits intomainfrom
286-oif-3009-v0-quotes-missing-version-field-in-eip712domain

Conversation

@nahimterrazas
Copy link
Collaborator

@nahimterrazas nahimterrazas commented Feb 11, 2026

Summary

Testing Process

Checklist

  • Add a reference to related issues in the PR description.
  • Add unit tests if applicable.

Summary by CodeRabbit

  • New Features

    • EIP-712 domain handling now includes token "version" and uses it when present.
    • Automatic token version resolution with graceful fallbacks (on-chain, token call, offline mappings, default).
    • Domain construction for EIP-3009 and compact flows now carries version.
  • Tests

    • Added thorough tests covering version resolution paths, fallback behavior, and digest/domain reconstruction differences.

solves #286

@coderabbitai
Copy link

coderabbitai bot commented Feb 11, 2026

📝 Walkthrough

Walkthrough

Adds EIP-712 version resolution and propagation: retrieves a token's EIP-712 version via EIP-5267, version() call, or a known mapping (default "1"), integrates the version into EIP-3009 domain construction and EIP-712 digest reconstruction, and expands tests for these flows.

Changes

Cohort / File(s) Summary
Version retrieval & domain building
crates/solver-service/src/apis/quote/generation.rs
Adds get_token_eip712_version() and helpers (EIP-5267, version() call, known-mapping fallback). build_eip3009_domain_object() now includes version. Exposes API helper and adds extensive tests and mocks for resolution/error paths.
EIP-712 digest reconstruction
crates/solver-types/src/utils/eip712.rs
Updates reconstruct_eip3009_digest() to read optional domain.version and compute a version-aware domain hash when present; preserves legacy branch when absent. Adds tests verifying differing digests with/without version.

Sequence Diagram

sequenceDiagram
    actor Client
    participant TokenContract
    participant Registry
    participant DomainBuilder

    Client->>TokenContract: call EIP-5267 domain()
    alt EIP-5267 returns domain+version
        TokenContract-->>Client: domain (with version)
    else EIP-5267 fails
        Client->>TokenContract: call version()
        alt version() returns non-empty
            TokenContract-->>Client: version string
        else version() empty/fails
            Client->>Registry: lookup known token version
            alt Registry has mapping
                Registry-->>Client: known version
            else no mapping
                Client-->>Client: use default "1"
            end
        end
    end

    Client->>DomainBuilder: build EIP-3009 domain (includes version)
    DomainBuilder->>DomainBuilder: compute domain hash (version-aware if present)
    DomainBuilder-->>Client: return domain object/hash
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibble on versions, crisp and bright,
Through contracts, calls, and registries light.
Fallback hops keep data tight,
Domains hashed by moonlit byte,
A carrot-coded stash — goodnight! 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is incomplete with only headers present and empty sections. The Testing Process section lacks details, and the Checklist items are unchecked with no actual testing information or issue references provided. Complete the Testing Process section with detailed testing methodology, uncheck and fill the Checklist items (especially confirming unit tests added for version resolution), and expand the Summary with key implementation details.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding token versioning support to EIP-712, which aligns with the substantial code additions for version resolution and domain construction.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 286-oif-3009-v0-quotes-missing-version-field-in-eip712domain

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.

@codecov
Copy link

codecov bot commented Feb 11, 2026

Codecov Report

❌ Patch coverage is 97.85203% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/solver-service/src/apis/quote/generation.rs 97.3% 9 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link

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

🤖 Fix all issues with AI agents
In `@crates/solver-service/src/apis/quote/generation.rs`:
- Around line 1300-1352: In get_token_eip712_version_via_eip5267, inspect the
returned eip712DomainCall::abi_decode_returns() result's fields before using
domain.version: parse domain.fields (bytes1) and check the version bit (0x02) is
set (e.g., (domain.fields[0] & 0x02) != 0); if the bit is not set, return an
Err(QuoteError::InvalidRequest(...)) indicating the token EIP-712 domain does
not include the version field, otherwise proceed to validate/trimming
domain.version as currently done.
- Around line 1213-1224: The domain version resolution must be validated against
the on-chain DOMAIN_SEPARATOR to avoid producing domains that mismatch on-chain
verification: after calling get_token_eip712_version(&alloy_token_address,
chain_id) and before returning the JSON domain (the block that builds the domain
object with "name","version","chainId","verifyingContract"), fetch the token's
DOMAIN_SEPARATOR from chain and compute the expected domain separator using the
resolved version; if they differ, fallback to retrying domain construction
without the "version" field (or log and return an explicit error) so we don't
silently return a domain that will produce signatures that fail on-chain; update
the code around the token version resolution and domain construction to perform
this compare and handle the mismatch (reference get_token_eip712_version, the
domain JSON construction, and the on-chain DOMAIN_SEPARATOR check).
🧹 Nitpick comments (1)
crates/solver-service/src/apis/quote/generation.rs (1)

1399-1420: Hardcoded known token registry — maintainability consideration.

This offline registry covers known USDC deployments and will need manual updates for new chain deployments. The to_ascii_lowercase() call on line 1405 is redundant since format!("0x{:040x}", ...) already produces lowercase, but it's harmless.

Consider adding a code comment listing the token name (e.g., // USDC) next to the chain/address pairs for future maintainers, since the addresses alone aren't self-documenting.

Comment on lines +1213 to 1224
let token_version = self
.get_token_eip712_version(&alloy_token_address, chain_id)
.await;

// Build domain object similar to TheCompact structure (without pre-computed domainSeparator)
// The client will compute the domainSeparator using these fields
Ok(serde_json::json!({
"name": token_name,
"version": token_version,
"chainId": chain_id.to_string(),
"verifyingContract": format!("0x{:040x}", alloy_token_address)
}))
Copy link

@coderabbitai coderabbitai bot Feb 11, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find where domain_separator is used in generation.rs and related files
rg -n "domain_separator" crates/solver-service/src/apis/quote/generation.rs -C 3

Repository: openintentsframework/oif-solver

Length of output: 2005


🏁 Script executed:

#!/bin/bash
# Check if there's any validation comparing computed domain hash vs fetched domain_separator
rg -n "domain_separator\|domain_hash" crates/solver-service/src/apis/quote/ -A 2 -B 2 --type=rust | head -100

Repository: openintentsframework/oif-solver

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Look for the get_eip3009_domain_separator function definition
rg -n "fn get_eip3009_domain_separator" crates/solver-service/src/apis/quote/generation.rs -A 20

Repository: openintentsframework/oif-solver

Length of output: 767


🏁 Script executed:

#!/bin/bash
# Search for where the domain is actually used during signature generation/verification
rg -n "\.domain\|payload\.domain" crates/solver-service/src/apis/quote/generation.rs -B 2 -A 2 | head -80

Repository: openintentsframework/oif-solver

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Check if there are any comments or documentation about EIP-3009 domain validation
rg -n "EIP.?3009\|eip.?3009" crates/solver-service/src/apis/quote/generation.rs -B 1 -A 3 | head -100

Repository: openintentsframework/oif-solver

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Check the build_eip3009_eip712_types to see the fixed domain structure
rg -n "fn build_eip3009_eip712_types" crates/solver-service/src/apis/quote/generation.rs -A 40 | head -60

Repository: openintentsframework/oif-solver

Length of output: 1681


🏁 Script executed:

#!/bin/bash
# Look at where the domain is actually used in payload construction
rg -n "\.domain\|payload" crates/solver-service/src/apis/quote/generation.rs -B 1 -A 1 | grep -A 2 -B 2 "eip3009\|domain"

Repository: openintentsframework/oif-solver

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Check the known USDC versions and see if there's any validation
rg -n "get_known_eip3009_token_version\|known.*version" crates/solver-service/src/apis/quote/generation.rs -A 25 | head -80

Repository: openintentsframework/oif-solver

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Look at eip712.rs to see how domain_separator is used during reconstruction
rg -n "reconstruct_eip3009\|domain_separator" crates/solver-service/src/eip712.rs -B 2 -A 5 | head -100

Repository: openintentsframework/oif-solver

Length of output: 150


🏁 Script executed:

#!/bin/bash
# Check if there's any logic that validates domain hash matches
rg -n "compute.*domain\|validate.*domain\|hash.*domain" crates/solver-service/src/ -i --type=rust | head -30

Repository: openintentsframework/oif-solver

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Find the actual domain construction for EIP-3009
rg -n "build_eip3009_domain_object" crates/solver-service/src/apis/quote/generation.rs -A 20 | head -50

Repository: openintentsframework/oif-solver

Length of output: 2073


🏁 Script executed:

#!/bin/bash
# Search for known USDC versions - look around line 1399
sed -n '1399,1430p' crates/solver-service/src/apis/quote/generation.rs

Repository: openintentsframework/oif-solver

Length of output: 1309


🏁 Script executed:

#!/bin/bash
# Look at the complete get_known_eip3009_token_version function
sed -n '1399,1425p' crates/solver-service/src/apis/quote/generation.rs

Repository: openintentsframework/oif-solver

Length of output: 1141


🏁 Script executed:

#!/bin/bash
# Find where the payload with domain and types is returned
rg -n "types.*domain\|\"types\"\|\"domain\"" crates/solver-service/src/apis/quote/generation.rs -B 2 -A 2 | head -80

Repository: openintentsframework/oif-solver

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Check if there's any code that computes the domain separator hash from the domain object
rg -n "domain_separator\|hash" crates/solver-service/src/apis/quote/generation.rs | head -50

Repository: openintentsframework/oif-solver

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Look for the EIP712 domain hash computation logic - check if it exists anywhere
find crates/solver-service/src -name "*.rs" -type f | xargs rg -l "domain.*hash\|EIP.?712.*domain" | head -10

Repository: openintentsframework/oif-solver

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Look at get_token_eip712_version to understand the complete fallback logic
sed -n '1267,1298p' crates/solver-service/src/apis/quote/generation.rs

Repository: openintentsframework/oif-solver

Length of output: 817


🏁 Script executed:

#!/bin/bash
# See if there's any code that uses the computed domain hash and compares it
rg -n "compute\|hash\|verify" crates/solver-service/src/apis/quote/generation.rs | grep -i domain

Repository: openintentsframework/oif-solver

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Check the complete flow - look at where domain and domain_separator are both present
sed -n '600,660p' crates/solver-service/src/apis/quote/generation.rs

Repository: openintentsframework/oif-solver

Length of output: 2490


🏁 Script executed:

#!/bin/bash
# Look for any validation or comparison of domain hash vs fetched separator
rg -n "domain_separator.*==" crates/solver-service/src/apis/quote/generation.rs

Repository: openintentsframework/oif-solver

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Verify: does metadata include both domain and domain_separator?
sed -n '642,680p' crates/solver-service/src/apis/quote/generation.rs

Repository: openintentsframework/oif-solver

Length of output: 1897


🏁 Script executed:

#!/bin/bash
# Check if there are any tests that validate domain_separator vs computed domain
rg -n "test.*domain\|domain.*test" crates/solver-service/src/apis/quote/generation.rs | grep -i separator

Repository: openintentsframework/oif-solver

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Look at the test for build_eip3009_domain_object to see what it expects
sed -n '3049,3080p' crates/solver-service/src/apis/quote/generation.rs

Repository: openintentsframework/oif-solver

Length of output: 947


🏁 Script executed:

#!/bin/bash
# Check test coverage for domain version fallback scenarios
rg -n "test_get_token_eip712_version" crates/solver-service/src/apis/quote/generation.rs -A 30 | head -80

Repository: openintentsframework/oif-solver

Length of output: 2905


🏁 Script executed:

#!/bin/bash
# Look for any error handling or validation related to domain mismatch
rg -n "version.*mismatch\|domain.*mismatch\|mismatch" crates/solver-service/src/apis/quote/generation.rs

Repository: openintentsframework/oif-solver

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Search for client-side domain validation or signature generation logic
find crates -name "*.rs" -type f | xargs rg -l "domain.*hash\|sign.*domain" | head -10

Repository: openintentsframework/oif-solver

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Let's look at the complete flow to understand the signature generation and metadata usage
sed -n '595,700p' crates/solver-service/src/apis/quote/generation.rs

Repository: openintentsframework/oif-solver

Length of output: 4040


🏁 Script executed:

#!/bin/bash
# Check if there's any code path that verifies domain hash matches domain_separator
rg -n "DOMAIN_SEPARATOR\|domain_separator" crates/solver-service/src/apis/quote/generation.rs -B 3 -A 3

Repository: openintentsframework/oif-solver

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Look for any comments about domain validation or known issues
rg -n "TODO\|FIXME\|XXX\|BUG\|domain.*version\|version.*domain" crates/solver-service/src/apis/quote/generation.rs -i

Repository: openintentsframework/oif-solver

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Check what happens in the payload - does it include both domain and domain_separator?
sed -n '685,695p' crates/solver-service/src/apis/quote/generation.rs

Repository: openintentsframework/oif-solver

Length of output: 321


🏁 Script executed:

#!/bin/bash
# Look for any client-side code that uses the domain object to compute domain hash
find crates -name "*.rs" -type f | xargs rg -l "domain.*hash\|compute.*domain" 2>/dev/null

Repository: openintentsframework/oif-solver

Length of output: 59


🏁 Script executed:

#!/bin/bash
# Check if OrderPayload and metadata are both returned to the client
rg -n "OrderPayload\|OifOrder" crates/solver-service/src/apis/quote/generation.rs | head -20

Repository: openintentsframework/oif-solver

Length of output: 57


Domain version resolution works well for common tokens but lacks validation for edge cases.

The version resolution uses solid fallbacks (EIP-5267 → version() call → known registry → default "1"), and all known USDC deployments are correctly registered with version "2". However, for unknown tokens without EIP-5267 or version() support whose on-chain DOMAIN_SEPARATOR was computed without a version field, defaulting to "1" and unconditionally including version in the domain could cause signatures that pass server-side verification to fail on-chain.

Consider adding a validation step that compares the domain hash computed from the resolved version against the fetched DOMAIN_SEPARATOR. If they don't match, you could either retry without version or log an explicit error, preventing silent signature failures.

🤖 Prompt for AI Agents
In `@crates/solver-service/src/apis/quote/generation.rs` around lines 1213 - 1224,
The domain version resolution must be validated against the on-chain
DOMAIN_SEPARATOR to avoid producing domains that mismatch on-chain verification:
after calling get_token_eip712_version(&alloy_token_address, chain_id) and
before returning the JSON domain (the block that builds the domain object with
"name","version","chainId","verifyingContract"), fetch the token's
DOMAIN_SEPARATOR from chain and compute the expected domain separator using the
resolved version; if they differ, fallback to retrying domain construction
without the "version" field (or log and return an explicit error) so we don't
silently return a domain that will produce signatures that fail on-chain; update
the code around the token version resolution and domain construction to perform
this compare and handle the mismatch (reference get_token_eip712_version, the
domain JSON construction, and the on-chain DOMAIN_SEPARATOR check).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah, perhaps this makes sense, wdyt @shahnami, still with that we managed most cases

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines +1300 to +1352
/// Resolve token EIP-712 version from EIP-5267 `eip712Domain()`.
async fn get_token_eip712_version_via_eip5267(
&self,
token_address: &alloy_primitives::Address,
chain_id: u64,
) -> Result<String, QuoteError> {
use alloy_sol_types::{sol, SolCall};

sol! {
function eip712Domain() external view returns (
bytes1 fields,
string name,
string version,
uint256 chainId,
address verifyingContract,
bytes32 salt,
uint256[] extensions
);
}

let tx = solver_types::Transaction {
to: Some(solver_types::Address(token_address.0.to_vec())),
data: eip712DomainCall {}.abi_encode(),
value: alloy_primitives::U256::ZERO,
chain_id,
nonce: None,
gas_limit: None,
gas_price: None,
max_fee_per_gas: None,
max_priority_fee_per_gas: None,
};

let result = self
.delivery_service
.contract_call(chain_id, tx)
.await
.map_err(|e| {
QuoteError::InvalidRequest(format!("Failed to get token EIP-712 domain: {e}"))
})?;

let domain = eip712DomainCall::abi_decode_returns(&result).map_err(|e| {
QuoteError::InvalidRequest(format!("Failed to decode token EIP-712 domain: {e}"))
})?;
let version = domain.version;

if version.trim().is_empty() {
return Err(QuoteError::InvalidRequest(
"Token EIP-712 version is empty".to_string(),
));
}

Ok(version)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

EIP-5267 fields bitmask eip712Domain specification

💡 Result:

In EIP-5267, the fields return value is a bitmask indicating which optional EIP-712 EIP712Domain fields are present. Bit i is 1 iff domain field i is present, with bits read least-significant to most-significant and indices matching the EIP-712 field order (the same order used in the eip712Domain() return tuple). [1]

Bit positions (EIP-5267 ⇄ EIP-712 EIP712Domain)

Bit Value Field (EIP-712)
0 0x01 name
1 0x02 version
2 0x04 chainId
3 0x08 verifyingContract
4 0x10 salt

EIP-712 defines the EIP712Domain struct as having one or more of these optional fields (name, version, chainId, verifyingContract, salt), and the domain separator is hashStruct(eip712Domain). [2]

Example

If a domain includes name, chainId, verifyingContract (but not version or salt), then:

  • bits set: 0, 2, 3 → binary 0b01101 → hex 0x0d [1]

Sources: EIP-5267 [1], EIP-712 [2].


🏁 Script executed:

cd crates/solver-service && find . -type f -name "*.rs" | head -20

Repository: openintentsframework/oif-solver

Length of output: 589


🏁 Script executed:

# Check the code around line 1300-1352 to understand the fields type
sed -n '1300,1352p' src/apis/quote/generation.rs

Repository: openintentsframework/oif-solver

Length of output: 146


🏁 Script executed:

# Look for similar eip712Domain usages in the codebase to see how fields is accessed elsewhere
rg "eip712Domain\|fields.*0x" -A 3 -B 3 --type rs

Repository: openintentsframework/oif-solver

Length of output: 101


🏁 Script executed:

# Read the relevant section from the correct path
sed -n '1300,1352p' crates/solver-service/src/apis/quote/generation.rs

Repository: openintentsframework/oif-solver

Length of output: 1424


🏁 Script executed:

# Search for eip712Domain usage in the repository
rg "eip712Domain" -A 5 -B 2 crates/solver-service/src/apis/quote/

Repository: openintentsframework/oif-solver

Length of output: 9214


EIP-5267 fields bitmask must be checked before using version.

Per EIP-5267, the fields return value is a bitmask where bit 1 (0x02) indicates whether version is part of the domain. The current code ignores fields and always uses the returned version string. If a token's fields value excludes the version bit but still returns a non-empty default for the version string, this could resolve to an incorrect version.

Add a check for the fields bitmask before accepting the version:

Proposed fix
 		let domain = eip712DomainCall::abi_decode_returns(&result).map_err(|e| {
 			QuoteError::InvalidRequest(format!("Failed to decode token EIP-712 domain: {e}"))
 		})?;
+		// Check EIP-5267 fields bitmask: bit 1 (0x02) indicates version is part of the domain
+		if domain.fields[0] & 0x02 == 0 {
+			return Err(QuoteError::InvalidRequest(
+				"Token EIP-712 domain does not include version field (per EIP-5267 fields bitmask)".to_string(),
+			));
+		}
 		let version = domain.version;
📝 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
/// Resolve token EIP-712 version from EIP-5267 `eip712Domain()`.
async fn get_token_eip712_version_via_eip5267(
&self,
token_address: &alloy_primitives::Address,
chain_id: u64,
) -> Result<String, QuoteError> {
use alloy_sol_types::{sol, SolCall};
sol! {
function eip712Domain() external view returns (
bytes1 fields,
string name,
string version,
uint256 chainId,
address verifyingContract,
bytes32 salt,
uint256[] extensions
);
}
let tx = solver_types::Transaction {
to: Some(solver_types::Address(token_address.0.to_vec())),
data: eip712DomainCall {}.abi_encode(),
value: alloy_primitives::U256::ZERO,
chain_id,
nonce: None,
gas_limit: None,
gas_price: None,
max_fee_per_gas: None,
max_priority_fee_per_gas: None,
};
let result = self
.delivery_service
.contract_call(chain_id, tx)
.await
.map_err(|e| {
QuoteError::InvalidRequest(format!("Failed to get token EIP-712 domain: {e}"))
})?;
let domain = eip712DomainCall::abi_decode_returns(&result).map_err(|e| {
QuoteError::InvalidRequest(format!("Failed to decode token EIP-712 domain: {e}"))
})?;
let version = domain.version;
if version.trim().is_empty() {
return Err(QuoteError::InvalidRequest(
"Token EIP-712 version is empty".to_string(),
));
}
Ok(version)
}
/// Resolve token EIP-712 version from EIP-5267 `eip712Domain()`.
async fn get_token_eip712_version_via_eip5267(
&self,
token_address: &alloy_primitives::Address,
chain_id: u64,
) -> Result<String, QuoteError> {
use alloy_sol_types::{sol, SolCall};
sol! {
function eip712Domain() external view returns (
bytes1 fields,
string name,
string version,
uint256 chainId,
address verifyingContract,
bytes32 salt,
uint256[] extensions
);
}
let tx = solver_types::Transaction {
to: Some(solver_types::Address(token_address.0.to_vec())),
data: eip712DomainCall {}.abi_encode(),
value: alloy_primitives::U256::ZERO,
chain_id,
nonce: None,
gas_limit: None,
gas_price: None,
max_fee_per_gas: None,
max_priority_fee_per_gas: None,
};
let result = self
.delivery_service
.contract_call(chain_id, tx)
.await
.map_err(|e| {
QuoteError::InvalidRequest(format!("Failed to get token EIP-712 domain: {e}"))
})?;
let domain = eip712DomainCall::abi_decode_returns(&result).map_err(|e| {
QuoteError::InvalidRequest(format!("Failed to decode token EIP-712 domain: {e}"))
})?;
// Check EIP-5267 fields bitmask: bit 1 (0x02) indicates version is part of the domain
if domain.fields[0] & 0x02 == 0 {
return Err(QuoteError::InvalidRequest(
"Token EIP-712 domain does not include version field (per EIP-5267 fields bitmask)".to_string(),
));
}
let version = domain.version;
if version.trim().is_empty() {
return Err(QuoteError::InvalidRequest(
"Token EIP-712 version is empty".to_string(),
));
}
Ok(version)
}
🤖 Prompt for AI Agents
In `@crates/solver-service/src/apis/quote/generation.rs` around lines 1300 - 1352,
In get_token_eip712_version_via_eip5267, inspect the returned
eip712DomainCall::abi_decode_returns() result's fields before using
domain.version: parse domain.fields (bytes1) and check the version bit (0x02) is
set (e.g., (domain.fields[0] & 0x02) != 0); if the bit is not set, return an
Err(QuoteError::InvalidRequest(...)) indicating the token EIP-712 domain does
not include the version field, otherwise proceed to validate/trimming
domain.version as currently done.

@nahimterrazas
Copy link
Collaborator Author

@shahnami @pepebndc

Copy link

@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

🤖 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-service/src/apis/quote/generation.rs`:
- Around line 3083-3092: The tests test_get_token_eip712_version_uses_eip5267
and test_get_token_eip712_version_via_eip5267_empty_version construct an
eip712DomainReturn with fields set to FixedBytes::<1>::from([0x1]) which only
marks the name bit and omits the version per EIP-5267; update the fields bitmask
in those test instances of eip712DomainReturn to include name, version, chainId,
and verifyingContract (use 0x0f) so the mocked EIP-5267 response correctly
indicates the version is present and the assertions against version "2" will be
valid.

---

Duplicate comments:
In `@crates/solver-service/src/apis/quote/generation.rs`:
- Around line 1340-1351: Before using domain.version (decoded via
eip712DomainCall::abi_decode_returns), check the EIP-5267 fields bitmask:
inspect domain.fields[0] & 0x02 to confirm the version field is present; if the
bit is not set, return an appropriate QuoteError::InvalidRequest (or treat as
absent) instead of reading domain.version, otherwise proceed to validate
version.trim().is_empty() as before. Ensure you reference domain.fields,
domain.version, eip712DomainCall::abi_decode_returns, and
QuoteError::InvalidRequest when locating and updating the code.
- Around line 1213-1224: The code unconditionally inserts the token "version"
from get_token_eip712_version into the reconstructed domain JSON (in
generation.rs where the domain object is built), which can mismatch tokens whose
on-chain DOMAIN_SEPARATOR was computed without a version. Change the logic to
query the token's on-chain DOMAIN_SEPARATOR (call DOMAIN_SEPARATOR()), then
compute the local domain separator twice: once including the obtained version
from get_token_eip712_version and once without a version; compare each computed
separator to the on-chain value and only include the version in the returned
JSON if the version-inclusive computation matches the on-chain
DOMAIN_SEPARATOR—otherwise omit the version (or use the variant that matches
on-chain). Use the existing get_token_eip712_version helper and the domain
construction code around the serde_json::json! block to implement this
validation and selection.

Comment on lines +3083 to +3092
let eip712_response =
Bytes::from(eip712DomainCall::abi_encode_returns(&eip712DomainReturn {
fields: FixedBytes::<1>::from([0x1]),
name: "USD Coin".to_string(),
version: "2".to_string(),
chainId: U256::from(1u64),
verifyingContract: AlloyAddress::from([0x11; 20]),
salt: FixedBytes::<32>::from([0u8; 32]),
extensions: Vec::<U256>::new(),
}));
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Incorrect EIP-5267 fields bitmask in two tests — version bit (0x02) is not set.

Both test_get_token_eip712_version_uses_eip5267 (line 3085) and test_get_token_eip712_version_via_eip5267_empty_version (line 3240) use fields: FixedBytes::<1>::from([0x1]). Per EIP-5267:

Bit Mask Field
0 0x01 name
1 0x02 version
2 0x04 chainId
3 0x08 verifyingContract

0x01 has only the name bit set, meaning the domain does not include a version field per spec. A response representing a standard token domain (name + version + chainId + verifyingContract) should use 0x0f.

As written, test_get_token_eip712_version_uses_eip5267 asserts version "2" is returned from a response where the fields bitmask says version is absent — which is only true because the fields check is not implemented. If the fields check from the previous review comment is added, this test will fail.

🔧 Suggested fix for both tests
- fields: FixedBytes::<1>::from([0x1]),
+ fields: FixedBytes::<1>::from([0x0f]),

Also applies to: 3237-3248

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

In `@crates/solver-service/src/apis/quote/generation.rs` around lines 3083 - 3092,
The tests test_get_token_eip712_version_uses_eip5267 and
test_get_token_eip712_version_via_eip5267_empty_version construct an
eip712DomainReturn with fields set to FixedBytes::<1>::from([0x1]) which only
marks the name bit and omits the version per EIP-5267; update the fields bitmask
in those test instances of eip712DomainReturn to include name, version, chainId,
and verifyingContract (use 0x0f) so the mocked EIP-5267 response correctly
indicates the version is present and the assertions against version "2" will be
valid.

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

Comments