Skip to main content

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