@@ -5,13 +5,13 @@ use crate::{
55 Cheatcode , Cheatcodes , CheatcodesExecutor , CheatsCtxt , Result , Vm :: * , inspector:: exec_create,
66} ;
77use alloy_dyn_abi:: DynSolType ;
8- use alloy_json_abi:: { ContractObject , JsonAbi } ;
8+ use alloy_json_abi:: ContractObject ;
99use alloy_network:: { Network , ReceiptResponse } ;
1010use alloy_primitives:: { Bytes , FixedBytes , U256 , hex, map:: Entry } ;
1111use alloy_sol_types:: SolValue ;
1212use dialoguer:: { Input , Password } ;
1313use forge_script_sequence:: { BroadcastReader , TransactionWithMetadata } ;
14- use foundry_common:: fs ;
14+ use foundry_common:: { contracts :: ContractData , fs } ;
1515use foundry_config:: fs_permissions:: FsAccessKind ;
1616use foundry_evm_core:: evm:: FoundryEvmNetwork ;
1717use revm:: {
@@ -313,9 +313,20 @@ impl Cheatcode for getDeployedCodeCall {
313313impl Cheatcode for getSelectorsCall {
314314 fn apply < FEN : FoundryEvmNetwork > ( & self , state : & mut Cheatcodes < FEN > ) -> Result {
315315 let Self { artifactPath : path } = self ;
316- let abi = get_artifact_abi ( state, path) ?;
317- let selectors: Vec < FixedBytes < 4 > > =
318- abi. functions ( ) . map ( |func| func. selector ( ) . into ( ) ) . collect ( ) ;
316+ let selectors: Vec < FixedBytes < 4 > > = match get_artifact_source ( state, path) ? {
317+ ArtifactSource :: InMemory ( data) => data. abi . functions ( ) . map ( |f| f. selector ( ) ) . collect ( ) ,
318+ ArtifactSource :: Disk ( path) => {
319+ let data = read_artifact_file ( state, & path) ?;
320+ // Parse as raw JSON rather than `ContractObject` so we can still read selectors
321+ // from artifacts with unlinked bytecode (which `ContractObject` rejects).
322+ let json: serde_json:: Value = serde_json:: from_str ( & data) ?;
323+ let abi =
324+ json. get ( "abi" ) . ok_or_else ( || fmt_err ! ( "no `abi` field in artifact JSON" ) ) ?;
325+ let abi: alloy_json_abi:: JsonAbi =
326+ serde_json:: from_value ( abi. clone ( ) ) . map_err ( |e| fmt_err ! ( "{e}" ) ) ?;
327+ abi. functions ( ) . map ( |f| f. selector ( ) ) . collect ( )
328+ }
329+ } ;
319330 Ok ( selectors. abi_encode ( ) )
320331 }
321332}
@@ -457,175 +468,31 @@ fn deploy_code<FEN: FoundryEvmNetwork>(
457468 Ok ( address. abi_encode ( ) )
458469}
459470
460- /// Returns the bytecode from a JSON artifact file.
471+ /// Resolved location of an artifact referenced by a cheatcode path argument.
472+ enum ArtifactSource < ' a > {
473+ /// The artifact was matched in the in-memory `available_artifacts` list.
474+ InMemory ( & ' a ContractData ) ,
475+ /// The artifact must be read from the given path on disk.
476+ Disk ( PathBuf ) ,
477+ }
478+
479+ /// Resolves a cheatcode artifact reference to its source.
461480///
462- /// Can parse following input formats:
481+ /// Can parse the following input formats:
463482/// - `path/to/artifact.json`
464483/// - `path/to/contract.sol`
465484/// - `path/to/contract.sol:ContractName`
466485/// - `path/to/contract.sol:ContractName:0.8.23`
467486/// - `path/to/contract.sol:0.8.23`
468487/// - `ContractName`
469488/// - `ContractName:0.8.23`
470- ///
471- /// This function is safe to use with contracts that have library dependencies.
472- /// `alloy_json_abi::ContractObject` validates bytecode during JSON parsing and will
473- /// reject artifacts with unlinked library placeholders.
474- fn get_artifact_code < FEN : FoundryEvmNetwork > (
475- state : & Cheatcodes < FEN > ,
476- path : & str ,
477- deployed : bool ,
478- ) -> Result < Bytes > {
479- let path = if path. ends_with ( ".json" ) {
480- PathBuf :: from ( path)
481- } else {
482- let mut parts = path. split ( ':' ) ;
483-
484- let mut file = None ;
485- let mut contract_name = None ;
486- let mut version = None ;
487-
488- let path_or_name = parts. next ( ) . unwrap ( ) ;
489- if path_or_name. contains ( '.' ) {
490- file = Some ( PathBuf :: from ( path_or_name) ) ;
491- if let Some ( name_or_version) = parts. next ( ) {
492- if name_or_version. contains ( '.' ) {
493- version = Some ( name_or_version) ;
494- } else {
495- contract_name = Some ( name_or_version) ;
496- version = parts. next ( ) ;
497- }
498- }
499- } else {
500- contract_name = Some ( path_or_name) ;
501- version = parts. next ( ) ;
502- }
503-
504- let version = if let Some ( version) = version {
505- Some ( Version :: parse ( version) . map_err ( |e| fmt_err ! ( "failed parsing version: {e}" ) ) ?)
506- } else {
507- None
508- } ;
509-
510- // Use available artifacts list if present
511- if let Some ( artifacts) = & state. config . available_artifacts {
512- let filtered = artifacts
513- . iter ( )
514- . filter ( |( id, _) | {
515- // name might be in the form of "Counter.0.8.23"
516- let id_name = id. name . split ( '.' ) . next ( ) . unwrap ( ) ;
517-
518- if let Some ( path) = & file
519- && !id. source . ends_with ( path)
520- {
521- return false ;
522- }
523- if let Some ( name) = contract_name
524- && id_name != name
525- {
526- return false ;
527- }
528- if let Some ( ref version) = version
529- && ( id. version . minor != version. minor
530- || id. version . major != version. major
531- || id. version . patch != version. patch )
532- {
533- return false ;
534- }
535- true
536- } )
537- . collect :: < Vec < _ > > ( ) ;
538-
539- let artifact = match & filtered[ ..] {
540- [ ] => None ,
541- [ artifact] => Some ( Ok ( * artifact) ) ,
542- filtered => {
543- let mut filtered = filtered. to_vec ( ) ;
544- // If we know the current script/test contract solc version, try to filter by it
545- Some (
546- state
547- . config
548- . running_artifact
549- . as_ref ( )
550- . and_then ( |running| {
551- // Firstly filter by version
552- filtered. retain ( |( id, _) | id. version == running. version ) ;
553-
554- // Return artifact if only one matched
555- if filtered. len ( ) == 1 {
556- return Some ( filtered[ 0 ] ) ;
557- }
558-
559- // Try filtering by profile as well
560- filtered. retain ( |( id, _) | id. profile == running. profile ) ;
561-
562- ( filtered. len ( ) == 1 ) . then ( || filtered[ 0 ] )
563- } )
564- . ok_or_else ( || fmt_err ! ( "multiple matching artifacts found" ) ) ,
565- )
566- }
567- } ;
568-
569- if let Some ( artifact) = artifact {
570- let artifact = artifact?;
571- let maybe_bytecode = if deployed {
572- artifact. 1 . deployed_bytecode ( ) . cloned ( )
573- } else {
574- artifact. 1 . bytecode ( ) . cloned ( )
575- } ;
576-
577- return maybe_bytecode. ok_or_else ( || {
578- fmt_err ! ( "no bytecode for contract; is it abstract or unlinked?" )
579- } ) ;
580- }
581- }
582-
583- // Fallback: construct path manually when no artifacts list or no match found
584- let path_in_artifacts = match ( file. map ( |f| f. to_string_lossy ( ) . to_string ( ) ) , contract_name)
585- {
586- ( Some ( file) , Some ( contract_name) ) => {
587- PathBuf :: from ( format ! ( "{file}/{contract_name}.json" ) )
588- }
589- ( None , Some ( contract_name) ) => {
590- PathBuf :: from ( format ! ( "{contract_name}.sol/{contract_name}.json" ) )
591- }
592- ( Some ( file) , None ) => {
593- let name = file. replace ( ".sol" , "" ) ;
594- PathBuf :: from ( format ! ( "{file}/{name}.json" ) )
595- }
596- _ => bail ! ( "invalid artifact path" ) ,
597- } ;
598-
599- state. config . paths . artifacts . join ( path_in_artifacts)
600- } ;
601-
602- let path = state. config . ensure_path_allowed ( path, FsAccessKind :: Read ) ?;
603- let data = fs:: read_to_string ( path) . map_err ( |e| {
604- if state. config . available_artifacts . is_some ( ) {
605- fmt_err ! ( "no matching artifact found" )
606- } else {
607- e. into ( )
608- }
609- } ) ?;
610- let artifact = serde_json:: from_str :: < ContractObject > ( & data) ?;
611- let maybe_bytecode = if deployed { artifact. deployed_bytecode } else { artifact. bytecode } ;
612- maybe_bytecode. ok_or_else ( || fmt_err ! ( "no bytecode for contract; is it abstract or unlinked?" ) )
613- }
614-
615- /// Returns the ABI of a matching artifact from the given path.
616- ///
617- /// See [`get_artifact_code`] for the supported path formats.
618- fn get_artifact_abi < FEN : FoundryEvmNetwork > (
619- state : & Cheatcodes < FEN > ,
489+ fn get_artifact_source < ' a , FEN : FoundryEvmNetwork > (
490+ state : & ' a Cheatcodes < FEN > ,
620491 path : & str ,
621- ) -> Result < JsonAbi > {
492+ ) -> Result < ArtifactSource < ' a > > {
622493 if path. ends_with ( ".json" ) {
623- // Read JSON artifact directly from disk.
624494 let path = state. config . ensure_path_allowed ( path, FsAccessKind :: Read ) ?;
625- let data = fs:: read_to_string ( path) ?;
626- let json: serde_json:: Value = serde_json:: from_str ( & data) ?;
627- let abi = json. get ( "abi" ) . ok_or_else ( || fmt_err ! ( "no `abi` field in artifact JSON" ) ) ?;
628- return serde_json:: from_value ( abi. clone ( ) ) . map_err ( |e| fmt_err ! ( "{e}" ) ) ;
495+ return Ok ( ArtifactSource :: Disk ( path) ) ;
629496 }
630497
631498 let mut parts = path. split ( ':' ) ;
@@ -661,6 +528,7 @@ fn get_artifact_abi<FEN: FoundryEvmNetwork>(
661528 let filtered = artifacts
662529 . iter ( )
663530 . filter ( |( id, _) | {
531+ // name might be in the form of "Counter.0.8.23"
664532 let id_name = id. name . split ( '.' ) . next ( ) . unwrap ( ) ;
665533
666534 if let Some ( path) = & file
@@ -689,17 +557,24 @@ fn get_artifact_abi<FEN: FoundryEvmNetwork>(
689557 [ artifact] => Some ( Ok ( * artifact) ) ,
690558 filtered => {
691559 let mut filtered = filtered. to_vec ( ) ;
560+ // If we know the current script/test contract solc version, try to filter by it.
692561 Some (
693562 state
694563 . config
695564 . running_artifact
696565 . as_ref ( )
697566 . and_then ( |running| {
567+ // Firstly filter by version.
698568 filtered. retain ( |( id, _) | id. version == running. version ) ;
569+
570+ // Return artifact if only one matched.
699571 if filtered. len ( ) == 1 {
700572 return Some ( filtered[ 0 ] ) ;
701573 }
574+
575+ // Try filtering by profile as well.
702576 filtered. retain ( |( id, _) | id. profile == running. profile ) ;
577+
703578 ( filtered. len ( ) == 1 ) . then ( || filtered[ 0 ] )
704579 } )
705580 . ok_or_else ( || fmt_err ! ( "multiple matching artifacts found" ) ) ,
@@ -708,16 +583,13 @@ fn get_artifact_abi<FEN: FoundryEvmNetwork>(
708583 } ;
709584
710585 if let Some ( artifact) = artifact {
711- let artifact = artifact?;
712- return Ok ( artifact. 1 . abi . clone ( ) ) ;
586+ return Ok ( ArtifactSource :: InMemory ( artifact?. 1 ) ) ;
713587 }
714588 }
715589
716- // Fallback: construct path manually and read JSON from disk .
590+ // Fallback: construct path manually when no artifacts list or no match found .
717591 let path_in_artifacts = match ( file. map ( |f| f. to_string_lossy ( ) . to_string ( ) ) , contract_name) {
718- ( Some ( file) , Some ( contract_name) ) => {
719- PathBuf :: from ( format ! ( "{file}/{contract_name}.json" ) )
720- }
592+ ( Some ( file) , Some ( contract_name) ) => PathBuf :: from ( format ! ( "{file}/{contract_name}.json" ) ) ,
721593 ( None , Some ( contract_name) ) => {
722594 PathBuf :: from ( format ! ( "{contract_name}.sol/{contract_name}.json" ) )
723595 }
@@ -730,16 +602,47 @@ fn get_artifact_abi<FEN: FoundryEvmNetwork>(
730602
731603 let path = state. config . paths . artifacts . join ( path_in_artifacts) ;
732604 let path = state. config . ensure_path_allowed ( path, FsAccessKind :: Read ) ?;
733- let data = fs:: read_to_string ( path) . map_err ( |e| {
605+ Ok ( ArtifactSource :: Disk ( path) )
606+ }
607+
608+ /// Reads an artifact JSON file, mapping I/O errors to a helpful message when the
609+ /// lookup fell through the in-memory artifacts list.
610+ fn read_artifact_file < FEN : FoundryEvmNetwork > (
611+ state : & Cheatcodes < FEN > ,
612+ path : & Path ,
613+ ) -> Result < String > {
614+ fs:: read_to_string ( path) . map_err ( |e| {
734615 if state. config . available_artifacts . is_some ( ) {
735616 fmt_err ! ( "no matching artifact found" )
736617 } else {
737618 e. into ( )
738619 }
739- } ) ?;
740- let json: serde_json:: Value = serde_json:: from_str ( & data) ?;
741- let abi = json. get ( "abi" ) . ok_or_else ( || fmt_err ! ( "no `abi` field in artifact JSON" ) ) ?;
742- serde_json:: from_value ( abi. clone ( ) ) . map_err ( |e| fmt_err ! ( "{e}" ) )
620+ } )
621+ }
622+
623+ /// Returns the bytecode from a JSON artifact file.
624+ ///
625+ /// See [`get_artifact_source`] for the supported path formats.
626+ ///
627+ /// This function is safe to use with contracts that have library dependencies.
628+ /// `alloy_json_abi::ContractObject` validates bytecode during JSON parsing and will
629+ /// reject artifacts with unlinked library placeholders.
630+ fn get_artifact_code < FEN : FoundryEvmNetwork > (
631+ state : & Cheatcodes < FEN > ,
632+ path : & str ,
633+ deployed : bool ,
634+ ) -> Result < Bytes > {
635+ let maybe_bytecode = match get_artifact_source ( state, path) ? {
636+ ArtifactSource :: InMemory ( data) => {
637+ if deployed { data. deployed_bytecode ( ) } else { data. bytecode ( ) } . cloned ( )
638+ }
639+ ArtifactSource :: Disk ( path) => {
640+ let data = read_artifact_file ( state, & path) ?;
641+ let artifact = serde_json:: from_str :: < ContractObject > ( & data) ?;
642+ if deployed { artifact. deployed_bytecode } else { artifact. bytecode }
643+ }
644+ } ;
645+ maybe_bytecode. ok_or_else ( || fmt_err ! ( "no bytecode for contract; is it abstract or unlinked?" ) )
743646}
744647
745648impl Cheatcode for ffiCall {
0 commit comments