1use std::path::{Path, PathBuf};
10
11use alloy_primitives::{Address, Bytes};
12use alloy_sol_types::{SolType, SolValue, abi::TokenSeq};
13use anyhow::{Context, Result, bail};
14use tracing::{debug, info};
15
16use crate::cache::{EvmCache, MissingTargetBehavior};
17
18#[derive(Debug, Clone)]
20pub struct FoundryArtifact {
21 pub path: PathBuf,
23 pub creation_code: Bytes,
25}
26
27impl FoundryArtifact {
28 pub fn load(path: impl AsRef<Path>) -> Result<Self> {
43 let path = path.as_ref();
44 let creation_code = load_foundry_creation_code(path)?;
45 Ok(Self {
46 path: path.to_path_buf(),
47 creation_code,
48 })
49 }
50
51 pub fn init_code(&self, constructor_args: impl AsRef<[u8]>) -> Bytes {
54 build_init_code(&self.creation_code, constructor_args)
55 }
56
57 pub fn deploy(
75 &self,
76 cache: &mut EvmCache,
77 deployer: Address,
78 constructor_args: impl AsRef<[u8]>,
79 ) -> Result<Address> {
80 let init_code = self.init_code(constructor_args);
81 let deployed = cache
82 .deploy_contract(deployer, init_code)
83 .with_context(|| format!("deploying Foundry artifact {}", self.path.display()))?;
84 debug!(
85 artifact = %self.path.display(),
86 %deployer,
87 %deployed,
88 "deployed Foundry artifact into EVM cache"
89 );
90 Ok(deployed)
91 }
92
93 pub fn etch(
115 &self,
116 cache: &mut EvmCache,
117 target: Address,
118 deployer: Address,
119 constructor_args: impl AsRef<[u8]>,
120 ) -> Result<EtchedContract> {
121 self.etch_with_missing_target_behavior(
122 cache,
123 target,
124 deployer,
125 constructor_args,
126 MissingTargetBehavior::Error,
127 )
128 }
129
130 pub fn etch_or_create(
151 &self,
152 cache: &mut EvmCache,
153 target: Address,
154 deployer: Address,
155 constructor_args: impl AsRef<[u8]>,
156 ) -> Result<EtchedContract> {
157 self.etch_with_missing_target_behavior(
158 cache,
159 target,
160 deployer,
161 constructor_args,
162 MissingTargetBehavior::Create,
163 )
164 }
165
166 fn etch_with_missing_target_behavior(
167 &self,
168 cache: &mut EvmCache,
169 target: Address,
170 deployer: Address,
171 constructor_args: impl AsRef<[u8]>,
172 missing_target: MissingTargetBehavior,
173 ) -> Result<EtchedContract> {
174 let snapshot = cache.snapshot();
175 let result = self.try_etch_with_missing_target_behavior(
176 cache,
177 target,
178 deployer,
179 constructor_args,
180 missing_target,
181 );
182
183 if result.is_err() {
184 cache.restore(snapshot);
185 }
186
187 result
188 }
189
190 fn try_etch_with_missing_target_behavior(
191 &self,
192 cache: &mut EvmCache,
193 target: Address,
194 deployer: Address,
195 constructor_args: impl AsRef<[u8]>,
196 missing_target: MissingTargetBehavior,
197 ) -> Result<EtchedContract> {
198 if matches!(missing_target, MissingTargetBehavior::Error) {
199 cache
200 .require_contract_target(target)
201 .with_context(|| format!("validating target contract {}", target))?;
202 }
203
204 let deployed = self.deploy(cache, deployer, constructor_args)?;
205 cache
206 .override_account_code_with_missing_target(deployed, target, missing_target)
207 .with_context(|| format!("etching runtime bytecode at {}", target))?;
208
209 let code_size = cache
210 .db_mut()
211 .cache
212 .accounts
213 .get(&target)
214 .and_then(|account| account.info.code.as_ref().map(|code| code.len()))
215 .unwrap_or_default();
216
217 info!(
218 artifact = %self.path.display(),
219 %deployed,
220 %target,
221 code_size,
222 "etched Foundry artifact runtime bytecode"
223 );
224
225 Ok(EtchedContract {
226 artifact_path: self.path.clone(),
227 deployed_address: deployed,
228 target_address: target,
229 code_size,
230 })
231 }
232}
233
234#[derive(Debug, Clone, PartialEq, Eq)]
236pub struct EtchedContract {
237 pub artifact_path: PathBuf,
239 pub deployed_address: Address,
241 pub target_address: Address,
243 pub code_size: usize,
245}
246
247pub fn encode_constructor_args<T>(args: T) -> Bytes
274where
275 T: SolValue,
276 for<'a> <T::SolType as SolType>::Token<'a>: TokenSeq<'a>,
277{
278 args.abi_encode_params().into()
279}
280
281pub fn load_foundry_creation_code(path: impl AsRef<Path>) -> Result<Bytes> {
296 let path = path.as_ref();
297 let content = std::fs::read_to_string(path)
298 .with_context(|| format!("failed to read Foundry artifact at {}", path.display()))?;
299 let json: serde_json::Value = serde_json::from_str(&content).with_context(|| {
300 format!(
301 "failed to parse Foundry artifact JSON at {}",
302 path.display()
303 )
304 })?;
305
306 let bytecode = json
307 .get("bytecode")
308 .ok_or_else(|| anyhow::anyhow!("artifact {} has no `bytecode` field", path.display()))?;
309
310 let bytecode_hex = bytecode
311 .get("object")
312 .and_then(serde_json::Value::as_str)
313 .or_else(|| bytecode.as_str())
314 .ok_or_else(|| {
315 anyhow::anyhow!(
316 "artifact {} has no `bytecode.object` string",
317 path.display()
318 )
319 })?;
320
321 decode_hex_bytecode(bytecode_hex)
322 .with_context(|| format!("failed to decode bytecode in {}", path.display()))
323}
324
325pub fn build_init_code(
340 creation_code: impl AsRef<[u8]>,
341 constructor_args: impl AsRef<[u8]>,
342) -> Bytes {
343 let creation_code = creation_code.as_ref();
344 let constructor_args = constructor_args.as_ref();
345 let mut init_code = Vec::with_capacity(creation_code.len() + constructor_args.len());
346 init_code.extend_from_slice(creation_code);
347 init_code.extend_from_slice(constructor_args);
348 Bytes::from(init_code)
349}
350
351pub fn deploy_foundry_artifact(
365 cache: &mut EvmCache,
366 artifact_path: impl AsRef<Path>,
367 deployer: Address,
368 constructor_args: impl AsRef<[u8]>,
369) -> Result<Address> {
370 FoundryArtifact::load(artifact_path)?.deploy(cache, deployer, constructor_args)
371}
372
373pub fn etch_foundry_artifact(
392 cache: &mut EvmCache,
393 target: Address,
394 artifact_path: impl AsRef<Path>,
395 deployer: Address,
396 constructor_args: impl AsRef<[u8]>,
397) -> Result<EtchedContract> {
398 FoundryArtifact::load(artifact_path)?.etch(cache, target, deployer, constructor_args)
399}
400
401pub fn etch_foundry_artifact_or_create(
420 cache: &mut EvmCache,
421 target: Address,
422 artifact_path: impl AsRef<Path>,
423 deployer: Address,
424 constructor_args: impl AsRef<[u8]>,
425) -> Result<EtchedContract> {
426 FoundryArtifact::load(artifact_path)?.etch_or_create(cache, target, deployer, constructor_args)
427}
428
429fn decode_hex_bytecode(bytecode_hex: &str) -> Result<Bytes> {
430 let stripped = bytecode_hex.strip_prefix("0x").unwrap_or(bytecode_hex);
431 if stripped.is_empty() {
432 bail!("empty bytecode");
433 }
434 if stripped.contains("__") {
435 bail!("bytecode contains unresolved library placeholders");
436 }
437
438 alloy_primitives::hex::decode(stripped)
439 .map(Bytes::from)
440 .context("invalid hex bytecode")
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446 use alloy_primitives::U256;
447 use revm::state::{AccountInfo, Bytecode};
448 use std::{
449 sync::Arc,
450 time::{SystemTime, UNIX_EPOCH},
451 };
452
453 #[test]
454 fn build_init_code_appends_constructor_args() {
455 let init = build_init_code([0x60, 0x80], [0x01, 0x02, 0x03]);
456 assert_eq!(init.as_ref(), &[0x60, 0x80, 0x01, 0x02, 0x03]);
457 }
458
459 #[test]
460 fn encode_constructor_args_uses_parameter_encoding_for_dynamic_args() {
461 let args = (String::from("hello"), U256::from(7));
462 let encoded = encode_constructor_args(args.clone());
463
464 assert_eq!(encoded.as_ref(), args.abi_encode_params().as_slice());
465 assert_ne!(encoded.as_ref(), args.abi_encode().as_slice());
466 }
467
468 #[test]
469 fn encode_constructor_args_empty_tuple_is_empty() {
470 assert!(encode_constructor_args(()).is_empty());
471 }
472
473 #[test]
474 fn strict_override_rejects_known_empty_target() -> Result<()> {
475 let mut cache = setup_cache();
476 let source = Address::repeat_byte(0x11);
477 let target = Address::repeat_byte(0x22);
478
479 cache
480 .db_mut()
481 .insert_account_info(source, account_with_runtime(&[0x00], U256::ZERO, 1));
482 cache
483 .db_mut()
484 .insert_account_info(target, AccountInfo::default());
485
486 let err = cache.override_account_code(source, target).unwrap_err();
487 assert!(err.to_string().contains("target account"));
488 assert!(err.to_string().contains("no runtime bytecode"));
489
490 let target_account = cache
491 .db_mut()
492 .cache
493 .accounts
494 .get(&target)
495 .expect("target should still exist");
496 assert!(
497 target_account
498 .info
499 .code
500 .as_ref()
501 .is_none_or(|code| code.is_empty())
502 );
503
504 cache.override_or_create_account_code(source, target)?;
505 let target_account = cache
506 .db_mut()
507 .cache
508 .accounts
509 .get(&target)
510 .expect("explicit create should write target code");
511 assert!(
512 target_account
513 .info
514 .code
515 .as_ref()
516 .is_some_and(|code| !code.is_empty())
517 );
518
519 Ok(())
520 }
521
522 #[test]
523 fn strict_etch_rejects_empty_target_before_deploying() -> Result<()> {
524 let mut cache = setup_cache();
525 let deployer = Address::ZERO;
526 let target = Address::repeat_byte(0x33);
527 let create_address = zero_nonce_create_address()?;
528 let artifact = memory_artifact(non_empty_runtime_creation_code());
529
530 cache
531 .db_mut()
532 .insert_account_info(deployer, AccountInfo::default());
533 cache
534 .db_mut()
535 .insert_account_info(create_address, AccountInfo::default());
536 cache
537 .db_mut()
538 .insert_account_info(target, AccountInfo::default());
539
540 let err = artifact
541 .etch(&mut cache, target, deployer, Bytes::new())
542 .unwrap_err();
543 let err = format!("{err:#}");
544 assert!(err.contains("validating target contract"));
545 assert!(err.contains("no runtime bytecode"));
546
547 assert_eq!(cached_nonce(&mut cache, deployer), Some(0));
548 assert_eq!(cached_code_len(&mut cache, create_address), Some(0));
549
550 Ok(())
551 }
552
553 #[test]
554 fn etch_restores_cache_when_override_fails_after_deploy() -> Result<()> {
555 let mut cache = setup_cache();
556 let deployer = Address::ZERO;
557 let target = Address::repeat_byte(0x44);
558 let create_address = zero_nonce_create_address()?;
559 let artifact = memory_artifact(empty_runtime_creation_code());
560
561 cache
562 .db_mut()
563 .insert_account_info(deployer, AccountInfo::default());
564 cache
565 .db_mut()
566 .insert_account_info(create_address, AccountInfo::default());
567
568 let err = artifact
569 .etch_or_create(&mut cache, target, deployer, Bytes::new())
570 .unwrap_err();
571 assert!(err.to_string().contains("etching runtime bytecode"));
572 assert!(err.to_string().contains("bytecode"));
573
574 assert_eq!(cached_nonce(&mut cache, deployer), Some(0));
575 assert_eq!(cached_code_len(&mut cache, create_address), Some(0));
576 assert!(
577 !cache.db_mut().cache.accounts.contains_key(&target),
578 "failed etch should not leave a synthetic target account behind"
579 );
580
581 Ok(())
582 }
583
584 #[test]
585 fn decode_hex_bytecode_rejects_unlinked_libraries() {
586 let err = decode_hex_bytecode("0x60__$abc$__").unwrap_err();
587 assert!(err.to_string().contains("unresolved library"));
588 }
589
590 #[test]
591 fn load_foundry_creation_code_reads_bytecode_object() -> Result<()> {
592 let path = temp_artifact_path("foundry-bytecode-object");
593 std::fs::write(&path, r#"{"bytecode":{"object":"0x60016002"}}"#)?;
594
595 let code = load_foundry_creation_code(&path)?;
596
597 std::fs::remove_file(&path).ok();
598 assert_eq!(code.as_ref(), &[0x60, 0x01, 0x60, 0x02]);
599 Ok(())
600 }
601
602 #[test]
603 fn load_foundry_creation_code_accepts_direct_string_bytecode() -> Result<()> {
604 let path = temp_artifact_path("foundry-bytecode-string");
605 std::fs::write(&path, r#"{"bytecode":"0x6001"}"#)?;
606
607 let code = load_foundry_creation_code(&path)?;
608
609 std::fs::remove_file(&path).ok();
610 assert_eq!(code.as_ref(), &[0x60, 0x01]);
611 Ok(())
612 }
613
614 fn temp_artifact_path(prefix: &str) -> PathBuf {
615 let nanos = SystemTime::now()
616 .duration_since(UNIX_EPOCH)
617 .expect("system time before unix epoch")
618 .as_nanos();
619 std::env::temp_dir().join(format!("{prefix}-{}-{nanos}.json", std::process::id()))
620 }
621
622 fn setup_cache() -> EvmCache {
623 use alloy_provider::{RootProvider, network::AnyNetwork};
624 use alloy_rpc_client::RpcClient;
625 use alloy_transport::mock::Asserter;
626
627 let asserter = Asserter::new();
628 let client = RpcClient::mocked(asserter);
629 let provider = RootProvider::<AnyNetwork>::new(client);
630
631 let rt = tokio::runtime::Builder::new_current_thread()
632 .enable_all()
633 .build()
634 .expect("runtime should build");
635
636 rt.block_on(EvmCache::new(Arc::new(provider)))
637 }
638
639 fn memory_artifact(creation_code: Bytes) -> FoundryArtifact {
640 FoundryArtifact {
641 path: PathBuf::from("memory-artifact.json"),
642 creation_code,
643 }
644 }
645
646 fn account_with_runtime(runtime: &[u8], balance: U256, nonce: u64) -> AccountInfo {
647 let bytecode = Bytecode::new_raw(Bytes::copy_from_slice(runtime));
648 let code_hash = bytecode.hash_slow();
649 AccountInfo {
650 balance,
651 nonce,
652 code: Some(bytecode),
653 code_hash,
654 account_id: None,
655 }
656 }
657
658 fn non_empty_runtime_creation_code() -> Bytes {
659 Bytes::from_static(&[
660 0x60, 0x01, 0x60, 0x0c, 0x60, 0x00, 0x39, 0x60, 0x01, 0x60, 0x00, 0xf3, 0x00, ])
669 }
670
671 fn empty_runtime_creation_code() -> Bytes {
672 Bytes::from_static(&[
673 0x60, 0x00, 0x60, 0x00, 0xf3, ])
677 }
678
679 fn zero_nonce_create_address() -> Result<Address> {
680 "0xbd770416a3345f91e4b34576cb804a576fa48eb1"
681 .parse()
682 .context("zero nonce CREATE address should parse")
683 }
684
685 fn cached_nonce(cache: &mut EvmCache, address: Address) -> Option<u64> {
686 cache
687 .db_mut()
688 .cache
689 .accounts
690 .get(&address)
691 .map(|account| account.info.nonce)
692 }
693
694 fn cached_code_len(cache: &mut EvmCache, address: Address) -> Option<usize> {
695 cache
696 .db_mut()
697 .cache
698 .accounts
699 .get(&address)
700 .map(|account| account.info.code.as_ref().map_or(0, |code| code.len()))
701 }
702}