1use crate::celo::transfer::{
6 CELO_TRANSFER_ADDRESS, CELO_TRANSFER_LABEL, PRECOMPILE_ID_CELO_TRANSFER,
7};
8use alloy_chains::{
9 Chain, NamedChain,
10 NamedChain::{Chiado, Gnosis, Moonbase, Moonbeam, MoonbeamDev, Moonriver, Rsk, RskTestnet},
11};
12use alloy_eips::eip1559::BaseFeeParams;
13use alloy_evm::precompiles::PrecompilesMap;
14use alloy_primitives::{Address, ChainId, map::AddressHashMap};
15use clap::Parser;
16use foundry_evm_hardforks::{FoundryHardfork, TempoHardfork};
17use serde::{Deserialize, Serialize};
18use std::collections::BTreeMap;
19use tempo_contracts::precompiles::{
20 ACCOUNT_KEYCHAIN_ADDRESS, ADDRESS_REGISTRY_ADDRESS, NONCE_PRECOMPILE_ADDRESS,
21 RECEIVE_POLICY_GUARD_ADDRESS, SIGNATURE_VERIFIER_ADDRESS, STABLECOIN_DEX_ADDRESS,
22 TIP_FEE_MANAGER_ADDRESS, TIP20_CHANNEL_RESERVE_ADDRESS, TIP20_FACTORY_ADDRESS,
23 TIP403_REGISTRY_ADDRESS, VALIDATOR_CONFIG_ADDRESS, VALIDATOR_CONFIG_V2_ADDRESS,
24};
25
26pub mod celo;
27
28#[cfg(feature = "optimism")]
29mod optimism;
30
31const TEMPO_PRECOMPILES: &[(&str, Address)] = &[
32 ("Nonce", NONCE_PRECOMPILE_ADDRESS),
33 ("StablecoinDex", STABLECOIN_DEX_ADDRESS),
34 ("TIP20Factory", TIP20_FACTORY_ADDRESS),
35 ("TIP403Registry", TIP403_REGISTRY_ADDRESS),
36 ("FeeManager", TIP_FEE_MANAGER_ADDRESS),
37 ("ValidatorConfig", VALIDATOR_CONFIG_ADDRESS),
38 ("ValidatorConfigV2", VALIDATOR_CONFIG_V2_ADDRESS),
39 ("AccountKeychain", ACCOUNT_KEYCHAIN_ADDRESS),
40 ("SignatureVerifier", SIGNATURE_VERIFIER_ADDRESS),
41 ("AddressRegistry", ADDRESS_REGISTRY_ADDRESS),
42 ("TIP20ChannelReserve", TIP20_CHANNEL_RESERVE_ADDRESS),
43 ("ReceivePolicyGuard", RECEIVE_POLICY_GUARD_ADDRESS),
44];
45
46pub const TEMPO_PRECOMPILE_ADDRESSES: &[Address] = &[
48 NONCE_PRECOMPILE_ADDRESS,
49 STABLECOIN_DEX_ADDRESS,
50 TIP20_FACTORY_ADDRESS,
51 TIP403_REGISTRY_ADDRESS,
52 TIP_FEE_MANAGER_ADDRESS,
53 VALIDATOR_CONFIG_ADDRESS,
54 VALIDATOR_CONFIG_V2_ADDRESS,
55 ACCOUNT_KEYCHAIN_ADDRESS,
56 SIGNATURE_VERIFIER_ADDRESS,
57 ADDRESS_REGISTRY_ADDRESS,
58 TIP20_CHANNEL_RESERVE_ADDRESS,
59 RECEIVE_POLICY_GUARD_ADDRESS,
60];
61
62pub fn is_tempo_precompile_active_at(address: Address, hardfork: TempoHardfork) -> bool {
64 if address == TIP20_CHANNEL_RESERVE_ADDRESS {
65 hardfork.is_t5()
66 } else if address == RECEIVE_POLICY_GUARD_ADDRESS {
67 hardfork.is_t6()
68 } else if address == ADDRESS_REGISTRY_ADDRESS || address == SIGNATURE_VERIFIER_ADDRESS {
69 hardfork.is_t3()
70 } else {
71 true
72 }
73}
74
75pub fn active_tempo_precompile_addresses(hardfork: TempoHardfork) -> impl Iterator<Item = Address> {
77 TEMPO_PRECOMPILE_ADDRESSES
78 .iter()
79 .copied()
80 .filter(move |&address| is_tempo_precompile_active_at(address, hardfork))
81}
82
83#[derive(
84 Clone,
85 Copy,
86 Debug,
87 Default,
88 PartialEq,
89 Eq,
90 PartialOrd,
91 Ord,
92 Hash,
93 Serialize,
94 Deserialize,
95 clap::ValueEnum,
96)]
97#[serde(rename_all = "lowercase")]
98#[clap(rename_all = "lowercase")]
99pub enum NetworkVariant {
100 #[default]
101 Ethereum,
102 #[cfg(feature = "optimism")]
103 Optimism,
104 Tempo,
105}
106
107impl std::str::FromStr for NetworkVariant {
108 type Err = String;
109
110 fn from_str(s: &str) -> Result<Self, Self::Err> {
111 match s {
112 "ethereum" => Ok(Self::Ethereum),
113 #[cfg(feature = "optimism")]
114 "optimism" => Ok(Self::Optimism),
115 "tempo" => Ok(Self::Tempo),
116 _ => Err(format!("unknown network variant: {s}")),
117 }
118 }
119}
120
121impl NetworkVariant {
122 pub const fn name(&self) -> &'static str {
123 match self {
124 Self::Ethereum => "ethereum",
125 #[cfg(feature = "optimism")]
126 Self::Optimism => "optimism",
127 Self::Tempo => "tempo",
128 }
129 }
130}
131
132impl std::fmt::Display for NetworkVariant {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 f.write_str(self.name())
135 }
136}
137
138impl From<ChainId> for NetworkVariant {
139 fn from(chain_id: ChainId) -> Self {
140 let chain = Chain::from_id(chain_id);
141 if chain.is_tempo() {
142 return Self::Tempo;
143 }
144 #[cfg(feature = "optimism")]
145 if chain.is_optimism() {
146 return Self::Optimism;
147 }
148 Self::Ethereum
149 }
150}
151
152#[derive(Clone, Debug, Default, Parser, Deserialize, Copy, PartialEq, Eq)]
153pub struct NetworkConfigs {
154 #[arg(help_heading = "Networks", long, short, num_args = 1, value_name = "NETWORK", value_enum, conflicts_with_all = ["celo", "tempo"])]
156 #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))]
157 #[serde(default)]
158 pub(crate) network: Option<NetworkVariant>,
159 #[arg(help_heading = "Networks", long, conflicts_with_all = ["network", "tempo"])]
161 #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))]
162 celo: bool,
163 #[cfg(feature = "optimism")]
165 #[arg(long, hide = true, conflicts_with_all = ["network", "celo", "tempo"])]
166 #[serde(default)]
169 pub(crate) optimism: bool,
170 #[arg(long, hide = true, conflicts_with_all = ["network", "celo"])]
172 #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))]
173 #[serde(default)]
176 tempo: bool,
177 #[arg(skip)]
179 #[serde(default)]
180 bypass_prevrandao: bool,
181}
182
183impl Serialize for NetworkConfigs {
188 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
189 use serde::ser::SerializeStruct;
190 let mut s = serializer.serialize_struct("NetworkConfigs", 3)?;
191 s.serialize_field("network", &self.resolved_network())?;
192 s.serialize_field("celo", &self.celo)?;
193 s.serialize_field("bypass_prevrandao", &self.bypass_prevrandao)?;
194 s.end()
195 }
196}
197
198impl NetworkConfigs {
199 pub fn with_celo() -> Self {
200 Self { celo: true, ..Default::default() }
201 }
202
203 pub fn with_tempo() -> Self {
204 Self { network: Some(NetworkVariant::Tempo), tempo: true, ..Default::default() }
205 }
206
207 pub const fn is_tempo(&self) -> bool {
208 matches!(self.resolved_network(), Some(NetworkVariant::Tempo))
209 }
210
211 pub const fn is_celo(&self) -> bool {
212 self.celo
213 }
214
215 pub const fn resolved_network(&self) -> Option<NetworkVariant> {
217 if let Some(n) = self.network {
218 return Some(n);
219 }
220 #[cfg(feature = "optimism")]
221 if self.optimism {
222 return Some(NetworkVariant::Optimism);
223 }
224 if self.tempo {
225 return Some(NetworkVariant::Tempo);
226 }
227 None
228 }
229
230 pub fn active_network_name(&self) -> Option<&'static str> {
232 self.resolved_network().and_then(|n| match n {
233 NetworkVariant::Ethereum => None,
234 _ => Some(n.name()),
235 })
236 }
237
238 pub fn base_fee_params(&self, timestamp: u64) -> BaseFeeParams {
243 #[cfg(feature = "optimism")]
244 if self.is_optimism() {
245 return self.op_base_fee_params(timestamp);
246 }
247 let _ = timestamp;
248 BaseFeeParams::ethereum()
249 }
250
251 pub fn bypass_prevrandao(&self, chain_id: u64) -> bool {
252 if let Ok(
253 Moonbeam | Moonbase | Moonriver | MoonbeamDev | Rsk | RskTestnet | Gnosis | Chiado,
254 ) = NamedChain::try_from(chain_id)
255 {
256 return true;
257 }
258 self.bypass_prevrandao
259 }
260
261 pub fn with_chain_id(self, chain_id: u64) -> Self {
262 let chain = Chain::from_id(chain_id);
263 if self.resolved_network().is_some() {
264 return if !self.celo
265 && matches!(chain.named(), Some(NamedChain::Celo | NamedChain::CeloSepolia))
266 {
267 Self::with_celo()
268 } else {
269 self
270 };
271 }
272 if chain.is_tempo() {
273 return Self::with_tempo();
274 }
275 #[cfg(feature = "optimism")]
276 if chain.is_optimism() {
277 return Self::with_optimism();
278 }
279 self
280 }
281
282 pub fn normalize_for_hardfork(self, hardfork: FoundryHardfork) -> Result<Self, String> {
287 if let Some(configured) =
288 self.active_network_name().filter(|&n| Some(n) != hardfork.namespace())
289 {
290 return Err(format!(
291 "hardfork `{}` conflicts with network config `{configured}`",
292 String::from(hardfork),
293 ));
294 }
295
296 let network = match hardfork {
297 FoundryHardfork::Ethereum(_) => self,
298 FoundryHardfork::Tempo(_) => Self::with_tempo(),
299 #[cfg(feature = "optimism")]
300 FoundryHardfork::Optimism(_) => Self::with_optimism(),
301 };
302
303 Ok(network)
304 }
305
306 pub fn inject_precompiles(self, precompiles: &mut PrecompilesMap) {
308 if self.celo {
309 precompiles.apply_precompile(&CELO_TRANSFER_ADDRESS, move |_| {
310 Some(celo::transfer::precompile())
311 });
312 }
313 }
314
315 pub fn precompiles_label(
317 self,
318 tempo_hardfork: Option<TempoHardfork>,
319 ) -> AddressHashMap<String> {
320 let mut labels = AddressHashMap::default();
321 if self.celo {
322 labels.insert(CELO_TRANSFER_ADDRESS, CELO_TRANSFER_LABEL.to_string());
323 }
324 if self.is_tempo() {
325 labels.extend(
326 TEMPO_PRECOMPILES
327 .iter()
328 .copied()
329 .filter(|(_, address)| {
330 tempo_hardfork.is_none_or(|hardfork| {
331 is_tempo_precompile_active_at(*address, hardfork)
332 })
333 })
334 .map(|(label, address)| (address, label.to_string())),
335 );
336 }
337 labels
338 }
339
340 pub fn precompiles(self, tempo_hardfork: Option<TempoHardfork>) -> BTreeMap<String, Address> {
342 let mut precompiles = BTreeMap::new();
343 if self.celo {
344 precompiles
345 .insert(PRECOMPILE_ID_CELO_TRANSFER.name().to_string(), CELO_TRANSFER_ADDRESS);
346 }
347 if self.is_tempo() {
348 precompiles.extend(
349 TEMPO_PRECOMPILES
350 .iter()
351 .copied()
352 .filter(|(_, address)| {
353 tempo_hardfork.is_none_or(|hardfork| {
354 is_tempo_precompile_active_at(*address, hardfork)
355 })
356 })
357 .map(|(label, address)| (label.to_string(), address)),
358 );
359 }
360 precompiles
361 }
362}
363
364impl From<NetworkVariant> for NetworkConfigs {
365 fn from(network: NetworkVariant) -> Self {
366 match network {
367 NetworkVariant::Ethereum => Self::default(),
368 NetworkVariant::Tempo => {
369 Self { network: Some(network), tempo: true, ..Default::default() }
370 }
371 #[cfg(feature = "optimism")]
372 NetworkVariant::Optimism => {
373 Self { network: Some(network), optimism: true, ..Default::default() }
374 }
375 }
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 #[test]
386 fn new_tempo_flag_equivalent_to_legacy() {
387 let via_new = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
388 let via_old = NetworkConfigs { tempo: true, ..Default::default() };
389 assert_eq!(via_new.is_tempo(), via_old.is_tempo());
390 assert_eq!(via_new.active_network_name(), via_old.active_network_name());
391 assert_eq!(via_new.precompiles(None), via_old.precompiles(None));
392 assert_eq!(via_new.precompiles_label(None), via_old.precompiles_label(None));
393 }
394
395 #[test]
396 fn canonical_tempo_network_reports_precompiles() {
397 let cfg = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
398
399 assert_eq!(
400 cfg.precompiles(None).get("TIP20ChannelReserve"),
401 Some(&TIP20_CHANNEL_RESERVE_ADDRESS)
402 );
403 assert!(!cfg.precompiles(Some(TempoHardfork::T4)).contains_key("TIP20ChannelReserve"));
404 assert!(!cfg.precompiles(Some(TempoHardfork::T4)).contains_key("ReceivePolicyGuard"));
405 assert!(!cfg.precompiles(Some(TempoHardfork::T2)).contains_key("AddressRegistry"));
406 assert!(!cfg.precompiles(Some(TempoHardfork::T2)).contains_key("SignatureVerifier"));
407 assert_eq!(
408 cfg.precompiles(Some(TempoHardfork::T3)).get("AddressRegistry"),
409 Some(&ADDRESS_REGISTRY_ADDRESS)
410 );
411 assert_eq!(
412 cfg.precompiles(Some(TempoHardfork::T3)).get("SignatureVerifier"),
413 Some(&SIGNATURE_VERIFIER_ADDRESS)
414 );
415 assert_eq!(
416 cfg.precompiles_label(Some(TempoHardfork::T5)).get(&TIP20_CHANNEL_RESERVE_ADDRESS),
417 Some(&"TIP20ChannelReserve".to_string())
418 );
419 assert!(cfg.precompiles_label(None).contains_key(&TIP20_CHANNEL_RESERVE_ADDRESS));
420 assert!(
421 !cfg.precompiles_label(Some(TempoHardfork::T5))
422 .contains_key(&RECEIVE_POLICY_GUARD_ADDRESS)
423 );
424 assert!(
425 cfg.precompiles_label(Some(TempoHardfork::T6))
426 .contains_key(&RECEIVE_POLICY_GUARD_ADDRESS)
427 );
428 }
429
430 #[test]
433 fn active_network_name_tempo() {
434 let cfg = NetworkConfigs::with_tempo();
435 assert_eq!(cfg.active_network_name(), Some("tempo"));
436 }
437
438 #[test]
439 fn active_network_name_default_is_none() {
440 assert_eq!(NetworkConfigs::default().active_network_name(), None);
441 }
442
443 #[test]
446 fn serde_roundtrip_tempo() {
447 let original = NetworkConfigs::with_tempo();
448 let json = serde_json::to_string(&original).unwrap();
449 let restored: NetworkConfigs = serde_json::from_str(&json).unwrap();
450 assert!(restored.is_tempo());
451 }
452
453 #[test]
454 fn serde_legacy_tempo_bool_deserialized() {
455 let json = r#"{"tempo": true, "celo": false, "bypass_prevrandao": false}"#;
457 let cfg: NetworkConfigs = serde_json::from_str(json).unwrap();
458 assert!(cfg.is_tempo());
459 }
460
461 #[test]
462 fn serde_serializes_legacy_alias_as_canonical_network() {
463 let cfg = NetworkConfigs { tempo: true, ..Default::default() };
466 let json = serde_json::to_value(cfg).unwrap();
467 assert_eq!(json["network"], serde_json::json!("tempo"));
468 assert!(json.get("tempo").is_none(), "legacy `tempo` key should not be serialized");
469 assert!(json.get("optimism").is_none(), "legacy `optimism` key should not be serialized");
470 }
471
472 #[test]
473 fn serde_new_network_field_deserialized() {
474 let json_tempo = r#"{"network": "tempo", "celo": false, "bypass_prevrandao": false}"#;
475 let cfg_tempo: NetworkConfigs = serde_json::from_str(json_tempo).unwrap();
476 assert!(cfg_tempo.is_tempo());
477 }
478
479 #[cfg(feature = "optimism")]
480 mod optimism {
481 use super::*;
482
483 #[test]
484 fn new_optimism_flag_equivalent_to_legacy() {
485 let via_new =
486 NetworkConfigs { network: Some(NetworkVariant::Optimism), ..Default::default() };
487 let via_old = NetworkConfigs { optimism: true, ..Default::default() };
488 assert_eq!(via_new.is_optimism(), via_old.is_optimism());
489 assert_eq!(via_new.is_tempo(), via_old.is_tempo());
490 assert_eq!(via_new.active_network_name(), via_old.active_network_name());
491 }
492
493 #[test]
494 fn active_network_name_optimism() {
495 let cfg = NetworkConfigs::with_optimism();
496 assert_eq!(cfg.active_network_name(), Some("optimism"));
497 }
498
499 #[test]
500 fn new_flag_wins_over_legacy_when_both_set() {
501 let cfg = NetworkConfigs {
503 network: Some(NetworkVariant::Optimism),
504 tempo: true,
505 ..Default::default()
506 };
507 assert!(cfg.is_optimism());
508 assert!(!cfg.is_tempo());
509 }
510
511 #[test]
512 fn serde_roundtrip_optimism() {
513 let original = NetworkConfigs::with_optimism();
514 let json = serde_json::to_string(&original).unwrap();
515 let restored: NetworkConfigs = serde_json::from_str(&json).unwrap();
516 assert!(restored.is_optimism());
517 assert!(!restored.is_tempo());
518 }
519
520 #[test]
521 fn serde_optimism_field_deserialized() {
522 let json_optimism =
523 r#"{"network": "optimism", "celo": false, "bypass_prevrandao": false}"#;
524 let cfg_optimism: NetworkConfigs = serde_json::from_str(json_optimism).unwrap();
525 assert!(cfg_optimism.is_optimism());
526 }
527 }
528}