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