Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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 @@ -76,11 +81,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
20 changes: 10 additions & 10 deletions crates/solver-account/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,28 @@ 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 }
solver-types = { path = "../solver-types" }
thiserror = "2.0.17"
toml = { workspace = true }
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 }
Copy link
Collaborator

Choose a reason for hiding this comment

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

We don't use it anymore


[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 @@ -103,6 +103,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 @@ -28,12 +28,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"] }
88 changes: 88 additions & 0 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 @@ -72,6 +76,11 @@ impl IntentHandler {
cost_profit_service: Arc<CostProfitService>,
dynamic_config: Arc<RwLock<Config>>,
) -> Self {
let static_config = tokio::task::block_in_place(|| dynamic_config.blocking_read().clone());
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())
{
// 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!("{:02x}", b)).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
6 changes: 3 additions & 3 deletions crates/solver-delivery/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -28,6 +31,3 @@ thiserror = "2.0.17"
tokio = { version = "1.0", features = ["rt-multi-thread"] }
toml = { workspace = true }
tracing = "0.1"

[features]
testing = ["mockall"]
2 changes: 1 addition & 1 deletion crates/solver-demo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,5 @@ uuid = { version = "1", features = ["v4"] }

[dev-dependencies]
tempfile = "3"
wiremock = "0.5"
tokio-test = "0.4"
wiremock = "0.5"
6 changes: 3 additions & 3 deletions crates/solver-order/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -20,8 +23,5 @@ toml = { workspace = true }
tracing = "0.1"
uuid = { version = "1.8", features = ["v4", "serde"] }

[features]
testing = ["mockall"]

[dev-dependencies]
tokio = { workspace = true }
7 changes: 3 additions & 4 deletions crates/solver-pricing/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -17,7 +20,3 @@ thiserror = "2.0"
tokio = { version = "1.0", features = ["sync", "time"] }
toml = { workspace = true }
tracing = "0.1"


[features]
testing = ["mockall"]
12 changes: 6 additions & 6 deletions crates/solver-service/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -55,17 +59,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"
1 change: 1 addition & 0 deletions crates/solver-service/src/config_merge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,7 @@ pub fn build_runtime_config(operator_config: &OperatorConfig) -> Result<Config,
commission_bps: operator_config.solver.commission_bps,
rate_buffer_bps: operator_config.solver.rate_buffer_bps,
monitoring_timeout_seconds: operator_config.solver.monitoring_timeout_seconds,
deny_list: None,
},
networks,
storage: build_storage_config_from_operator(&operator_config.solver_id),
Expand Down
6 changes: 3 additions & 3 deletions crates/solver-settlement/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ version = "0.1.0"
edition = "2021"
rust-version.workspace = true

[features]
testing = ["mockall"]

[dependencies]
alloy-primitives = { workspace = true }
alloy-provider = { workspace = true }
Expand All @@ -23,6 +26,3 @@ thiserror = "2.0"
tokio = { version = "1.0", features = ["rt-multi-thread", "sync"] }
toml = { workspace = true }
tracing = "0.1"

[features]
testing = ["mockall"]
10 changes: 5 additions & 5 deletions crates/solver-storage/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ version = "0.1.0"
edition = "2021"
rust-version.workspace = true

[features]
testing = ["mockall"]

[dependencies]
async-trait = "0.1"
chrono = { version = "0.4", features = ["serde"] }
Expand All @@ -25,11 +28,8 @@ toml = { workspace = true }
tracing = { workspace = true }
uuid = { workspace = true }

[features]
testing = ["mockall"]

[dev-dependencies]
tempfile = { workspace = true }
futures = { workspace = true }
rust_decimal = { workspace = true }
tempfile = { workspace = true }
uuid = { workspace = true }
rust_decimal = { workspace = true }
Loading