Skip to content

Commit b8ecb8f

Browse files
committed
feat: --compile-autolink
This implements automatic dependency resolution and linking of libraries through crytic-compile, using the existing internal mechanism provided by `--compile-libraries`. The chosen library deployment addresses are provided on a `<key>.link` JSON file when using the solc export format. This can be then used by fuzzers or other tools that would like to deploy contracts that require external libraries.
1 parent bd7a221 commit b8ecb8f

File tree

7 files changed

+456
-11
lines changed

7 files changed

+456
-11
lines changed

crytic_compile/crytic_compile.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from crytic_compile.platform.all_export import PLATFORMS_EXPORT
2828
from crytic_compile.platform.solc import Solc
2929
from crytic_compile.platform.standard import export_to_standard
30+
from crytic_compile.utils.libraries import generate_library_addresses, get_deployment_order
3031
from crytic_compile.utils.naming import Filename
3132
from crytic_compile.utils.npm import get_package_name
3233
from crytic_compile.utils.zip import load_from_zip
@@ -206,6 +207,10 @@ def __init__(self, target: Union[str, AbstractPlatform], **kwargs: str) -> None:
206207

207208
self._bytecode_only = False
208209

210+
self._autolink: bool = kwargs.get("compile_autolink", False) # type: ignore
211+
212+
self._autolink_deployment_order: Optional[List[str]] = None
213+
209214
self.libraries: Optional[Dict[str, int]] = _extract_libraries(kwargs.get("compile_libraries", None)) # type: ignore
210215

211216
self._compile(**kwargs)
@@ -632,12 +637,59 @@ def _compile(self, **kwargs: str) -> None:
632637
self._platform.clean(**kwargs)
633638
self._platform.compile(self, **kwargs)
634639

640+
# Handle autolink after compilation
641+
if self._autolink:
642+
self._apply_autolink()
643+
635644
remove_metadata = kwargs.get("compile_remove_metadata", False)
636645
if remove_metadata:
637646
for compilation_unit in self._compilation_units.values():
638647
for source_unit in compilation_unit.source_units.values():
639648
source_unit.remove_metadata()
640649

