foundry_linking/
lib.rs

1//! # foundry-linking
2//!
3//! EVM bytecode linker.
4
5#![cfg_attr(not(test), warn(unused_crate_dependencies))]
6#![cfg_attr(docsrs, feature(doc_cfg))]
7
8use alloy_primitives::{Address, B256, Bytes};
9use foundry_compilers::{
10    Artifact, ArtifactId,
11    artifacts::{CompactBytecode, CompactContractBytecodeCow, Libraries},
12    contracts::ArtifactContracts,
13};
14use rayon::prelude::*;
15use semver::Version;
16use std::{
17    collections::{BTreeMap, BTreeSet},
18    path::{Path, PathBuf},
19    str::FromStr,
20};
21
22/// Errors that can occur during linking.
23#[derive(Debug, thiserror::Error)]
24pub enum LinkerError {
25    #[error("wasn't able to find artifact for library {name} at {file}")]
26    MissingLibraryArtifact { file: String, name: String },
27    #[error("target artifact is not present in provided artifacts set")]
28    MissingTargetArtifact,
29    #[error(transparent)]
30    InvalidAddress(<Address as std::str::FromStr>::Err),
31    #[error("cyclic dependency found, can't link libraries via CREATE2")]
32    CyclicDependency,
33    #[error("failed linking {artifact}")]
34    LinkingFailed { artifact: String },
35}
36
37pub struct Linker<'a> {
38    /// Root of the project, used to determine whether artifact/library path can be stripped.
39    pub root: PathBuf,
40    /// Compilation artifacts.
41    pub contracts: ArtifactContracts<CompactContractBytecodeCow<'a>>,
42}
43
44/// Output of the `link_with_nonce_or_address`
45pub struct LinkOutput {
46    /// Resolved library addresses. Contains both user-provided and newly deployed libraries.
47    /// It will always contain library paths with stripped path prefixes.
48    pub libraries: Libraries,
49    /// Vector of libraries that need to be deployed from sender address.
50    /// The order in which they appear in the vector is the order in which they should be deployed.
51    pub libs_to_deploy: Vec<Bytes>,
52}
53
54impl<'a> Linker<'a> {
55    pub fn new(
56        root: impl Into<PathBuf>,
57        contracts: ArtifactContracts<CompactContractBytecodeCow<'a>>,
58    ) -> Self {
59        Linker { root: root.into(), contracts }
60    }
61
62    /// Helper method to convert [ArtifactId] to the format in which libraries are stored in
63    /// [Libraries] object.
64    ///
65    /// Strips project root path from source file path.
66    fn convert_artifact_id_to_lib_path(&self, id: &ArtifactId) -> (PathBuf, String) {
67        let path = id.source.strip_prefix(self.root.as_path()).unwrap_or(&id.source);
68        // name is either {LibName} or {LibName}.{version}
69        let name = id.name.split('.').next().unwrap();
70
71        (path.to_path_buf(), name.to_owned())
72    }
73
74    /// Finds an [ArtifactId] object in the given [ArtifactContracts] keys which corresponds to the
75    /// library path in the form of "./path/to/Lib.sol:Lib"
76    ///
77    /// Optionally accepts solc version, and if present, only compares artifacts with given version.
78    fn find_artifact_id_by_library_path(
79        &'a self,
80        file: &str,
81        name: &str,
82        version: Option<&Version>,
83    ) -> Option<&'a ArtifactId> {
84        for id in self.contracts.keys() {
85            if let Some(version) = version
86                && id.version != *version
87            {
88                continue;
89            }
90            let (artifact_path, artifact_name) = self.convert_artifact_id_to_lib_path(id);
91
92            if artifact_name == *name && artifact_path == Path::new(file) {
93                return Some(id);
94            }
95        }
96
97        None
98    }
99
100    /// Performs DFS on the graph of link references, and populates `deps` with all found libraries.
101    fn collect_dependencies(
102        &'a self,
103        target: &'a ArtifactId,
104        deps: &mut BTreeSet<&'a ArtifactId>,
105    ) -> Result<(), LinkerError> {
106        let contract = self.contracts.get(target).ok_or(LinkerError::MissingTargetArtifact)?;
107
108        let mut references: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
109        let mut extend = |bytecode: &CompactBytecode| {
110            for (file, libs) in &bytecode.link_references {
111                references.entry(file.clone()).or_default().extend(libs.keys().cloned());
112            }
113        };
114        if let Some(bytecode) = &contract.bytecode {
115            extend(bytecode);
116        }
117        if let Some(deployed_bytecode) = &contract.deployed_bytecode
118            && let Some(bytecode) = &deployed_bytecode.bytecode
119        {
120            extend(bytecode);
121        }
122
123        for (file, libs) in references {
124            for name in libs {
125                let id = self
126                    .find_artifact_id_by_library_path(&file, &name, Some(&target.version))
127                    .ok_or_else(|| LinkerError::MissingLibraryArtifact {
128                        file: file.clone(),
129                        name,
130                    })?;
131                if deps.insert(id) {
132                    self.collect_dependencies(id, deps)?;
133                }
134            }
135        }
136
137        Ok(())
138    }
139
140    /// Links given artifact with either given library addresses or address computed from sender and
141    /// nonce.
142    ///
143    /// Each key in `libraries` should either be a global path or relative to project root. All
144    /// remappings should be resolved.
145    ///
146    /// When calling for `target` being an external library itself, you should check that `target`
147    /// does not appear in `libs_to_deploy` to avoid deploying it twice. It may happen in cases
148    /// when there is a dependency cycle including `target`.
149    pub fn link_with_nonce_or_address(
150        &'a self,
151        libraries: Libraries,
152        sender: Address,
153        mut nonce: u64,
154        targets: impl IntoIterator<Item = &'a ArtifactId>,
155    ) -> Result<LinkOutput, LinkerError> {
156        // Library paths in `link_references` keys are always stripped, so we have to strip
157        // user-provided paths to be able to match them correctly.
158        let mut libraries = libraries.with_stripped_file_prefixes(self.root.as_path());
159
160        let mut needed_libraries = BTreeSet::new();
161        for target in targets {
162            self.collect_dependencies(target, &mut needed_libraries)?;
163        }
164
165        let mut libs_to_deploy = Vec::new();
166
167        // If `libraries` does not contain needed dependency, compute its address and add to
168        // `libs_to_deploy`.
169        for id in needed_libraries {
170            let (lib_path, lib_name) = self.convert_artifact_id_to_lib_path(id);
171
172            libraries.libs.entry(lib_path).or_default().entry(lib_name).or_insert_with(|| {
173                let address = sender.create(nonce);
174                libs_to_deploy.push((id, address));
175                nonce += 1;
176
177                address.to_checksum(None)
178            });
179        }
180
181        // Link and collect bytecodes for `libs_to_deploy`.
182        let libs_to_deploy = libs_to_deploy
183            .into_par_iter()
184            .map(|(id, _)| {
185                Ok(self.link(id, &libraries)?.get_bytecode_bytes().unwrap().into_owned())
186            })
187            .collect::<Result<Vec<_>, LinkerError>>()?;
188
189        Ok(LinkOutput { libraries, libs_to_deploy })
190    }
191
192    pub fn link_with_create2(
193        &'a self,
194        libraries: Libraries,
195        sender: Address,
196        salt: B256,
197        target: &'a ArtifactId,
198    ) -> Result<LinkOutput, LinkerError> {
199        // Library paths in `link_references` keys are always stripped, so we have to strip
200        // user-provided paths to be able to match them correctly.
201        let mut libraries = libraries.with_stripped_file_prefixes(self.root.as_path());
202
203        let mut needed_libraries = BTreeSet::new();
204        self.collect_dependencies(target, &mut needed_libraries)?;
205
206        let mut needed_libraries = needed_libraries
207            .into_par_iter()
208            .filter(|id| {
209                // Filter out already provided libraries.
210                let (file, name) = self.convert_artifact_id_to_lib_path(id);
211                !libraries.libs.contains_key(&file) || !libraries.libs[&file].contains_key(&name)
212            })
213            .map(|id| {
214                // Link library with provided libs and extract bytecode object (possibly unlinked).
215                let bytecode = self.link(id, &libraries).unwrap().bytecode.unwrap();
216                (id, bytecode)
217            })
218            .collect::<Vec<_>>();
219
220        let mut libs_to_deploy = Vec::new();
221
222        // Iteratively compute addresses and link libraries until we have no unlinked libraries
223        // left.
224        while !needed_libraries.is_empty() {
225            // Find any library which is fully linked.
226            let deployable = needed_libraries
227                .iter()
228                .enumerate()
229                .find(|(_, (_, bytecode))| !bytecode.object.is_unlinked());
230
231            // If we haven't found any deployable library, it means we have a cyclic dependency.
232            let Some((index, &(id, _))) = deployable else {
233                return Err(LinkerError::CyclicDependency);
234            };
235            let (_, bytecode) = needed_libraries.swap_remove(index);
236            let code = bytecode.bytes().unwrap();
237            let address = sender.create2_from_code(salt, code);
238            libs_to_deploy.push(code.clone());
239
240            let (file, name) = self.convert_artifact_id_to_lib_path(id);
241
242            needed_libraries.par_iter_mut().for_each(|(_, bytecode)| {
243                bytecode.to_mut().link(&file.to_string_lossy(), &name, address);
244            });
245
246            libraries.libs.entry(file).or_default().insert(name, address.to_checksum(None));
247        }
248
249        Ok(LinkOutput { libraries, libs_to_deploy })
250    }
251
252    /// Links given artifact with given libraries.
253    pub fn link(
254        &self,
255        target: &ArtifactId,
256        libraries: &Libraries,
257    ) -> Result<CompactContractBytecodeCow<'a>, LinkerError> {
258        let mut contract =
259            self.contracts.get(target).ok_or(LinkerError::MissingTargetArtifact)?.clone();
260        for (file, libs) in &libraries.libs {
261            for (name, address) in libs {
262                let address = Address::from_str(address).map_err(LinkerError::InvalidAddress)?;
263                if let Some(bytecode) = contract.bytecode.as_mut() {
264                    bytecode.to_mut().link(&file.to_string_lossy(), name, address);
265                }
266                if let Some(deployed_bytecode) =
267                    contract.deployed_bytecode.as_mut().and_then(|b| b.to_mut().bytecode.as_mut())
268                {
269                    deployed_bytecode.link(&file.to_string_lossy(), name, address);
270                }
271            }
272        }
273        Ok(contract)
274    }
275
276    /// Ensures that both initial and deployed bytecode are linked.
277    pub fn ensure_linked(
278        &self,
279        contract: &CompactContractBytecodeCow<'a>,
280        target: &ArtifactId,
281    ) -> Result<(), LinkerError> {
282        if let Some(bytecode) = &contract.bytecode
283            && bytecode.object.is_unlinked()
284        {
285            return Err(LinkerError::LinkingFailed {
286                artifact: target.source.to_string_lossy().into(),
287            });
288        }
289        if let Some(deployed_bytecode) = &contract.deployed_bytecode
290            && let Some(deployed_bytecode_obj) = &deployed_bytecode.bytecode
291            && deployed_bytecode_obj.object.is_unlinked()
292        {
293            return Err(LinkerError::LinkingFailed {
294                artifact: target.source.to_string_lossy().into(),
295            });
296        }
297        Ok(())
298    }
299
300    pub fn get_linked_artifacts(
301        &self,
302        libraries: &Libraries,
303    ) -> Result<ArtifactContracts, LinkerError> {
304        self.get_linked_artifacts_cow(libraries).map(ArtifactContracts::from_iter)
305    }
306
307    pub fn get_linked_artifacts_cow(
308        &self,
309        libraries: &Libraries,
310    ) -> Result<ArtifactContracts<CompactContractBytecodeCow<'a>>, LinkerError> {
311        self.contracts
312            .par_iter()
313            .map(|(id, _)| Ok((id.clone(), self.link(id, libraries)?)))
314            .collect::<Result<_, _>>()
315            .map(ArtifactContracts)
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322    use alloy_primitives::{address, fixed_bytes, map::HashMap};
323    use foundry_compilers::{
324        Project, ProjectCompileOutput, ProjectPathsConfig,
325        multi::MultiCompiler,
326        solc::{Solc, SolcCompiler},
327    };
328    use std::sync::OnceLock;
329
330    fn testdata() -> &'static Path {
331        static CACHE: OnceLock<PathBuf> = OnceLock::new();
332        CACHE.get_or_init(|| {
333            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../testdata").canonicalize().unwrap()
334        })
335    }
336
337    #[must_use]
338    struct LinkerTest {
339        project: Project,
340        output: ProjectCompileOutput,
341        dependency_assertions: HashMap<&'static str, Vec<(&'static str, Address)>>,
342    }
343
344    impl LinkerTest {
345        fn new(path: &Path, strip_prefixes: bool) -> Self {
346            assert!(path.exists(), "Path {path:?} does not exist");
347            let paths = ProjectPathsConfig::builder()
348                .root(testdata())
349                .lib(testdata().join("lib"))
350                .sources(path)
351                .tests(path)
352                .build()
353                .unwrap();
354
355            let solc = Solc::find_or_install(&Version::new(0, 8, 18)).unwrap();
356            let project = Project::builder()
357                .paths(paths)
358                .ephemeral()
359                .no_artifacts()
360                .build(MultiCompiler { solc: Some(SolcCompiler::Specific(solc)), vyper: None })
361                .unwrap();
362
363            let mut output = project.compile().unwrap();
364
365            if strip_prefixes {
366                output = output.with_stripped_file_prefixes(project.root());
367            }
368
369            Self { project, output, dependency_assertions: HashMap::default() }
370        }
371
372        fn assert_dependencies(
373            mut self,
374            artifact_id: &'static str,
375            deps: &[(&'static str, Address)],
376        ) -> Self {
377            self.dependency_assertions.insert(artifact_id, deps.to_vec());
378            self
379        }
380
381        fn test_with_sender_and_nonce(self, sender: Address, initial_nonce: u64) {
382            let linker = Linker::new(self.project.root(), self.output.artifact_ids().collect());
383            for (id, identifier) in self.iter_linking_targets(&linker) {
384                let output = linker
385                    .link_with_nonce_or_address(Default::default(), sender, initial_nonce, [id])
386                    .expect("Linking failed");
387                self.validate_assertions(identifier, output);
388            }
389        }
390
391        fn test_with_create2(self, sender: Address, salt: B256) {
392            let linker = Linker::new(self.project.root(), self.output.artifact_ids().collect());
393            for (id, identifier) in self.iter_linking_targets(&linker) {
394                let output = linker
395                    .link_with_create2(Default::default(), sender, salt, id)
396                    .expect("Linking failed");
397                self.validate_assertions(identifier, output);
398            }
399        }
400
401        fn iter_linking_targets<'a>(
402            &'a self,
403            linker: &'a Linker<'_>,
404        ) -> impl Iterator<Item = (&'a ArtifactId, String)> + 'a {
405            self.sanity_check(linker);
406            linker.contracts.keys().filter_map(move |id| {
407                // If we didn't strip paths, artifacts will have absolute paths.
408                // That's expected and we want to ensure that only `libraries` object has relative
409                // paths, artifacts should be kept as is.
410                let source = id
411                    .source
412                    .strip_prefix(self.project.root())
413                    .unwrap_or(&id.source)
414                    .to_string_lossy();
415                let identifier = format!("{source}:{}", id.name);
416
417                // Skip test utils as they always have no dependencies.
418                if identifier.contains("utils/") {
419                    return None;
420                }
421
422                Some((id, identifier))
423            })
424        }
425
426        fn sanity_check(&self, linker: &Linker<'_>) {
427            assert!(!self.dependency_assertions.is_empty(), "Dependency assertions are empty");
428            assert!(!linker.contracts.is_empty(), "Linker contracts are empty");
429        }
430
431        fn validate_assertions(&self, identifier: String, output: LinkOutput) {
432            let LinkOutput { libs_to_deploy, libraries } = output;
433
434            let assertions = self
435                .dependency_assertions
436                .get(identifier.as_str())
437                .unwrap_or_else(|| panic!("Unexpected artifact: {identifier}"));
438
439            assert_eq!(
440                libs_to_deploy.len(),
441                assertions.len(),
442                "artifact {identifier} has more/less dependencies than expected ({} vs {}): {:#?}",
443                libs_to_deploy.len(),
444                assertions.len(),
445                libs_to_deploy
446            );
447
448            for &(dep_identifier, address) in assertions {
449                let (file, name) = dep_identifier.split_once(':').unwrap();
450                if let Some(lib_address) =
451                    libraries.libs.get(Path::new(file)).and_then(|libs| libs.get(name))
452                {
453                    assert_eq!(
454                        lib_address.parse::<Address>().unwrap(),
455                        address,
456                        "incorrect library address for dependency {dep_identifier} of {identifier}"
457                    );
458                } else {
459                    panic!("Library {dep_identifier} not found");
460                }
461            }
462        }
463    }
464
465    fn link_test(path: impl AsRef<Path>, mut test_fn: impl FnMut(LinkerTest)) {
466        fn link_test(path: &Path, test_fn: &mut dyn FnMut(LinkerTest)) {
467            test_fn(LinkerTest::new(path, true));
468            test_fn(LinkerTest::new(path, false));
469        }
470        link_test(path.as_ref(), &mut test_fn);
471    }
472
473    #[test]
474    #[should_panic = "assertions are empty"]
475    fn no_assertions() {
476        link_test(testdata().join("default/linking/simple"), |linker| {
477            linker.test_with_sender_and_nonce(Address::default(), 1);
478        });
479    }
480
481    #[test]
482    #[should_panic = "does not exist"]
483    fn unknown_path() {
484        link_test("doesnotexist", |linker| {
485            linker
486                .assert_dependencies("a:b", &[])
487                .test_with_sender_and_nonce(Address::default(), 1);
488        });
489    }
490
491    #[test]
492    fn link_simple() {
493        link_test(testdata().join("default/linking/simple"), |linker| {
494            linker
495                .assert_dependencies("default/linking/simple/Simple.t.sol:Lib", &[])
496                .assert_dependencies(
497                    "default/linking/simple/Simple.t.sol:LibraryConsumer",
498                    &[(
499                        "default/linking/simple/Simple.t.sol:Lib",
500                        address!("0x5a443704dd4b594b382c22a083e2bd3090a6fef3"),
501                    )],
502                )
503                .assert_dependencies(
504                    "default/linking/simple/Simple.t.sol:SimpleLibraryLinkingTest",
505                    &[(
506                        "default/linking/simple/Simple.t.sol:Lib",
507                        address!("0x5a443704dd4b594b382c22a083e2bd3090a6fef3"),
508                    )],
509                )
510                .test_with_sender_and_nonce(Address::default(), 1);
511        });
512    }
513
514    #[test]
515    fn link_nested() {
516        link_test(testdata().join("default/linking/nested"), |linker| {
517            linker
518                .assert_dependencies("default/linking/nested/Nested.t.sol:Lib", &[])
519                .assert_dependencies(
520                    "default/linking/nested/Nested.t.sol:NestedLib",
521                    &[(
522                        "default/linking/nested/Nested.t.sol:Lib",
523                        address!("0x5a443704dd4b594b382c22a083e2bd3090a6fef3"),
524                    )],
525                )
526                .assert_dependencies(
527                    "default/linking/nested/Nested.t.sol:LibraryConsumer",
528                    &[
529                        // Lib shows up here twice, because the linker sees it twice, but it should
530                        // have the same address and nonce.
531                        (
532                            "default/linking/nested/Nested.t.sol:Lib",
533                            Address::from_str("0x5a443704dd4b594b382c22a083e2bd3090a6fef3")
534                                .unwrap(),
535                        ),
536                        (
537                            "default/linking/nested/Nested.t.sol:NestedLib",
538                            Address::from_str("0x47e9Fbef8C83A1714F1951F142132E6e90F5fa5D")
539                                .unwrap(),
540                        ),
541                    ],
542                )
543                .assert_dependencies(
544                    "default/linking/nested/Nested.t.sol:NestedLibraryLinkingTest",
545                    &[
546                        (
547                            "default/linking/nested/Nested.t.sol:Lib",
548                            Address::from_str("0x5a443704dd4b594b382c22a083e2bd3090a6fef3")
549                                .unwrap(),
550                        ),
551                        (
552                            "default/linking/nested/Nested.t.sol:NestedLib",
553                            Address::from_str("0x47e9fbef8c83a1714f1951f142132e6e90f5fa5d")
554                                .unwrap(),
555                        ),
556                    ],
557                )
558                .test_with_sender_and_nonce(Address::default(), 1);
559        });
560    }
561
562    #[test]
563    fn link_duplicate() {
564        link_test(testdata().join("default/linking/duplicate"), |linker| {
565            linker
566                .assert_dependencies("default/linking/duplicate/Duplicate.t.sol:A", &[])
567                .assert_dependencies("default/linking/duplicate/Duplicate.t.sol:B", &[])
568                .assert_dependencies(
569                    "default/linking/duplicate/Duplicate.t.sol:C",
570                    &[(
571                        "default/linking/duplicate/Duplicate.t.sol:A",
572                        address!("0x5a443704dd4b594b382c22a083e2bd3090a6fef3"),
573                    )],
574                )
575                .assert_dependencies(
576                    "default/linking/duplicate/Duplicate.t.sol:D",
577                    &[(
578                        "default/linking/duplicate/Duplicate.t.sol:B",
579                        address!("0x5a443704dd4b594b382c22a083e2bd3090a6fef3"),
580                    )],
581                )
582                .assert_dependencies(
583                    "default/linking/duplicate/Duplicate.t.sol:E",
584                    &[
585                        (
586                            "default/linking/duplicate/Duplicate.t.sol:A",
587                            Address::from_str("0x5a443704dd4b594b382c22a083e2bd3090a6fef3")
588                                .unwrap(),
589                        ),
590                        (
591                            "default/linking/duplicate/Duplicate.t.sol:C",
592                            Address::from_str("0x47e9fbef8c83a1714f1951f142132e6e90f5fa5d")
593                                .unwrap(),
594                        ),
595                    ],
596                )
597                .assert_dependencies(
598                    "default/linking/duplicate/Duplicate.t.sol:LibraryConsumer",
599                    &[
600                        (
601                            "default/linking/duplicate/Duplicate.t.sol:A",
602                            Address::from_str("0x5a443704dd4b594b382c22a083e2bd3090a6fef3")
603                                .unwrap(),
604                        ),
605                        (
606                            "default/linking/duplicate/Duplicate.t.sol:B",
607                            Address::from_str("0x47e9fbef8c83a1714f1951f142132e6e90f5fa5d")
608                                .unwrap(),
609                        ),
610                        (
611                            "default/linking/duplicate/Duplicate.t.sol:C",
612                            Address::from_str("0x8be503bcded90ed42eff31f56199399b2b0154ca")
613                                .unwrap(),
614                        ),
615                        (
616                            "default/linking/duplicate/Duplicate.t.sol:D",
617                            Address::from_str("0x47c5e40890bce4a473a49d7501808b9633f29782")
618                                .unwrap(),
619                        ),
620                        (
621                            "default/linking/duplicate/Duplicate.t.sol:E",
622                            Address::from_str("0x29b2440db4a256b0c1e6d3b4cdcaa68e2440a08f")
623                                .unwrap(),
624                        ),
625                    ],
626                )
627                .assert_dependencies(
628                    "default/linking/duplicate/Duplicate.t.sol:DuplicateLibraryLinkingTest",
629                    &[
630                        (
631                            "default/linking/duplicate/Duplicate.t.sol:A",
632                            Address::from_str("0x5a443704dd4b594b382c22a083e2bd3090a6fef3")
633                                .unwrap(),
634                        ),
635                        (
636                            "default/linking/duplicate/Duplicate.t.sol:B",
637                            Address::from_str("0x47e9fbef8c83a1714f1951f142132e6e90f5fa5d")
638                                .unwrap(),
639                        ),
640                        (
641                            "default/linking/duplicate/Duplicate.t.sol:C",
642                            Address::from_str("0x8be503bcded90ed42eff31f56199399b2b0154ca")
643                                .unwrap(),
644                        ),
645                        (
646                            "default/linking/duplicate/Duplicate.t.sol:D",
647                            Address::from_str("0x47c5e40890bce4a473a49d7501808b9633f29782")
648                                .unwrap(),
649                        ),
650                        (
651                            "default/linking/duplicate/Duplicate.t.sol:E",
652                            Address::from_str("0x29b2440db4a256b0c1e6d3b4cdcaa68e2440a08f")
653                                .unwrap(),
654                        ),
655                    ],
656                )
657                .test_with_sender_and_nonce(Address::default(), 1);
658        });
659    }
660
661    #[test]
662    fn link_cycle() {
663        link_test(testdata().join("default/linking/cycle"), |linker| {
664            linker
665                .assert_dependencies(
666                    "default/linking/cycle/Cycle.t.sol:Foo",
667                    &[
668                        (
669                            "default/linking/cycle/Cycle.t.sol:Foo",
670                            Address::from_str("0x47e9Fbef8C83A1714F1951F142132E6e90F5fa5D")
671                                .unwrap(),
672                        ),
673                        (
674                            "default/linking/cycle/Cycle.t.sol:Bar",
675                            Address::from_str("0x5a443704dd4B594B382c22a083e2BD3090A6feF3")
676                                .unwrap(),
677                        ),
678                    ],
679                )
680                .assert_dependencies(
681                    "default/linking/cycle/Cycle.t.sol:Bar",
682                    &[
683                        (
684                            "default/linking/cycle/Cycle.t.sol:Foo",
685                            Address::from_str("0x47e9Fbef8C83A1714F1951F142132E6e90F5fa5D")
686                                .unwrap(),
687                        ),
688                        (
689                            "default/linking/cycle/Cycle.t.sol:Bar",
690                            Address::from_str("0x5a443704dd4B594B382c22a083e2BD3090A6feF3")
691                                .unwrap(),
692                        ),
693                    ],
694                )
695                .test_with_sender_and_nonce(Address::default(), 1);
696        });
697    }
698
699    #[test]
700    fn link_create2_nested() {
701        link_test(testdata().join("default/linking/nested"), |linker| {
702            linker
703                .assert_dependencies("default/linking/nested/Nested.t.sol:Lib", &[])
704                .assert_dependencies(
705                    "default/linking/nested/Nested.t.sol:NestedLib",
706                    &[(
707                        "default/linking/nested/Nested.t.sol:Lib",
708                        address!("0x773253227cce756e50c3993ec6366b3ec27786f9"),
709                    )],
710                )
711                .assert_dependencies(
712                    "default/linking/nested/Nested.t.sol:LibraryConsumer",
713                    &[
714                        // Lib shows up here twice, because the linker sees it twice, but it should
715                        // have the same address and nonce.
716                        (
717                            "default/linking/nested/Nested.t.sol:Lib",
718                            Address::from_str("0x773253227cce756e50c3993ec6366b3ec27786f9")
719                                .unwrap(),
720                        ),
721                        (
722                            "default/linking/nested/Nested.t.sol:NestedLib",
723                            Address::from_str("0xac231df03403867b05d092c26fc91b6b83f4bebe")
724                                .unwrap(),
725                        ),
726                    ],
727                )
728                .assert_dependencies(
729                    "default/linking/nested/Nested.t.sol:NestedLibraryLinkingTest",
730                    &[
731                        (
732                            "default/linking/nested/Nested.t.sol:Lib",
733                            Address::from_str("0x773253227cce756e50c3993ec6366b3ec27786f9")
734                                .unwrap(),
735                        ),
736                        (
737                            "default/linking/nested/Nested.t.sol:NestedLib",
738                            Address::from_str("0xac231df03403867b05d092c26fc91b6b83f4bebe")
739                                .unwrap(),
740                        ),
741                    ],
742                )
743                .test_with_create2(
744                    Address::default(),
745                    fixed_bytes!(
746                        "19bf59b7b67ae8edcbc6e53616080f61fa99285c061450ad601b0bc40c9adfc9"
747                    ),
748                );
749        });
750    }
751
752    #[test]
753    fn link_samefile_union() {
754        link_test(testdata().join("default/linking/samefile_union"), |linker| {
755            linker
756                .assert_dependencies("default/linking/samefile_union/Libs.sol:LInit", &[])
757                .assert_dependencies("default/linking/samefile_union/Libs.sol:LRun", &[])
758                .assert_dependencies(
759                    "default/linking/samefile_union/SameFileUnion.t.sol:UsesBoth",
760                    &[
761                        (
762                            "default/linking/samefile_union/Libs.sol:LInit",
763                            Address::from_str("0x5a443704dd4b594b382c22a083e2bd3090a6fef3")
764                                .unwrap(),
765                        ),
766                        (
767                            "default/linking/samefile_union/Libs.sol:LRun",
768                            Address::from_str("0x47e9fbef8c83a1714f1951f142132e6e90f5fa5d")
769                                .unwrap(),
770                        ),
771                    ],
772                )
773                .test_with_sender_and_nonce(Address::default(), 1);
774        });
775    }
776
777    #[test]
778    fn linking_failure() {
779        let linker = LinkerTest::new(&testdata().join("default/linking/simple"), true);
780        let linker_instance =
781            Linker::new(linker.project.root(), linker.output.artifact_ids().collect());
782
783        // Create a libraries object with an incorrect library name that won't match any references
784        let mut libraries = Libraries::default();
785        libraries.libs.entry("default/linking/simple/Simple.t.sol".into()).or_default().insert(
786            "NonExistentLib".to_string(),
787            "0x5a443704dd4b594b382c22a083e2bd3090a6fef3".to_string(),
788        );
789
790        // Try to link the LibraryConsumer contract with incorrect library
791        let artifact_id = linker_instance
792            .contracts
793            .keys()
794            .find(|id| id.name == "LibraryConsumer")
795            .expect("LibraryConsumer contract not found");
796
797        let contract = linker_instance.contracts.get(artifact_id).unwrap();
798
799        // Verify that the artifact has unlinked bytecode
800        assert!(
801            linker_instance.ensure_linked(contract, artifact_id).is_err(),
802            "Expected artifact to have unlinked bytecode"
803        );
804    }
805}