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