1#![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 semver::Version;
15use std::{
16 collections::{BTreeMap, BTreeSet},
17 path::{Path, PathBuf},
18 str::FromStr,
19};
20
21#[derive(Debug, thiserror::Error)]
23pub enum LinkerError {
24 #[error("wasn't able to find artifact for library {name} at {file}")]
25 MissingLibraryArtifact { file: String, name: String },
26 #[error("target artifact is not present in provided artifacts set")]
27 MissingTargetArtifact,
28 #[error(transparent)]
29 InvalidAddress(<Address as std::str::FromStr>::Err),
30 #[error("cyclic dependency found, can't link libraries via CREATE2")]
31 CyclicDependency,
32}
33
34pub struct Linker<'a> {
35 pub root: PathBuf,
37 pub contracts: ArtifactContracts<CompactContractBytecodeCow<'a>>,
39}
40
41pub struct LinkOutput {
43 pub libraries: Libraries,
46 pub libs_to_deploy: Vec<Bytes>,
49}
50
51impl<'a> Linker<'a> {
52 pub fn new(
53 root: impl Into<PathBuf>,
54 contracts: ArtifactContracts<CompactContractBytecodeCow<'a>>,
55 ) -> Self {
56 Linker { root: root.into(), contracts }
57 }
58
59 fn convert_artifact_id_to_lib_path(&self, id: &ArtifactId) -> (PathBuf, String) {
64 let path = id.source.strip_prefix(self.root.as_path()).unwrap_or(&id.source);
65 let name = id.name.split('.').next().unwrap();
67
68 (path.to_path_buf(), name.to_owned())
69 }
70
71 fn find_artifact_id_by_library_path(
76 &'a self,
77 file: &str,
78 name: &str,
79 version: Option<&Version>,
80 ) -> Option<&'a ArtifactId> {
81 for id in self.contracts.keys() {
82 if let Some(version) = version
83 && id.version != *version
84 {
85 continue;
86 }
87 let (artifact_path, artifact_name) = self.convert_artifact_id_to_lib_path(id);
88
89 if artifact_name == *name && artifact_path == Path::new(file) {
90 return Some(id);
91 }
92 }
93
94 None
95 }
96
97 fn collect_dependencies(
99 &'a self,
100 target: &'a ArtifactId,
101 deps: &mut BTreeSet<&'a ArtifactId>,
102 ) -> Result<(), LinkerError> {
103 let contract = self.contracts.get(target).ok_or(LinkerError::MissingTargetArtifact)?;
104
105 let mut references = BTreeMap::new();
106 if let Some(bytecode) = &contract.bytecode {
107 references.extend(bytecode.link_references.clone());
108 }
109 if let Some(deployed_bytecode) = &contract.deployed_bytecode
110 && let Some(bytecode) = &deployed_bytecode.bytecode
111 {
112 references.extend(bytecode.link_references.clone());
113 }
114
115 for (file, libs) in &references {
116 for contract in libs.keys() {
117 let id = self
118 .find_artifact_id_by_library_path(file, contract, Some(&target.version))
119 .ok_or_else(|| LinkerError::MissingLibraryArtifact {
120 file: file.to_string(),
121 name: contract.to_string(),
122 })?;
123 if deps.insert(id) {
124 self.collect_dependencies(id, deps)?;
125 }
126 }
127 }
128
129 Ok(())
130 }
131
132 pub fn link_with_nonce_or_address(
142 &'a self,
143 libraries: Libraries,
144 sender: Address,
145 mut nonce: u64,
146 targets: impl IntoIterator<Item = &'a ArtifactId>,
147 ) -> Result<LinkOutput, LinkerError> {
148 let mut libraries = libraries.with_stripped_file_prefixes(self.root.as_path());
151
152 let mut needed_libraries = BTreeSet::new();
153 for target in targets {
154 self.collect_dependencies(target, &mut needed_libraries)?;
155 }
156
157 let mut libs_to_deploy = Vec::new();
158
159 for id in needed_libraries {
162 let (lib_path, lib_name) = self.convert_artifact_id_to_lib_path(id);
163
164 libraries.libs.entry(lib_path).or_default().entry(lib_name).or_insert_with(|| {
165 let address = sender.create(nonce);
166 libs_to_deploy.push((id, address));
167 nonce += 1;
168
169 address.to_checksum(None)
170 });
171 }
172
173 let libs_to_deploy = libs_to_deploy
175 .into_iter()
176 .map(|(id, _)| {
177 Ok(self.link(id, &libraries)?.get_bytecode_bytes().unwrap().into_owned())
178 })
179 .collect::<Result<Vec<_>, LinkerError>>()?;
180
181 Ok(LinkOutput { libraries, libs_to_deploy })
182 }
183
184 pub fn link_with_create2(
185 &'a self,
186 libraries: Libraries,
187 sender: Address,
188 salt: B256,
189 target: &'a ArtifactId,
190 ) -> Result<LinkOutput, LinkerError> {
191 let mut libraries = libraries.with_stripped_file_prefixes(self.root.as_path());
194
195 let mut needed_libraries = BTreeSet::new();
196 self.collect_dependencies(target, &mut needed_libraries)?;
197
198 let mut needed_libraries = needed_libraries
199 .into_iter()
200 .filter(|id| {
201 let (file, name) = self.convert_artifact_id_to_lib_path(id);
203 !libraries.libs.contains_key(&file) || !libraries.libs[&file].contains_key(&name)
204 })
205 .map(|id| {
206 let bytecode = self.link(id, &libraries).unwrap().bytecode.unwrap();
208 (id, bytecode)
209 })
210 .collect::<Vec<_>>();
211
212 let mut libs_to_deploy = Vec::new();
213
214 while !needed_libraries.is_empty() {
217 let deployable = needed_libraries
219 .iter()
220 .enumerate()
221 .find(|(_, (_, bytecode))| !bytecode.object.is_unlinked());
222
223 let Some((index, &(id, _))) = deployable else {
225 return Err(LinkerError::CyclicDependency);
226 };
227 let (_, bytecode) = needed_libraries.swap_remove(index);
228 let code = bytecode.bytes().unwrap();
229 let address = sender.create2_from_code(salt, code);
230 libs_to_deploy.push(code.clone());
231
232 let (file, name) = self.convert_artifact_id_to_lib_path(id);
233
234 for (_, bytecode) in &mut needed_libraries {
235 bytecode.to_mut().link(&file.to_string_lossy(), &name, address);
236 }
237
238 libraries.libs.entry(file).or_default().insert(name, address.to_checksum(None));
239 }
240
241 Ok(LinkOutput { libraries, libs_to_deploy })
242 }
243
244 pub fn link(
246 &self,
247 target: &ArtifactId,
248 libraries: &Libraries,
249 ) -> Result<CompactContractBytecodeCow<'a>, LinkerError> {
250 let mut contract =
251 self.contracts.get(target).ok_or(LinkerError::MissingTargetArtifact)?.clone();
252 for (file, libs) in &libraries.libs {
253 for (name, address) in libs {
254 let address = Address::from_str(address).map_err(LinkerError::InvalidAddress)?;
255 if let Some(bytecode) = contract.bytecode.as_mut() {
256 bytecode.to_mut().link(&file.to_string_lossy(), name, address);
257 }
258 if let Some(deployed_bytecode) =
259 contract.deployed_bytecode.as_mut().and_then(|b| b.to_mut().bytecode.as_mut())
260 {
261 deployed_bytecode.link(&file.to_string_lossy(), name, address);
262 }
263 }
264 }
265 Ok(contract)
266 }
267
268 pub fn get_linked_artifacts(
269 &self,
270 libraries: &Libraries,
271 ) -> Result<ArtifactContracts, LinkerError> {
272 self.contracts.keys().map(|id| Ok((id.clone(), self.link(id, libraries)?))).collect()
273 }
274
275 pub fn get_linked_artifacts_cow(
276 &self,
277 libraries: &Libraries,
278 ) -> Result<ArtifactContracts<CompactContractBytecodeCow<'a>>, LinkerError> {
279 self.contracts.keys().map(|id| Ok((id.clone(), self.link(id, libraries)?))).collect()
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 use alloy_primitives::{address, fixed_bytes, map::HashMap};
287 use foundry_compilers::{
288 Project, ProjectCompileOutput, ProjectPathsConfig,
289 multi::{MultiCompiler, SolidityCompiler},
290 solc::{Solc, SolcCompiler},
291 };
292
293 struct LinkerTest {
294 project: Project,
295 output: ProjectCompileOutput,
296 dependency_assertions: HashMap<String, Vec<(String, Address)>>,
297 }
298
299 impl LinkerTest {
300 fn new(path: impl Into<PathBuf>, strip_prefixes: bool) -> Self {
301 let path = path.into();
302 let paths = ProjectPathsConfig::builder()
303 .root("../../testdata")
304 .lib("../../testdata/lib")
305 .sources(path.clone())
306 .tests(path)
307 .build()
308 .unwrap();
309
310 let solc = Solc::find_or_install(&Version::new(0, 8, 18)).unwrap();
311 let project = Project::builder()
312 .paths(paths)
313 .ephemeral()
314 .no_artifacts()
315 .build(MultiCompiler {
316 solidity: SolidityCompiler::Solc(SolcCompiler::Specific(solc)),
317 vyper: None,
318 })
319 .unwrap();
320
321 let mut output = project.compile().unwrap();
322
323 if strip_prefixes {
324 output = output.with_stripped_file_prefixes(project.root());
325 }
326
327 Self { project, output, dependency_assertions: HashMap::default() }
328 }
329
330 fn assert_dependencies(
331 mut self,
332 artifact_id: String,
333 deps: Vec<(String, Address)>,
334 ) -> Self {
335 self.dependency_assertions.insert(artifact_id, deps);
336 self
337 }
338
339 fn test_with_sender_and_nonce(self, sender: Address, initial_nonce: u64) {
340 let linker = Linker::new(self.project.root(), self.output.artifact_ids().collect());
341 for (id, identifier) in self.iter_linking_targets(&linker) {
342 let output = linker
343 .link_with_nonce_or_address(Default::default(), sender, initial_nonce, [id])
344 .expect("Linking failed");
345 self.validate_assertions(identifier, output);
346 }
347 }
348
349 fn test_with_create2(self, sender: Address, salt: B256) {
350 let linker = Linker::new(self.project.root(), self.output.artifact_ids().collect());
351 for (id, identifier) in self.iter_linking_targets(&linker) {
352 let output = linker
353 .link_with_create2(Default::default(), sender, salt, id)
354 .expect("Linking failed");
355 self.validate_assertions(identifier, output);
356 }
357 }
358
359 fn iter_linking_targets<'a>(
360 &'a self,
361 linker: &'a Linker<'_>,
362 ) -> impl IntoIterator<Item = (&'a ArtifactId, String)> + 'a {
363 linker.contracts.keys().filter_map(move |id| {
364 let source = id
368 .source
369 .strip_prefix(self.project.root())
370 .unwrap_or(&id.source)
371 .to_string_lossy();
372 let identifier = format!("{source}:{}", id.name);
373
374 if identifier.contains("DSTest") {
377 return None;
378 }
379
380 Some((id, identifier))
381 })
382 }
383
384 fn validate_assertions(&self, identifier: String, output: LinkOutput) {
385 let LinkOutput { libs_to_deploy, libraries } = output;
386
387 let assertions = self
388 .dependency_assertions
389 .get(&identifier)
390 .unwrap_or_else(|| panic!("Unexpected artifact: {identifier}"));
391
392 assert_eq!(
393 libs_to_deploy.len(),
394 assertions.len(),
395 "artifact {identifier} has more/less dependencies than expected ({} vs {}): {:#?}",
396 libs_to_deploy.len(),
397 assertions.len(),
398 libs_to_deploy
399 );
400
401 for (dep_identifier, address) in assertions {
402 let (file, name) = dep_identifier.split_once(':').unwrap();
403 if let Some(lib_address) =
404 libraries.libs.get(Path::new(file)).and_then(|libs| libs.get(name))
405 {
406 assert_eq!(
407 *lib_address,
408 address.to_string(),
409 "incorrect library address for dependency {dep_identifier} of {identifier}"
410 );
411 } else {
412 panic!("Library {dep_identifier} not found");
413 }
414 }
415 }
416 }
417
418 fn link_test(path: impl Into<PathBuf>, test_fn: impl Fn(LinkerTest)) {
419 let path = path.into();
420 test_fn(LinkerTest::new(path.clone(), true));
421 test_fn(LinkerTest::new(path, false));
422 }
423
424 #[test]
425 fn link_simple() {
426 link_test("../../testdata/default/linking/simple", |linker| {
427 linker
428 .assert_dependencies("default/linking/simple/Simple.t.sol:Lib".to_string(), vec![])
429 .assert_dependencies(
430 "default/linking/simple/Simple.t.sol:LibraryConsumer".to_string(),
431 vec![(
432 "default/linking/simple/Simple.t.sol:Lib".to_string(),
433 address!("0x5a443704dd4b594b382c22a083e2bd3090a6fef3"),
434 )],
435 )
436 .assert_dependencies(
437 "default/linking/simple/Simple.t.sol:SimpleLibraryLinkingTest".to_string(),
438 vec![(
439 "default/linking/simple/Simple.t.sol:Lib".to_string(),
440 address!("0x5a443704dd4b594b382c22a083e2bd3090a6fef3"),
441 )],
442 )
443 .test_with_sender_and_nonce(Address::default(), 1);
444 });
445 }
446
447 #[test]
448 fn link_nested() {
449 link_test("../../testdata/default/linking/nested", |linker| {
450 linker
451 .assert_dependencies("default/linking/nested/Nested.t.sol:Lib".to_string(), vec![])
452 .assert_dependencies(
453 "default/linking/nested/Nested.t.sol:NestedLib".to_string(),
454 vec![(
455 "default/linking/nested/Nested.t.sol:Lib".to_string(),
456 address!("0x5a443704dd4b594b382c22a083e2bd3090a6fef3"),
457 )],
458 )
459 .assert_dependencies(
460 "default/linking/nested/Nested.t.sol:LibraryConsumer".to_string(),
461 vec![
462 (
465 "default/linking/nested/Nested.t.sol:Lib".to_string(),
466 Address::from_str("0x5a443704dd4b594b382c22a083e2bd3090a6fef3")
467 .unwrap(),
468 ),
469 (
470 "default/linking/nested/Nested.t.sol:NestedLib".to_string(),
471 Address::from_str("0x47e9Fbef8C83A1714F1951F142132E6e90F5fa5D")
472 .unwrap(),
473 ),
474 ],
475 )
476 .assert_dependencies(
477 "default/linking/nested/Nested.t.sol:NestedLibraryLinkingTest".to_string(),
478 vec![
479 (
480 "default/linking/nested/Nested.t.sol:Lib".to_string(),
481 Address::from_str("0x5a443704dd4b594b382c22a083e2bd3090a6fef3")
482 .unwrap(),
483 ),
484 (
485 "default/linking/nested/Nested.t.sol:NestedLib".to_string(),
486 Address::from_str("0x47e9fbef8c83a1714f1951f142132e6e90f5fa5d")
487 .unwrap(),
488 ),
489 ],
490 )
491 .test_with_sender_and_nonce(Address::default(), 1);
492 });
493 }
494
495 #[test]
496 fn link_duplicate() {
497 link_test("../../testdata/default/linking/duplicate", |linker| {
498 linker
499 .assert_dependencies(
500 "default/linking/duplicate/Duplicate.t.sol:A".to_string(),
501 vec![],
502 )
503 .assert_dependencies(
504 "default/linking/duplicate/Duplicate.t.sol:B".to_string(),
505 vec![],
506 )
507 .assert_dependencies(
508 "default/linking/duplicate/Duplicate.t.sol:C".to_string(),
509 vec![(
510 "default/linking/duplicate/Duplicate.t.sol:A".to_string(),
511 address!("0x5a443704dd4b594b382c22a083e2bd3090a6fef3"),
512 )],
513 )
514 .assert_dependencies(
515 "default/linking/duplicate/Duplicate.t.sol:D".to_string(),
516 vec![(
517 "default/linking/duplicate/Duplicate.t.sol:B".to_string(),
518 address!("0x5a443704dd4b594b382c22a083e2bd3090a6fef3"),
519 )],
520 )
521 .assert_dependencies(
522 "default/linking/duplicate/Duplicate.t.sol:E".to_string(),
523 vec![
524 (
525 "default/linking/duplicate/Duplicate.t.sol:A".to_string(),
526 Address::from_str("0x5a443704dd4b594b382c22a083e2bd3090a6fef3")
527 .unwrap(),
528 ),
529 (
530 "default/linking/duplicate/Duplicate.t.sol:C".to_string(),
531 Address::from_str("0x47e9fbef8c83a1714f1951f142132e6e90f5fa5d")
532 .unwrap(),
533 ),
534 ],
535 )
536 .assert_dependencies(
537 "default/linking/duplicate/Duplicate.t.sol:LibraryConsumer".to_string(),
538 vec![
539 (
540 "default/linking/duplicate/Duplicate.t.sol:A".to_string(),
541 Address::from_str("0x5a443704dd4b594b382c22a083e2bd3090a6fef3")
542 .unwrap(),
543 ),
544 (
545 "default/linking/duplicate/Duplicate.t.sol:B".to_string(),
546 Address::from_str("0x47e9fbef8c83a1714f1951f142132e6e90f5fa5d")
547 .unwrap(),
548 ),
549 (
550 "default/linking/duplicate/Duplicate.t.sol:C".to_string(),
551 Address::from_str("0x8be503bcded90ed42eff31f56199399b2b0154ca")
552 .unwrap(),
553 ),
554 (
555 "default/linking/duplicate/Duplicate.t.sol:D".to_string(),
556 Address::from_str("0x47c5e40890bce4a473a49d7501808b9633f29782")
557 .unwrap(),
558 ),
559 (
560 "default/linking/duplicate/Duplicate.t.sol:E".to_string(),
561 Address::from_str("0x29b2440db4a256b0c1e6d3b4cdcaa68e2440a08f")
562 .unwrap(),
563 ),
564 ],
565 )
566 .assert_dependencies(
567 "default/linking/duplicate/Duplicate.t.sol:DuplicateLibraryLinkingTest"
568 .to_string(),
569 vec![
570 (
571 "default/linking/duplicate/Duplicate.t.sol:A".to_string(),
572 Address::from_str("0x5a443704dd4b594b382c22a083e2bd3090a6fef3")
573 .unwrap(),
574 ),
575 (
576 "default/linking/duplicate/Duplicate.t.sol:B".to_string(),
577 Address::from_str("0x47e9fbef8c83a1714f1951f142132e6e90f5fa5d")
578 .unwrap(),
579 ),
580 (
581 "default/linking/duplicate/Duplicate.t.sol:C".to_string(),
582 Address::from_str("0x8be503bcded90ed42eff31f56199399b2b0154ca")
583 .unwrap(),
584 ),
585 (
586 "default/linking/duplicate/Duplicate.t.sol:D".to_string(),
587 Address::from_str("0x47c5e40890bce4a473a49d7501808b9633f29782")
588 .unwrap(),
589 ),
590 (
591 "default/linking/duplicate/Duplicate.t.sol:E".to_string(),
592 Address::from_str("0x29b2440db4a256b0c1e6d3b4cdcaa68e2440a08f")
593 .unwrap(),
594 ),
595 ],
596 )
597 .test_with_sender_and_nonce(Address::default(), 1);
598 });
599 }
600
601 #[test]
602 fn link_cycle() {
603 link_test("../../testdata/default/linking/cycle", |linker| {
604 linker
605 .assert_dependencies(
606 "default/linking/cycle/Cycle.t.sol:Foo".to_string(),
607 vec![
608 (
609 "default/linking/cycle/Cycle.t.sol:Foo".to_string(),
610 Address::from_str("0x47e9Fbef8C83A1714F1951F142132E6e90F5fa5D")
611 .unwrap(),
612 ),
613 (
614 "default/linking/cycle/Cycle.t.sol:Bar".to_string(),
615 Address::from_str("0x5a443704dd4B594B382c22a083e2BD3090A6feF3")
616 .unwrap(),
617 ),
618 ],
619 )
620 .assert_dependencies(
621 "default/linking/cycle/Cycle.t.sol:Bar".to_string(),
622 vec![
623 (
624 "default/linking/cycle/Cycle.t.sol:Foo".to_string(),
625 Address::from_str("0x47e9Fbef8C83A1714F1951F142132E6e90F5fa5D")
626 .unwrap(),
627 ),
628 (
629 "default/linking/cycle/Cycle.t.sol:Bar".to_string(),
630 Address::from_str("0x5a443704dd4B594B382c22a083e2BD3090A6feF3")
631 .unwrap(),
632 ),
633 ],
634 )
635 .test_with_sender_and_nonce(Address::default(), 1);
636 });
637 }
638
639 #[test]
640 fn link_create2_nested() {
641 link_test("../../testdata/default/linking/nested", |linker| {
642 linker
643 .assert_dependencies("default/linking/nested/Nested.t.sol:Lib".to_string(), vec![])
644 .assert_dependencies(
645 "default/linking/nested/Nested.t.sol:NestedLib".to_string(),
646 vec![(
647 "default/linking/nested/Nested.t.sol:Lib".to_string(),
648 address!("0xddb1Cd2497000DAeA687CEa3dc34Af44084BEa74"),
649 )],
650 )
651 .assert_dependencies(
652 "default/linking/nested/Nested.t.sol:LibraryConsumer".to_string(),
653 vec![
654 (
657 "default/linking/nested/Nested.t.sol:Lib".to_string(),
658 Address::from_str("0xddb1Cd2497000DAeA687CEa3dc34Af44084BEa74")
659 .unwrap(),
660 ),
661 (
662 "default/linking/nested/Nested.t.sol:NestedLib".to_string(),
663 Address::from_str("0xfebE2F30641170642f317Ff6F644Cee60E7Ac369")
664 .unwrap(),
665 ),
666 ],
667 )
668 .assert_dependencies(
669 "default/linking/nested/Nested.t.sol:NestedLibraryLinkingTest".to_string(),
670 vec![
671 (
672 "default/linking/nested/Nested.t.sol:Lib".to_string(),
673 Address::from_str("0xddb1Cd2497000DAeA687CEa3dc34Af44084BEa74")
674 .unwrap(),
675 ),
676 (
677 "default/linking/nested/Nested.t.sol:NestedLib".to_string(),
678 Address::from_str("0xfebE2F30641170642f317Ff6F644Cee60E7Ac369")
679 .unwrap(),
680 ),
681 ],
682 )
683 .test_with_create2(
684 Address::default(),
685 fixed_bytes!(
686 "19bf59b7b67ae8edcbc6e53616080f61fa99285c061450ad601b0bc40c9adfc9"
687 ),
688 );
689 });
690 }
691}