Skip to content

Commit 049a96d

Browse files
elopezdguido
authored andcommitted
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 079330f commit 049a96d

File tree

7 files changed

+456
-11
lines changed

7 files changed

+456
-11
lines changed

crytic_compile/crytic_compile.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from crytic_compile.platform.solc_standard_json import SolcStandardJson
3030
from crytic_compile.platform.standard import export_to_standard
3131
from crytic_compile.platform.vyper import VyperStandardJson
32+
from crytic_compile.utils.libraries import generate_library_addresses, get_deployment_order
3233
from crytic_compile.utils.naming import Filename
3334
from crytic_compile.utils.npm import get_package_name
3435
from crytic_compile.utils.zip import load_from_zip
@@ -201,6 +202,10 @@ def __init__(self, target: str | AbstractPlatform, **kwargs: str) -> None:
201202

202203
self._bytecode_only = False
203204

205+
self._autolink: bool = kwargs.get("compile_autolink", False) # type: ignore
206+
207+
self._autolink_deployment_order: list[str] | None = None
208+
204209
self.libraries: dict[str, int] | None = _extract_libraries(
205210
kwargs.get("compile_libraries", None)
206211
)
@@ -628,12 +633,60 @@ def _compile(self, **kwargs: str) -> None:
628633
self._platform.clean(**kwargs)
629634
self._platform.compile(self, **kwargs)
630635

636+
# Handle autolink after compilation
637+
if self._autolink:
638+
self._apply_autolink()
639+
631640
remove_metadata = kwargs.get("compile_remove_metadata", False)
632641
if remove_metadata:
633642
for compilation_unit in self._compilation_units.values():
634643
for source_unit in compilation_unit.source_units.values():
635644
source_unit.remove_metadata()
636645

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

crytic_compile/cryticparser/cryticparser.py

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

39+
group_compile.add_argument(
40+
"--compile-autolink",
41+
help="Automatically link all found libraries with sequential addresses starting from 0xa070",
42+
action="store_true",
43+
default=DEFAULTS_FLAG_IN_CONFIG["compile_autolink"],
44+
)
45+
3946
group_compile.add_argument(
4047
"--compile-remove-metadata",
4148
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
@@ -49,4 +49,5 @@
4949
"foundry_deny": None,
5050
"export_dir": "crytic-export",
5151
"compile_libraries": None,
52+
"compile_autolink": False,
5253
}

crytic_compile/platform/solc.py

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

6060

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

@@ -89,7 +126,15 @@ def export_to_solc_from_compilation_unit(
89126

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

95140

@@ -111,17 +156,18 @@ def export_to_solc(crytic_compile: "CryticCompile", **kwargs: str) -> list[str]:
111156

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

119-
paths = []
164+
all_paths = []
120165
for key, compilation_unit in crytic_compile.compilation_units.items():
121-
path = export_to_solc_from_compilation_unit(compilation_unit, key, export_dir)
122-
if path:
123-
paths.append(path)
124-
return paths
166+
paths = export_to_solc_from_compilation_unit(compilation_unit, key, export_dir)
167+
if paths:
168+
all_paths.extend(paths)
169+
170+
return all_paths
125171

126172

127173
class Solc(AbstractPlatform):

crytic_compile/utils/libraries.py

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