650+
def _apply_autolink(self) -> None:
651+
"""Apply automatic library linking with sequential addresses"""
652+
653+
# Collect all libraries that need linking and compute deployment info
654+
all_libraries_needed: Set[str] = set()
655+
all_dependencies: Dict[str, List[str]] = {}
656+
all_target_contracts: List[str] = []
657+
658+
for compilation_unit in self._compilation_units.values():
659+
# Build dependency graph for this compilation unit
660+
for source_unit in compilation_unit.source_units.values():
661+
all_target_contracts.extend(source_unit.contracts_names_without_libraries)
662+
663+
for contract_name in source_unit.contracts_names:
664+
deps = source_unit.libraries_names(contract_name)
665+
666+
if deps or contract_name in all_target_contracts:
667+
all_dependencies[contract_name] = deps
668+
all_libraries_needed.update(deps)
669+
670+
# Calculate deployment order globally
671+
deployment_order, _ = get_deployment_order(all_dependencies, all_target_contracts)
672+
self._autolink_deployment_order = deployment_order
673+
674+
if all_libraries_needed:
675+
# Apply the library linking (similar to compile_libraries but auto-generated)
676+
library_addresses = generate_library_addresses(all_libraries_needed)
677+
678+
if self.libraries is None:
679+
self.libraries = {}
680+
681+
# Respect any user-provided addresses through compile_libraries
682+
self.libraries = library_addresses | self.libraries
683+
684+
@property
685+
def deployment_order(self) -> Optional[List[str]]:
686+
"""Return the library deployment order.
687+
688+
Returns:
689+
Optional[List[str]]: Library deployment order
690+
"""
691+
return self._autolink_deployment_order
692+
641693
@staticmethod
642694
def _run_custom_build(custom_build: str) -> None:
643695
"""Run a custom build

crytic_compile/cryticparser/cryticparser.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ def init(parser: ArgumentParser) -> None:
3535
default=DEFAULTS_FLAG_IN_CONFIG["compile_libraries"],
3636
)
3737

38+
group_compile.add_argument(
39+
"--compile-autolink",
40+
help="Automatically link all found libraries with sequential addresses starting from 0xa070",
41+
action="store_true",
42+
default=DEFAULTS_FLAG_IN_CONFIG["compile_autolink"],
43+
)
44+
3845
group_compile.add_argument(
3946
"--compile-remove-metadata",
4047
help="Remove the metadata from the bytecodes",

crytic_compile/cryticparser/defaults.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,5 @@
4848
"foundry_compile_all": False,
4949
"export_dir": "crytic-export",
5050
"compile_libraries": None,
51+
"compile_autolink": False,
5152
}

crytic_compile/platform/solc.py

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,46 @@ def _build_contract_data(compilation_unit: "CompilationUnit") -> Dict:
5757
return contracts
5858

5959

60+
def _export_link_info(compilation_unit: "CompilationUnit", key: str, export_dir: str) -> str:
61+
"""Export linking information to a separate file.
62+
63+
Args:
64+
compilation_unit (CompilationUnit): Compilation unit to export
65+
key (str): Filename Id
66+
export_dir (str): Export directory
67+
68+
Returns:
69+
str: path to the generated file"""
70+
71+
autolink_path = os.path.join(export_dir, f"{key}.link")
72+
73+
# Get library addresses if they exist
74+
library_addresses = {}
75+
if compilation_unit.crytic_compile.libraries:
76+
library_addresses = {
77+
name: f"0x{addr:040x}"
78+
for name, addr in compilation_unit.crytic_compile.libraries.items()
79+
}
80+
81+
# Filter deployment order to only include libraries that have addresses
82+
full_deployment_order = compilation_unit.crytic_compile.deployment_order or []
83+
filtered_deployment_order = [lib for lib in full_deployment_order if lib in library_addresses]
84+
85+
# Create autolink output with deployment order and library addresses
86+
autolink_output = {
87+
"deployment_order": filtered_deployment_order,
88+
"library_addresses": library_addresses,
89+
}
90+
91+
with open(autolink_path, "w", encoding="utf8") as file_desc:
92+
json.dump(autolink_output, file_desc, indent=2)
93+
94+
return autolink_path
95+
96+
6097
def export_to_solc_from_compilation_unit(
6198
compilation_unit: "CompilationUnit", key: str, export_dir: str
62-
) -> Optional[str]:
99+
) -> Optional[List[str]]:
63100
"""Export the compilation unit to the standard solc output format.
64101
The exported file will be $key.json
65102
@@ -69,7 +106,7 @@ def export_to_solc_from_compilation_unit(
69106
export_dir (str): Export directory
70107
71108
Returns:
72-
Optional[str]: path to the file generated
109+
Optional[List[str]]: path to the files generated
73110
"""
74111
contracts = _build_contract_data(compilation_unit)
75112

@@ -88,7 +125,15 @@ def export_to_solc_from_compilation_unit(
88125

89126
with open(path, "w", encoding="utf8") as file_desc:
90127
json.dump(output, file_desc)
91-
return path
128+
129+
paths = [path]
130+
131+
# Export link info if compile_autolink or compile_libraries was used
132+
if compilation_unit.crytic_compile.libraries:
133+
link_path = _export_link_info(compilation_unit, key, export_dir)
134+
paths.append(link_path)
135+
136+
return paths
92137
return None
93138

94139

@@ -110,17 +155,18 @@ def export_to_solc(crytic_compile: "CryticCompile", **kwargs: str) -> List[str]:
110155

111156
if len(crytic_compile.compilation_units) == 1:
112157
compilation_unit = list(crytic_compile.compilation_units.values())[0]
113-
path = export_to_solc_from_compilation_unit(compilation_unit, "combined_solc", export_dir)
114-
if path:
115-
return [path]
158+
paths = export_to_solc_from_compilation_unit(compilation_unit, "combined_solc", export_dir)
159+
if paths:
160+
return paths
116161
return []
117162

118-
paths = []
163+
all_paths = []
119164
for key, compilation_unit in crytic_compile.compilation_units.items():
120-
path = export_to_solc_from_compilation_unit(compilation_unit, key, export_dir)
121-
if path:
122-
paths.append(path)
123-
return paths
165+
paths = export_to_solc_from_compilation_unit(compilation_unit, key, export_dir)
166+
if paths:
167+
all_paths.extend(paths)
168+
169+
return all_paths
124170

125171

126172
class Solc(AbstractPlatform):

crytic_compile/utils/libraries.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""
2+
Library utilities for dependency resolution and auto-linking
3+
"""
4+
from typing import Dict, List, Set, Tuple
5+
6+
7+
def get_deployment_order(
8+
dependencies: Dict[str, List[str]], target_contracts: List[str]
9+
) -> Tuple[List[str], Set[str]]:
10+
"""Get deployment order using topological sorting (Kahn's algorithm)
11+
12+
Args:
13+
dependencies: Dict mapping contract_name -> [required_libraries]
14+
target_contracts: List of target contracts to prioritize
15+
16+
Raises:
17+
ValueError: if a circular dependency is identified
18+
19+
Returns:
20+
Tuple of (deployment_order, libraries_needed)
21+
"""
22+
# Build complete dependency graph
23+
all_contracts = set(dependencies.keys())
24+
for deps in dependencies.values():
25+
all_contracts.update(deps)
26+
27+
# Calculate in-degrees
28+
in_degree = {contract: 0 for contract in all_contracts}
29+
for contract, deps in dependencies.items():
30+
for dep in deps:
31+
if dep in in_degree:
32+
in_degree[contract] += 1
33+
34+
# Initialize queue with nodes that have no dependencies
35+
queue = [contract for contract in all_contracts if in_degree[contract] == 0]
36+
37+
result = []
38+
libraries_needed = set()
39+
40+
deployment_order = []
41+
42+
while queue:
43+
# Sort queue to prioritize libraries first, then target contracts in order
44+
queue.sort(
45+
key=lambda x: (
46+
x in target_contracts, # Libraries (False) come before targets (True)
47+
target_contracts.index(x) if x in target_contracts else 0, # Target order
48+
)
49+
)
50+
51+
current = queue.pop(0)
52+
result.append(current)
53+
54+
# Check if this is a library (not in target contracts but required by others)
55+
if current not in target_contracts:
56+
libraries_needed.add(current)
57+
deployment_order.append(current) # Only add libraries to deployment order
58+
59+
# Update in-degrees for dependents
60+
for contract, deps in dependencies.items():
61+
if current in deps:
62+
in_degree[contract] -= 1
63+
if in_degree[contract] == 0 and contract not in result:
64+
queue.append(contract)
65+
66+
# Check for circular dependencies
67+
if len(result) != len(all_contracts):
68+
remaining = all_contracts - set(result)
69+
raise ValueError(f"Circular dependency detected involving: {remaining}")
70+
71+
return deployment_order, libraries_needed
72+
73+
74+
def generate_library_addresses(
75+
libraries_needed: Set[str], start_address: int = 0xA070
76+
) -> Dict[str, int]:
77+
"""Generate sequential addresses for libraries
78+
79+
Args:
80+
libraries_needed: Set of library names that need addresses
81+
start_address: Starting address (default 0xa070, resembling "auto")
82+
83+
Returns:
84+
Dict mapping library_name -> address
85+
"""
86+
library_addresses = {}
87+
current_address = start_address
88+
89+
# Sort libraries for consistent ordering
90+
for library in sorted(libraries_needed):
91+
library_addresses[library] = current_address
92+
current_address += 1
93+
94+
return library_addresses

tests/library_dependency_test.sol

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
// Simple library with no dependencies
5+
library MathLib {
6+
function add(uint256 a, uint256 b) external pure returns (uint256) {
7+
return a + b;
8+
}
9+
10+
function multiply(uint256 a, uint256 b) external pure returns (uint256) {
11+
return a * b;
12+
}
13+
}
14+
15+
// Library that depends on MathLib
16+
library AdvancedMath {
17+
function square(uint256 a) external pure returns (uint256) {
18+
return MathLib.multiply(a, a);
19+
}
20+
21+
function addAndSquare(uint256 a, uint256 b) external pure returns (uint256) {
22+
uint256 sum = MathLib.add(a, b);
23+
return MathLib.multiply(sum, sum);
24+
}
25+
}
26+
27+
// Library that depends on both MathLib and AdvancedMath
28+
library ComplexMath {
29+
function complexOperation(uint256 a, uint256 b) external pure returns (uint256) {
30+
uint256 squared = AdvancedMath.square(a);
31+
return MathLib.add(squared, b);
32+
}
33+
34+
function megaOperation(uint256 a, uint256 b, uint256 c) external pure returns (uint256) {
35+
uint256 result1 = AdvancedMath.addAndSquare(a, b);
36+
uint256 result2 = MathLib.multiply(result1, c);
37+
return result2;
38+
}
39+
}
40+
41+
// Contract that uses ComplexMath (which transitively depends on others)
42+
contract TestComplexDependencies {
43+
uint256 public result;
44+
45+
constructor() {
46+
result = 0;
47+
}
48+
49+
function performComplexCalculation(uint256 a, uint256 b, uint256 c) public {
50+
result = ComplexMath.megaOperation(a, b, c);
51+
}
52+
53+
function performSimpleCalculation(uint256 a, uint256 b) public {
54+
result = ComplexMath.complexOperation(a, b);
55+
}
56+
57+
function getResult() public view returns (uint256) {
58+
return result;
59+
}
60+
}
61+
62+
// Another contract that only uses MathLib directly
63+
contract SimpleMathContract {
64+
uint256 public value;
65+
66+
constructor(uint256 _initial) {
67+
value = _initial;
68+
}
69+
70+
function addValue(uint256 _amount) public {
71+
value = MathLib.add(value, _amount);
72+
}
73+
74+
function multiplyValue(uint256 _factor) public {
75+
value = MathLib.multiply(value, _factor);
76+
}
77+
}
78+
79+
// Contract that uses multiple libraries at the same level
80+
contract MultiLibraryContract {
81+
uint256 public simpleResult;
82+
uint256 public advancedResult;
83+
84+
function calculate(uint256 a, uint256 b) public {
85+
simpleResult = MathLib.add(a, b);
86+
advancedResult = AdvancedMath.square(a);
87+
}
88+
}

0 commit comments

Comments
 (0)