Skip to content

implementing exclusivity orders#318

Open
nahimterrazas wants to merge 1 commit into
mainfrom
exclusive-orders
Open

implementing exclusivity orders#318
nahimterrazas wants to merge 1 commit into
mainfrom
exclusive-orders

Conversation

@nahimterrazas
Copy link
Copy Markdown
Collaborator

@nahimterrazas nahimterrazas commented Apr 26, 2026

Summary

Testing Process

Checklist

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

Summary by CodeRabbit

Release Notes

  • New Features

    • Added exclusive-fill configuration for quotes supporting three modes: disabled, optional, or required
    • Quotes now embed solver-specific exclusivity context in order types
    • Configuration includes enforced time constraints with default and maximum duration settings
  • Tests

    • Added tests validating exclusivity configuration, context generation, and constraint enforcement

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 2026

📝 Walkthrough

Walkthrough

This PR introduces optional exclusive-fill configuration to quote generation. It defines ExclusivityMode and ExclusivityConfig in solver-config, adds exclusivity context resolution and encoding logic, and integrates exclusive context injection into quote generation workflows (Permit2, EIP-3009, and BatchCompact). The solver address is passed through the quote generation stack to enable solver-specific context embedding.

Changes

Cohort / File(s) Summary
Configuration & Types
crates/solver-config/src/lib.rs, crates/solver-core/src/engine/mod.rs, crates/solver-types/src/standards/eip7683.rs
Added ExclusivityMode enum (disabled|optional|required) and ExclusivityConfig struct with validation; extended QuoteConfig with optional exclusivity field; added solver_address() accessor to SolverEngine; updated EIP-3009 conversion to populate SolMandateOutput.context from metadata.
Exclusivity Logic
crates/solver-service/src/apis/quote/signing/exclusivity.rs, crates/solver-service/src/apis/quote/signing/mod.rs
Introduced new exclusivity module with ExclusivityParams, resolve_exclusivity() (parses request metadata and server config with overflow checks), and encode_exclusive_context() (produces 37-byte tagged context); module is publicly exported.
Quote Generation Flow
crates/solver-service/src/apis/quote/generation.rs, crates/solver-service/src/apis/quote/mod.rs
Modified QuoteGenerator::new() to accept solver_address; updated compute_eip3009_order_identifier() to return context bytes and inject them into SolMandateOutput.context; integrated exclusivity resolution for Permit2, EIP-3009, and BatchCompact protocols; expanded tests to verify default empty context and exclusive context behavior.
Permit2 Witness Signing
crates/solver-service/src/apis/quote/signing/payloads/permit2.rs
Updated build_permit2_batch_witness_digest() signature to accept solver_address and optional exclusivity parameters; modified context hash and JSON output to include encoded exclusive context when present, or empty hash/"0x" when disabled.

Sequence Diagram

sequenceDiagram
    participant Client as Client Request
    participant QG as QuoteGenerator
    participant ER as Exclusivity Resolver
    participant EC as Context Encoder
    participant PS as Protocol Signing
    participant Output as Quote Response

    Client->>QG: Quote request with metadata
    QG->>ER: resolve_exclusivity(config, request, now)
    ER->>ER: Validate duration from metadata
    ER->>ER: Check against config mode
    ER-->>QG: ExclusivityParams or None
    
    alt Exclusivity Enabled
        QG->>EC: encode_exclusive_context(solver_address, start_time)
        EC-->>QG: context_bytes (37-byte tagged format)
        QG->>PS: Sign with context_bytes
        PS->>PS: Hash context into witness digest
        PS-->>Output: Include context_hex in output
    else Exclusivity Disabled
        QG->>PS: Sign without context
        PS->>PS: Use empty context hash
        PS-->>Output: context_hex = "0x"
    end
    
    Output-->>Client: Quote with protocol-specific context
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Quote time config #248: Modifies QuoteConfig struct in solver-config with timing-related fields alongside this PR's exclusivity field additions.
  • Quote latency fix #313: Updates quote generation flow in generation.rs and mod.rs, including QuoteGenerator construction changes that overlap with solver address integration.
  • chore: Update contracts and introduce some fixes #237: Modifies Permit2 witness construction path and build_permit2_batch_witness_digest function signature similarly to inject additional context into digest encoding.

Suggested reviewers

  • shahnami
  • pepebndc
  • NicoMolinaOZ

