Skip to main content

chainrpc_core/
mev.rs

1//! MEV protection — route transactions through private relays.
2//!
3//! Supports Flashbots Protect and other MEV-protection RPC endpoints.
4
5use std::collections::HashMap;
6
7/// MEV protection configuration.
8#[derive(Debug, Clone)]
9pub struct MevConfig {
10    /// Whether MEV protection is enabled.
11    pub enabled: bool,
12    /// Private relay URL (e.g. Flashbots Protect).
13    pub relay_url: Option<String>,
14    /// Auto-detect MEV-susceptible transactions (swaps, liquidations).
15    pub auto_detect: bool,
16}
17
18impl Default for MevConfig {
19    fn default() -> Self {
20        Self {
21            enabled: false,
22            relay_url: None,
23            auto_detect: true,
24        }
25    }
26}
27
28/// Well-known MEV protection relay endpoints.
29pub fn relay_urls() -> HashMap<u64, &'static str> {
30    let mut urls = HashMap::new();
31    urls.insert(1, "https://rpc.flashbots.net"); // Ethereum mainnet
32    urls.insert(5, "https://rpc-goerli.flashbots.net"); // Goerli testnet
33    urls
34}
35
36/// Known function selectors that are MEV-susceptible.
37///
38/// These are common swap/trade selectors that front-runners target.
39const MEV_SUSCEPTIBLE_SELECTORS: &[&str] = &[
40    "0x38ed1739", // swapExactTokensForTokens (Uniswap V2)
41    "0x8803dbee", // swapTokensForExactTokens (Uniswap V2)
42    "0x7ff36ab5", // swapExactETHForTokens (Uniswap V2)
43    "0x18cbafe5", // swapExactTokensForETH (Uniswap V2)
44    "0x5ae401dc", // multicall (Uniswap V3 Router)
45    "0xac9650d8", // multicall (Uniswap V3 Router)
46    "0x04e45aaf", // exactInputSingle (Uniswap V3)
47    "0xb858183f", // exactInput (Uniswap V3)
48    "0x414bf389", // exactInputSingle (old Uniswap V3)
49    "0xc04b8d59", // exactInput (old Uniswap V3)
50    "0x2e1a7d4d", // withdraw (WETH — unwrap)
51    "0xd0e30db0", // deposit (WETH — wrap, used in sandwiches)
52];
53
54/// Check if transaction calldata appears MEV-susceptible.
55///
56/// `input` is the hex-encoded transaction input data (with or without 0x prefix).
57pub fn is_mev_susceptible(input: &str) -> bool {
58    let input = input.strip_prefix("0x").unwrap_or(input);
59    if input.len() < 8 {
60        return false;
61    }
62    let selector = format!("0x{}", &input[..8]);
63    MEV_SUSCEPTIBLE_SELECTORS.contains(&selector.as_str())
64}
65
66/// Determine if a transaction should be routed through the MEV relay.
67pub fn should_use_relay(config: &MevConfig, input: &str) -> bool {
68    if !config.enabled {
69        return false;
70    }
71    if config.relay_url.is_none() {
72        return false;
73    }
74    if config.auto_detect {
75        is_mev_susceptible(input)
76    } else {
77        // When auto_detect is off but relay is enabled, always use relay
78        true
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn detect_uniswap_v2_swap() {
88        // swapExactTokensForTokens selector
89        assert!(is_mev_susceptible("0x38ed1739000000000000000000000000"));
90    }
91
92    #[test]
93    fn detect_uniswap_v3_multicall() {
94        assert!(is_mev_susceptible("0x5ae401dc000000000000000000000000"));
95    }
96
97    #[test]
98    fn non_mev_transaction() {
99        // ERC20 transfer selector
100        assert!(!is_mev_susceptible("0xa9059cbb000000000000000000000000"));
101    }
102
103    #[test]
104    fn short_input() {
105        assert!(!is_mev_susceptible("0x"));
106        assert!(!is_mev_susceptible(""));
107        assert!(!is_mev_susceptible("0x1234"));
108    }
109
110    #[test]
111    fn should_use_relay_disabled() {
112        let config = MevConfig::default(); // enabled = false
113        assert!(!should_use_relay(&config, "0x38ed1739"));
114    }
115
116    #[test]
117    fn should_use_relay_enabled_auto() {
118        let config = MevConfig {
119            enabled: true,
120            relay_url: Some("https://rpc.flashbots.net".into()),
121            auto_detect: true,
122        };
123        assert!(should_use_relay(&config, "0x38ed1739")); // swap
124        assert!(!should_use_relay(&config, "0xa9059cbb")); // transfer
125    }
126
127    #[test]
128    fn should_use_relay_always_when_no_autodetect() {
129        let config = MevConfig {
130            enabled: true,
131            relay_url: Some("https://rpc.flashbots.net".into()),
132            auto_detect: false,
133        };
134        assert!(should_use_relay(&config, "0xa9059cbb")); // even transfer
135    }
136
137    #[test]
138    fn relay_urls_has_mainnet() {
139        let urls = relay_urls();
140        assert!(urls.contains_key(&1));
141    }
142
143    #[test]
144    fn without_0x_prefix() {
145        assert!(is_mev_susceptible("38ed1739000000000000000000000000"));
146    }
147}