diff --git a/Cargo.toml b/Cargo.toml index 9435b29f..ff33c4eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] @@ -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" @@ -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 diff --git a/crates/solver-account/Cargo.toml b/crates/solver-account/Cargo.toml index 6c4afbe3..454341ee 100644 --- a/crates/solver-account/Cargo.toml +++ b/crates/solver-account/Cargo.toml @@ -4,13 +4,22 @@ 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 } @@ -18,15 +27,7 @@ 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 } diff --git a/crates/solver-config/src/builders/config.rs b/crates/solver-config/src/builders/config.rs index 365ef33e..dfe4f03e 100644 --- a/crates/solver-config/src/builders/config.rs +++ b/crates/solver-config/src/builders/config.rs @@ -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 { diff --git a/crates/solver-config/src/lib.rs b/crates/solver-config/src/lib.rs index c8ab5bd7..702326fb 100644 --- a/crates/solver-config/src/lib.rs +++ b/crates/solver-config/src/lib.rs @@ -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, } /// Configuration for the storage backend. diff --git a/crates/solver-core/Cargo.toml b/crates/solver-core/Cargo.toml index 54330ebb..ef118c5a 100644 --- a/crates/solver-core/Cargo.toml +++ b/crates/solver-core/Cargo.toml @@ -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"] } \ No newline at end of file +solver-storage = { path = "../solver-storage", features = ["testing"] } diff --git a/crates/solver-core/src/engine/mod.rs b/crates/solver-core/src/engine/mod.rs index 7f109654..42af1a92 100644 --- a/crates/solver-core/src/engine/mod.rs +++ b/crates/solver-core/src/engine/mod.rs @@ -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( diff --git a/crates/solver-core/src/handlers/intent.rs b/crates/solver-core/src/handlers/intent.rs index 5424185e..25d51904 100644 --- a/crates/solver-core/src/handlers/intent.rs +++ b/crates/solver-core/src/handlers/intent.rs @@ -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; @@ -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>>, + /// Denied Ethereum addresses (lowercase hex with 0x prefix). + /// Loaded once at startup from `config.solver.deny_list` if set. + denied_addresses: HashSet, } impl IntentHandler { @@ -71,7 +75,12 @@ impl IntentHandler { token_manager: Arc, cost_profit_service: Arc, dynamic_config: Arc>, + 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, @@ -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 { + 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::>(&content) { + Ok(addrs) => { + let set: HashSet = + 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() + }, } } @@ -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::(intent.data.clone()) + { + // Check the order sender (user field). + let user_addr = order_data.user.to_lowercase(); + if self.denied_addresses.contains(&user_addr) { + 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 @@ -402,8 +490,9 @@ mod tests { Address(vec![0xab; 20]) } - fn create_test_config() -> Arc> { - Arc::new(RwLock::new(ConfigBuilder::new().build())) + fn create_test_config() -> (Arc>, Config) { + let config = ConfigBuilder::new().build(); + (Arc::new(RwLock::new(config.clone())), config) } fn create_mock_cost_profit_service() -> Arc { @@ -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, @@ -631,6 +720,7 @@ mod tests { token_manager, cost_profit_service, config, + &static_config, ); let result = handler.handle(intent).await; @@ -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, @@ -682,6 +772,7 @@ mod tests { token_manager, cost_profit_service, config, + &static_config, ); let result = handler.handle(intent).await; @@ -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(); @@ -752,6 +843,7 @@ mod tests { token_manager, cost_profit_service, config, + &static_config, ); let result = handler.handle(intent).await; @@ -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(); @@ -843,6 +935,7 @@ mod tests { token_manager, cost_profit_service, config, + &static_config, ); let result = handler.handle(intent).await; @@ -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(); @@ -934,6 +1027,7 @@ mod tests { token_manager, cost_profit_service, config, + &static_config, ); let result = handler.handle(intent).await; @@ -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(); @@ -985,6 +1079,7 @@ mod tests { token_manager, cost_profit_service, config, + &static_config, ); let result = handler.handle(intent).await; @@ -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(); @@ -1077,6 +1172,7 @@ mod tests { token_manager, cost_profit_service, config, + &static_config, ); // Handle intent and check events diff --git a/crates/solver-delivery/Cargo.toml b/crates/solver-delivery/Cargo.toml index f6438c81..80e53957 100644 --- a/crates/solver-delivery/Cargo.toml +++ b/crates/solver-delivery/Cargo.toml @@ -4,6 +4,9 @@ version = "0.1.0" edition = "2021" rust-version.workspace = true +[features] +testing = ["mockall"] + [dependencies] alloy-consensus = { workspace = true } alloy-network = { workspace = true } @@ -27,6 +30,3 @@ solver-types = { path = "../solver-types" } thiserror = "2.0.17" tokio = { version = "1.0", features = ["rt-multi-thread"] } tracing = "0.1" - -[features] -testing = ["mockall"] \ No newline at end of file diff --git a/crates/solver-demo/Cargo.toml b/crates/solver-demo/Cargo.toml index 0a39f60e..c1bfdc64 100644 --- a/crates/solver-demo/Cargo.toml +++ b/crates/solver-demo/Cargo.toml @@ -52,5 +52,5 @@ uuid = { version = "1", features = ["v4"] } [dev-dependencies] tempfile = "3" -wiremock = "0.5" tokio-test = "0.4" +wiremock = "0.5" diff --git a/crates/solver-order/Cargo.toml b/crates/solver-order/Cargo.toml index bd88f8c2..e3d40e07 100644 --- a/crates/solver-order/Cargo.toml +++ b/crates/solver-order/Cargo.toml @@ -4,6 +4,9 @@ version = "0.1.0" edition = "2021" rust-version.workspace = true +[features] +testing = ["mockall"] + [dependencies] alloy-dyn-abi = { workspace = true } alloy-primitives = { workspace = true } @@ -19,8 +22,5 @@ thiserror = "2.0" tracing = "0.1" uuid = { version = "1.8", features = ["v4", "serde"] } -[features] -testing = ["mockall"] - [dev-dependencies] tokio = { workspace = true } diff --git a/crates/solver-pricing/Cargo.toml b/crates/solver-pricing/Cargo.toml index bda58bea..8dc75e17 100644 --- a/crates/solver-pricing/Cargo.toml +++ b/crates/solver-pricing/Cargo.toml @@ -4,6 +4,9 @@ version = "0.1.0" edition = "2021" rust-version.workspace = true +[features] +testing = ["mockall"] + [dependencies] alloy-primitives = { workspace = true } async-trait = "0.1.73" @@ -16,7 +19,3 @@ solver-types = { path = "../solver-types" } thiserror = "2.0" tokio = { version = "1.0", features = ["sync", "time"] } tracing = "0.1" - - -[features] -testing = ["mockall"] \ No newline at end of file diff --git a/crates/solver-service/Cargo.toml b/crates/solver-service/Cargo.toml index a4119c02..84ac40b5 100644 --- a/crates/solver-service/Cargo.toml +++ b/crates/solver-service/Cargo.toml @@ -13,6 +13,10 @@ path = "src/lib.rs" name = "solver" path = "src/main.rs" +[features] +default = [] +kms = ["solver-account/kms"] + [dependencies] alloy-primitives = { workspace = true, features = ["std", "serde"] } alloy-signer = { workspace = true } @@ -54,17 +58,13 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } uuid = { version = "1.0", features = ["v4", "serde"] } -[features] -default = [] -kms = ["solver-account/kms"] - [dev-dependencies] alloy-signer-local = { workspace = true } mockall = { workspace = true } +serial_test = "3" +solver-delivery = { path = "../solver-delivery", features = ["testing"] } solver-settlement = { path = "../solver-settlement", features = ["testing"] } solver-storage = { path = "../solver-storage", features = ["testing"] } -solver-delivery = { path = "../solver-delivery", features = ["testing"] } -serial_test = "3" tempfile = { workspace = true } tokio = { workspace = true } wiremock = "0.5" diff --git a/crates/solver-service/src/config_merge.rs b/crates/solver-service/src/config_merge.rs index 8ebf5d53..2d635675 100644 --- a/crates/solver-service/src/config_merge.rs +++ b/crates/solver-service/src/config_merge.rs @@ -845,6 +845,7 @@ pub fn build_runtime_config(operator_config: &OperatorConfig) -> Result