🐰 Exclusive fills now bloom so bright,
Context encoded, wrapped just right,
Solver's address marks the way,
Future timestamps guard the day,
Quotes now carry secrets tight! 🌟

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is largely incomplete with empty sections for Summary and Testing Process, and unchecked checklist items for referencing issues and adding unit tests. Complete the description by adding a detailed summary of changes, documenting the testing process, and ensuring all checklist items are addressed or updated appropriately.
Title check ❓ Inconclusive The title 'implementing exclusivity orders' is vague and generic, using non-descriptive language that lacks specific technical detail about the feature being added. Use a more specific title that describes the feature more clearly, such as 'Add exclusive-fill configuration and context encoding for quote generation'.
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ 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 exclusive-orders

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
Copy Markdown

codecov Bot commented Apr 26, 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/solver-service/src/apis/quote/generation.rs (1)

822-824: ⚠️ Potential issue | 🟡 Minor

Stale rustdoc on compute_eip3009_order_identifier.

The doc comment still says it returns (nonce, order_identifier) but the signature now returns a 3-tuple including context_bytes. Update the doc so callers don't have to read the body to know about the exclusive-context payload.

📝 Proposed doc fix
-	/// Compute the orderIdentifier for an EIP-3009 order by building a StandardOrder
-	/// and calling the contract's orderIdentifier function using the delivery service
-	/// Returns (nonce, order_identifier)
+	/// Compute the orderIdentifier for an EIP-3009 order by building a StandardOrder
+	/// and calling the contract's orderIdentifier function using the delivery service.
+	///
+	/// Returns `(nonce, order_identifier, context_bytes)` where `context_bytes` is the
+	/// resolved exclusive-fill context (empty when exclusivity is disabled) that was
+	/// embedded into `outputs[0].context` for the orderIdentifier computation.
🤖 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 822 - 824,
Update the stale doc comment for compute_eip3009_order_identifier to reflect the
new return type: mention that it returns a 3-tuple (nonce, order_identifier,
context_bytes) where context_bytes is the exclusive-context payload used in the
EIP-3009 order; keep the existing brief description about building a
StandardOrder and calling the contract's orderIdentifier function via the
delivery service but add the new context_bytes element to the documented return
value so callers see all three components without reading the implementation.
🧹 Nitpick comments (3)
crates/solver-core/src/engine/mod.rs (1)

671-674: Tighten doc wording for the accessor return type

The method returns &solver_types::Address (wrapper type), not raw bytes directly. Clarifying this avoids caller confusion.

✏️ Suggested doc tweak
-/// Returns the solver address as raw bytes.
+/// Returns the solver address.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/solver-core/src/engine/mod.rs` around lines 671 - 674, The doc comment
for the accessor solver_address should be corrected to state that it returns a
reference to the wrapper type solver_types::Address (not raw bytes); update the
doc line above pub fn solver_address(&self) -> &solver_types::Address to clarify
it returns &solver_types::Address (a wrapper/address type) and not raw bytes, so
callers understand they receive the Address wrapper rather than raw byte data.
crates/solver-service/src/apis/quote/signing/exclusivity.rs (1)

107-110: Make address-length validation non-panicking in production

copy_from_slice will panic if solver_address.0.len() != 20. Consider making encode_exclusive_context fallible and propagating an explicit QuoteError instead of relying on a debug-only assert.

