Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
[workspace]
resolver = "2"
members = [
"crates/solver-types",
"crates/solver-core",
"crates/solver-config",
"crates/solver-storage",
"crates/solver-account",
"crates/solver-config",
"crates/solver-core",
"crates/solver-delivery",
"crates/solver-demo",
"crates/solver-discovery",
"crates/solver-order",
"crates/solver-settlement",
"crates/solver-pricing",
"crates/solver-service",
"crates/solver-demo",
"crates/solver-settlement",
"crates/solver-storage",
"crates/solver-types",
]
default-members = ["crates/solver-service", "crates/solver-demo"]

Expand Down Expand Up @@ -48,6 +48,11 @@ anyhow = "1.0"
arc-swap = "1.7"
async-stream = "0.3"
async-trait = "0.1"

# AWS dependencies (for KMS signer)
# Minimum versions that fix CVE GHSA-g59m-gf8j-gjf5 (region validation flaw)
aws-config = "1.8"
aws-sdk-kms = "1.93"
axum = "0.8.4"
backoff = { version = "0.4", features = ["tokio"] }
bytes = "1.8"
Expand Down Expand Up @@ -75,11 +80,6 @@ tracing-subscriber = "0.3"
uuid = { version = "1.10", features = ["v4", "serde"] }
validator = { version = "0.20", features = ["derive"] }

# AWS dependencies (for KMS signer)
# Minimum versions that fix CVE GHSA-g59m-gf8j-gjf5 (region validation flaw)
aws-config = "1.8"
aws-sdk-kms = "1.93"

[profile.release]
opt-level = 3
lto = true
Expand Down
19 changes: 10 additions & 9 deletions crates/solver-account/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,30 @@ version = "0.1.0"
edition = "2021"
rust-version.workspace = true

[features]
testing = ["mockall"]
kms = ["alloy-signer-aws", "aws-config", "aws-sdk-kms"]

[dependencies]
alloy-consensus = { workspace = true }
alloy-network = { workspace = true }
alloy-primitives = { workspace = true }
alloy-signer = { workspace = true }

# KMS dependencies (optional)
alloy-signer-aws = { workspace = true, optional = true }
alloy-signer-local = { workspace = true }
async-trait = "0.1"
aws-config = { workspace = true, optional = true }
aws-sdk-kms = { workspace = true, optional = true }
hex = "0.4"
mockall = { workspace = true, optional = true }
serde = { workspace = true }
serde_json = { workspace = true }
solver-types = { path = "../solver-types" }
thiserror = "2.0.17"
tokio = { workspace = true }

# KMS dependencies (optional)
alloy-signer-aws = { workspace = true, optional = true }
aws-config = { workspace = true, optional = true }
aws-sdk-kms = { workspace = true, optional = true }

[features]
testing = ["mockall"]
kms = ["alloy-signer-aws", "aws-config", "aws-sdk-kms"]
toml = { workspace = true }

[dev-dependencies]
tokio = { workspace = true }
1 change: 1 addition & 0 deletions crates/solver-config/src/builders/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ impl ConfigBuilder {
commission_bps: self.commission_bps,
rate_buffer_bps: self.rate_buffer_bps,
monitoring_timeout_seconds: self.monitoring_timeout_seconds,
deny_list: None,
},
networks: self.networks.unwrap_or_default(),
storage: StorageConfig {
Expand Down
5 changes: 5 additions & 0 deletions crates/solver-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ pub struct SolverConfig {
/// Defaults to 28800 seconds (8 hours) if not specified.
#[serde(default = "default_monitoring_timeout_seconds")]
pub monitoring_timeout_seconds: u64,
/// Optional path to a JSON file containing denied Ethereum addresses.
/// The file must contain a JSON array of lowercase hex strings (e.g. ["0xabc...", ...]).
/// When set, any intent whose sender or recipient appears in the list is silently dropped.
#[serde(default)]
pub deny_list: Option<String>,
}

/// Configuration for the storage backend.
Expand Down
8 changes: 4 additions & 4 deletions crates/solver-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ tracing = "0.1"

[dev-dependencies]
alloy-signer-local = { workspace = true }
mockall = { workspace = true }
async-trait = { workspace = true }
mockall = { workspace = true }
solver-account = { path = "../solver-account", features = ["testing"] }
# Add testing feature only for tests
solver-delivery = { path = "../solver-delivery", features = ["testing"] }
solver-storage = { path = "../solver-storage", features = ["testing"] }
solver-order = { path = "../solver-order", features = ["testing"] }
solver-account = { path = "../solver-account", features = ["testing"] }
solver-pricing = { path = "../solver-pricing", features = ["testing"] }
solver-settlement = { path = "../solver-settlement", features = ["testing"] }
solver-pricing = { path = "../solver-pricing", features = ["testing"] }
solver-storage = { path = "../solver-storage", features = ["testing"] }
1 change: 1 addition & 0 deletions crates/solver-core/src/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ impl SolverEngine {
token_manager.clone(),
cost_profit_service,
dynamic_config.clone(), // Pass dynamic config for hot-reload support
&static_config, // Pass static config for deny list loading
));

