-
Notifications
You must be signed in to change notification settings - Fork 19
feat: Denylist #308
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: Denylist #308
Changes from all commits
2364b4d
070e2d5
41083a6
7fa3e77
d014b73
12e4dbe
8506954
94cd7cf
7b147cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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<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 { | ||||||||||||||
|
|
@@ -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, | ||||||||||||||
|
|
@@ -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() | ||||||||||||||
| }, | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -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) { | ||||||||||||||
|
Comment on lines
+181
to
+183
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. User address may lack 0x prefix, causing match failure. The deny list stores addresses with 🐛 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| 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<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> { | ||||||||||||||
|
|
@@ -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 | ||||||||||||||
|
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Silent skip on parse failure may bypass sanctions enforcement.
If
intent.datacannot be parsed asEip7683OrderData(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
🤖 Prompt for AI Agents