🛡️ Suggested direction
-pub fn encode_exclusive_context(solver_address: &Address, start_time: u32) -> Vec<u8> {
+pub fn encode_exclusive_context(
+	solver_address: &Address,
+	start_time: u32,
+) -> Result<Vec<u8>, QuoteError> {
 	let mut out = Vec::with_capacity(1 + 32 + 4);
 	out.push(EXCLUSIVE_CONTEXT_TAG);
 	let mut bytes32 = [0u8; 32];
 	let addr_bytes = &solver_address.0;
-	debug_assert_eq!(addr_bytes.len(), 20, "solver address must be 20 bytes");
+	if addr_bytes.len() != 20 {
+		return Err(QuoteError::InvalidRequest(
+			"solver address must be 20 bytes".to_string(),
+		));
+	}
 	bytes32[12..].copy_from_slice(addr_bytes);
 	out.extend_from_slice(&bytes32);
 	out.extend_from_slice(&start_time.to_be_bytes());
-	out
+	Ok(out)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/solver-service/src/apis/quote/signing/exclusivity.rs` around lines 107
- 110, The code uses debug_assert_eq and copy_from_slice on solver_address.0
which will panic in production if the length is not 20; change
encode_exclusive_context to be fallible (return Result<..., QuoteError>),
replace the debug_assert_eq with an explicit length check on solver_address.0
(expecting 20) that returns a descriptive QuoteError on mismatch, and only then
perform bytes32[12..].copy_from_slice(addr_bytes) and
out.extend_from_slice(&bytes32); update callers of encode_exclusive_context to
propagate the Result accordingly.
crates/solver-service/src/apis/quote/generation.rs (1)

843-858: Extract the exclusivity-resolution boilerplate into a helper.

The same five-line now_secs + config.api → quote → exclusivity + resolve_exclusivity(...) block is duplicated verbatim in three places:

  • here (lines 843-858) inside compute_eip3009_order_identifier,
  • lines 1235-1256 inside build_compact_message,
  • lines 1678-1687 inside build_permit2_message_object.

Beyond the DRY violation, hoisting this into a QuoteGenerator method (or, even better, into generate_eip3009_order so it runs before tokio::try_join! at lines 725-736) lets the EIP-3009 path fail-fast on malformed metadata.exclusivity without first dispatching the sibling build_eip3009_domain_object / get_eip3009_domain_separator contract calls. The resolved ExclusivityParams (or already-encoded bytes) can then be threaded down into compute_eip3009_order_identifier the same way it is into build_permit2_batch_witness_digest.

♻️ Sketch of the helper
fn resolve_request_exclusivity(
    &self,
    request: &GetQuoteRequest,
    config: &Config,
) -> Result<Option<crate::apis::quote::signing::exclusivity::ExclusivityParams>, QuoteError> {
    use crate::apis::quote::signing::exclusivity::resolve_exclusivity;
    let now_secs = chrono::Utc::now().timestamp() as u64;
    let excl_cfg = config
        .api
        .as_ref()
        .and_then(|a| a.quote.as_ref())
        .and_then(|q| q.exclusivity.as_ref());
    resolve_exclusivity(excl_cfg, request.intent.metadata.as_ref(), now_secs)
}

Then compute_eip3009_order_identifier accepts exclusivity: Option<ExclusivityParams> (mirroring the permit2 helper), build_compact_message calls self.resolve_request_exclusivity(request, config)? once, and generate_eip3009_order resolves once before try_join!.

🤖 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 843 - 858,
Duplicate exclusivity-resolution logic appears in
compute_eip3009_order_identifier, build_compact_message, and
build_permit2_message_object; extract it into a QuoteGenerator method (e.g.,
resolve_request_exclusivity) that encapsulates the now_secs + config.api → quote
→ exclusivity lookup and resolve_exclusivity(...) call and returns
Result<Option<ExclusivityParams>, QuoteError>. Call this helper once from
generate_eip3009_order before the tokio::try_join! so the EIP-3009 path fails
fast, and change compute_eip3009_order_identifier, build_compact_message, and
build_permit2_message_object to accept the resolved Option<ExclusivityParams>
(or pre-encoded context bytes) instead of re-running the boilerplate.
🤖 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-types/src/standards/eip7683.rs`:
- Around line 754-763: The code currently treats a present-but-non-string
outputs[].context as "0x" which silently changes reconstructed output context;
update the extraction to explicitly reject non-string types: instead of using
output_obj.get("context").and_then(|c| c.as_str()).unwrap_or("0x"), match on
output_obj.get("context") and if Some(value) then require value.as_str() to
return Some(str) or return an Err with a clear message (e.g., "outputs[].context
must be a string") so malformed types are rejected; keep the existing hex
decoding logic for the resulting string (refer to the
output_obj/context_str/context_bytes variables in this block).

---

Outside diff comments:
In `@crates/solver-service/src/apis/quote/generation.rs`:
- Around line 822-824: Update the stale doc comment for
compute_eip3009_order_identifier to reflect the new return type: mention that it
returns a 3-tuple (nonce, order_identifier, context_bytes) where context_bytes
is the exclusive-context payload used in the EIP-3009 order; keep the existing
brief description about building a StandardOrder and calling the contract's
orderIdentifier function via the delivery service but add the new context_bytes
element to the documented return value so callers see all three components
without reading the implementation.

---

Nitpick comments:
In `@crates/solver-core/src/engine/mod.rs`:
- Around line 671-674: The doc comment for the accessor solver_address should be
corrected to state that it returns a reference to the wrapper type
solver_types::Address (not raw bytes); update the doc line above pub fn
solver_address(&self) -> &solver_types::Address to clarify it returns
&solver_types::Address (a wrapper/address type) and not raw bytes, so callers
understand they receive the Address wrapper rather than raw byte data.

In `@crates/solver-service/src/apis/quote/generation.rs`:
- Around line 843-858: Duplicate exclusivity-resolution logic appears in
compute_eip3009_order_identifier, build_compact_message, and
build_permit2_message_object; extract it into a QuoteGenerator method (e.g.,
resolve_request_exclusivity) that encapsulates the now_secs + config.api → quote
→ exclusivity lookup and resolve_exclusivity(...) call and returns
Result<Option<ExclusivityParams>, QuoteError>. Call this helper once from
generate_eip3009_order before the tokio::try_join! so the EIP-3009 path fails
fast, and change compute_eip3009_order_identifier, build_compact_message, and
build_permit2_message_object to accept the resolved Option<ExclusivityParams>
(or pre-encoded context bytes) instead of re-running the boilerplate.

In `@crates/solver-service/src/apis/quote/signing/exclusivity.rs`:
- Around line 107-110: The code uses debug_assert_eq and copy_from_slice on
solver_address.0 which will panic in production if the length is not 20; change
encode_exclusive_context to be fallible (return Result<..., QuoteError>),
replace the debug_assert_eq with an explicit length check on solver_address.0
(expecting 20) that returns a descriptive QuoteError on mismatch, and only then
perform bytes32[12..].copy_from_slice(addr_bytes) and
out.extend_from_slice(&bytes32); update callers of encode_exclusive_context to
propagate the Result accordingly.
🪄 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: 865a6279-c196-451f-89dd-893a86f8090d

📥 Commits

Reviewing files that changed from the base of the PR and between dcb62d1 and 98ec1a5.

📒 Files selected for processing (8)
  • crates/solver-config/src/lib.rs
  • crates/solver-core/src/engine/mod.rs
  • crates/solver-service/src/apis/quote/generation.rs
  • crates/solver-service/src/apis/quote/mod.rs
  • crates/solver-service/src/apis/quote/signing/exclusivity.rs
  • crates/solver-service/src/apis/quote/signing/mod.rs
  • crates/solver-service/src/apis/quote/signing/payloads/permit2.rs
  • crates/solver-types/src/standards/eip7683.rs

Comment on lines +754 to +763
let context_str = output_obj
.get("context")
.and_then(|c| c.as_str())
.unwrap_or("0x");
let context_bytes = if context_str == "0x" || context_str.is_empty() {
Vec::new()
} else {
hex::decode(context_str.trim_start_matches("0x"))
.map_err(|e| format!("Invalid context hex '{context_str}': {e}"))?
};
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

Reject malformed outputs[].context types instead of silently zeroing

If context is present but not a string, current logic treats it as "0x". That silently changes reconstructed output context and can produce inconsistent order reconstruction behavior.

🔧 Suggested fix
-				let context_str = output_obj
-					.get("context")
-					.and_then(|c| c.as_str())
-					.unwrap_or("0x");
+				let context_str = match output_obj.get("context") {
+					None => "0x",
+					Some(v) => v
+						.as_str()
+						.ok_or("Invalid context in output metadata: expected hex string")?,
+				};
📝 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
let context_str = output_obj
.get("context")
.and_then(|c| c.as_str())
.unwrap_or("0x");
let context_bytes = if context_str == "0x" || context_str.is_empty() {
Vec::new()
} else {
hex::decode(context_str.trim_start_matches("0x"))
.map_err(|e| format!("Invalid context hex '{context_str}': {e}"))?
};
let context_str = match output_obj.get("context") {
None => "0x",
Some(v) => v
.as_str()
.ok_or("Invalid context in output metadata: expected hex string")?,
};
let context_bytes = if context_str == "0x" || context_str.is_empty() {
Vec::new()
} else {
hex::decode(context_str.trim_start_matches("0x"))
.map_err(|e| format!("Invalid context hex '{context_str}': {e}"))?
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/solver-types/src/standards/eip7683.rs` around lines 754 - 763, The
code currently treats a present-but-non-string outputs[].context as "0x" which
silently changes reconstructed output context; update the extraction to
explicitly reject non-string types: instead of using
output_obj.get("context").and_then(|c| c.as_str()).unwrap_or("0x"), match on
output_obj.get("context") and if Some(value) then require value.as_str() to
return Some(str) or return an Err with a clear message (e.g., "outputs[].context
must be a string") so malformed types are rejected; keep the existing hex
decoding logic for the resulting string (refer to the
output_obj/context_str/context_bytes variables in this block).

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