let order_handler = Arc::new(OrderHandler::new(
Expand Down
114 changes: 105 additions & 9 deletions crates/solver-core/src/handlers/intent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use solver_types::{
truncate_id, with_0x_prefix, Address, DiscoveryEvent, Eip7683OrderData, ExecutionDecision,
ExecutionParams, Intent, OrderEvent, SolverEvent, StorageKey,
};
use std::collections::HashSet;
use std::num::NonZeroUsize;
use std::sync::Arc;
use thiserror::Error;
Expand Down Expand Up @@ -57,6 +58,9 @@ pub struct IntentHandler {
/// In-memory LRU cache for fast intent deduplication to prevent race conditions
/// Automatically evicts oldest entries when capacity is exceeded
processed_intents: Arc<RwLock<LruCache<String, ()>>>,
/// Denied Ethereum addresses (lowercase hex with 0x prefix).
/// Loaded once at startup from `config.solver.deny_list` if set.
denied_addresses: HashSet<String>,
}

impl IntentHandler {
Expand All @@ -71,7 +75,12 @@ impl IntentHandler {
token_manager: Arc<TokenManager>,
cost_profit_service: Arc<CostProfitService>,
dynamic_config: Arc<RwLock<Config>>,
static_config: &Config,
) -> Self {
let denied_addresses = Self::load_deny_list(static_config.solver.deny_list.as_deref());
if denied_addresses.is_empty() {
tracing::warn!("Deny List could not be loaded. Enforcement is DISABLED!");
}
Self {
order_service,
storage,
Expand All @@ -85,6 +94,40 @@ impl IntentHandler {
processed_intents: Arc::new(RwLock::new(LruCache::new(
NonZeroUsize::new(10000).unwrap(),
))),
denied_addresses,
}
}

/// Load denied addresses from a JSON file.
///
/// Returns an empty set if no path is configured, the file is missing,
/// or the file cannot be parsed. All addresses are stored in lowercase.
fn load_deny_list(path: Option<&str>) -> HashSet<String> {
let path = match path {
Some(p) if !p.is_empty() => p,
_ => return HashSet::new(),
};
match std::fs::read_to_string(path) {
Ok(content) => match serde_json::from_str::<Vec<String>>(&content) {
Ok(addrs) => {
let set: HashSet<String> =
addrs.into_iter().map(|a| a.to_lowercase()).collect();
tracing::info!(
path = %path,
count = %set.len(),
"Deny list was found"
);
set
},
Err(e) => {
tracing::warn!(path = %path, error = %e, "Failed to parse deny list");
HashSet::new()
},
},
Err(e) => {
tracing::warn!(path = %path, error = %e, "Failed to read deny list");
HashSet::new()
},
}
}

Expand Down Expand Up @@ -130,6 +173,51 @@ impl IntentHandler {
return Ok(());
}

// Deny list check — runs before storing to avoid polluting the dedup cache
// with addresses that will always be rejected.
if !self.denied_addresses.is_empty() {
if let Ok(order_data) = serde_json::from_value::<Eip7683OrderData>(intent.data.clone())
{
Comment on lines +179 to +180
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

Silent skip on parse failure may bypass sanctions enforcement.

If intent.data cannot be parsed as Eip7683OrderData (malformed data, different standard), the deny list check is silently skipped. For compliance-critical functionality, consider logging a warning when parsing fails so operators have visibility.

🛡️ Suggested improvement
-		if let Ok(order_data) = serde_json::from_value::<Eip7683OrderData>(intent.data.clone())
-		{
+		match serde_json::from_value::<Eip7683OrderData>(intent.data.clone()) {
+			Ok(order_data) => {
 			// Check the order sender (user field).
 			// ... existing deny list logic ...
+			}
+			Err(e) => {
+				tracing::debug!(
+					intent_id = %intent.id,
+					error = %e,
+					"Could not parse intent data for deny list check, skipping"
+				);
+			}
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/solver-core/src/handlers/intent.rs` around lines 179 - 180, The code
currently uses serde_json::from_value::<Eip7683OrderData>(intent.data.clone())
with an if let Ok(...) so parsing failures are silently skipped; change this to
handle the Err case by logging a warning that includes the parse error and
identifying info (e.g., intent id or intent.raw data) before skipping the
deny-list check. Locate the parsing site (the
from_value::<Eip7683OrderData>(intent.data.clone()) call) and replace the if let
with a match or if let Err arm that emits a warning via the existing logger
(e.g., process_logger, tracing, or the module's logger) including the error and
context so operators can observe malformed or non-EIP-7683 data. Ensure the
normal deny-list logic remains unchanged for the Ok branch.

// Check the order sender (user field).
let user_addr = order_data.user.to_lowercase();
if self.denied_addresses.contains(&user_addr) {
Comment on lines +181 to +183
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

User address may lack 0x prefix, causing match failure.

The deny list stores addresses with 0x prefix (per line 61-62 documentation), but order_data.user is lowercased without normalization. If the user field doesn't include the 0x prefix, it will never match a deny list entry. The with_0x_prefix utility is already used elsewhere in this file (line 226).

🐛 Proposed fix
 			// Check the order sender (user field).
-			let user_addr = order_data.user.to_lowercase();
+			let user_addr = with_0x_prefix(&order_data.user).to_lowercase();
 			if self.denied_addresses.contains(&user_addr) {
📝 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
// Check the order sender (user field).
let user_addr = order_data.user.to_lowercase();
if self.denied_addresses.contains(&user_addr) {
// Check the order sender (user field).
let user_addr = with_0x_prefix(&order_data.user).to_lowercase();
if self.denied_addresses.contains(&user_addr) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/solver-core/src/handlers/intent.rs` around lines 181 - 183, The
deny-list check is comparing self.denied_addresses (which store addresses with
"0x" prefix) against order_data.user after only lowercasing it, so addresses
missing the "0x" prefix won't match; update the check to normalize the user
address the same way used elsewhere by calling
with_0x_prefix(order_data.user.to_lowercase()) (or equivalent helper) before
testing contains on self.denied_addresses so the comparison uses the canonical
"0x"-prefixed, lowercased form.

tracing::warn!(
intent_id = %intent.id,
address = %user_addr,
"Intent rejected: sender is on deny list"
);
self.event_bus
.publish(SolverEvent::Discovery(DiscoveryEvent::IntentRejected {
intent_id: intent.id,
reason: "Sender address is on deny list".to_string(),
}))
.ok();
return Ok(());
}
// Check every output recipient.
for output in &order_data.outputs {
// recipient is bytes32; the Ethereum address occupies the last 20 bytes.
let addr_bytes = &output.recipient[12..];
let hex_str: String = addr_bytes.iter().map(|b| format!("{b:02x}")).collect();
let recipient_addr = format!("0x{hex_str}");
if self.denied_addresses.contains(&recipient_addr) {
tracing::warn!(
intent_id = %intent.id,
address = %recipient_addr,
"Intent rejected: recipient is on deny list"
);
self.event_bus
.publish(SolverEvent::Discovery(DiscoveryEvent::IntentRejected {
intent_id: intent.id,
reason: "Recipient address is on deny list".to_string(),
}))
.ok();
return Ok(());
}
}
}
}

// Store intent immediately to prevent race conditions with duplicate discovery
// This claims the intent ID slot before we start the potentially slow validation process
self.storage
Expand Down Expand Up @@ -402,8 +490,9 @@ mod tests {
Address(vec![0xab; 20])
}

fn create_test_config() -> Arc<RwLock<Config>> {
Arc::new(RwLock::new(ConfigBuilder::new().build()))
fn create_test_config() -> (Arc<RwLock<Config>>, Config) {
let config = ConfigBuilder::new().build();
(Arc::new(RwLock::new(config.clone())), config)
}

fn create_mock_cost_profit_service() -> Arc<CostProfitService> {
Expand Down Expand Up @@ -619,7 +708,7 @@ mod tests {
))),
));
let cost_profit_service = create_mock_cost_profit_service();
let config = create_test_config();
let (config, static_config) = create_test_config();

let handler = IntentHandler::new(
order_service,
Expand All @@ -631,6 +720,7 @@ mod tests {
token_manager,
cost_profit_service,
config,
&static_config,
);

let result = handler.handle(intent).await;
Expand Down Expand Up @@ -670,7 +760,7 @@ mod tests {
))),
));
let cost_profit_service = create_mock_cost_profit_service();
let config = create_test_config();
let (config, static_config) = create_test_config();

let handler = IntentHandler::new(
order_service,
Expand All @@ -682,6 +772,7 @@ mod tests {
token_manager,
cost_profit_service,
config,
&static_config,
);

let result = handler.handle(intent).await;
Expand Down Expand Up @@ -738,7 +829,7 @@ mod tests {
MockAccountInterface::new(),
))),
));
let config = create_test_config();
let (config, static_config) = create_test_config();

let cost_profit_service = create_mock_cost_profit_service();

Expand All @@ -752,6 +843,7 @@ mod tests {
token_manager,
cost_profit_service,
config,
&static_config,
);

let result = handler.handle(intent).await;
Expand Down Expand Up @@ -829,7 +921,7 @@ mod tests {
MockAccountInterface::new(),
))),
));
let config = create_test_config();
let (config, static_config) = create_test_config();

let cost_profit_service = create_mock_cost_profit_service();

Expand All @@ -843,6 +935,7 @@ mod tests {
token_manager,
cost_profit_service,
config,
&static_config,
);

let result = handler.handle(intent).await;
Expand Down Expand Up @@ -920,7 +1013,7 @@ mod tests {
MockAccountInterface::new(),
))),
));
let config = create_test_config();
let (config, static_config) = create_test_config();

let cost_profit_service = create_mock_cost_profit_service();

Expand All @@ -934,6 +1027,7 @@ mod tests {
token_manager,
cost_profit_service,
config,
&static_config,
);

let result = handler.handle(intent).await;
Expand Down Expand Up @@ -971,7 +1065,7 @@ mod tests {
MockAccountInterface::new(),
))),
));
let config = create_test_config();
let (config, static_config) = create_test_config();

let cost_profit_service = create_mock_cost_profit_service();

Expand All @@ -985,6 +1079,7 @@ mod tests {
token_manager,
cost_profit_service,
config,
&static_config,
);

let result = handler.handle(intent).await;
Expand Down Expand Up @@ -1060,7 +1155,7 @@ mod tests {
MockAccountInterface::new(),
))),
));
let config = create_test_config();
let (config, static_config) = create_test_config();

// Subscribe to events before creating handler
let mut receiver = event_bus.subscribe();
Expand All @@ -1077,6 +1172,7 @@ mod tests {
token_manager,
cost_profit_service,
config,
&static_config,
);

// Handle intent and check events
Expand Down
Loading
Loading