Skip to content

Commit 9e9cea7

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 9e9cea7

File tree

7 files changed

+438
-11
lines changed

7 files changed

+438
-11
lines changed

crytic_compile/crytic_compile.py

Lines changed: 47 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)
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,54 @@ 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 self._autolink_deployment_order
687+
641688
@staticmethod
642689
def _run_custom_build(custom_build: str) -> None:
643690
"""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: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,34 @@ def _build_contract_data(compilation_unit: "CompilationUnit") -> Dict:
5757
return contracts
5858

5959

60+
def _export_link_info(compilation_unit: "CompilationUnit", key: str, autolink_path: str) -> None:
61+
"""Export linking information to a separate file"""
62+
63+
# Get library addresses if they exist
64+
library_addresses = {}
65+
if compilation_unit.crytic_compile.libraries:
66+
library_addresses = {
67+
name: f"0x{addr:040x}"
68+
for name, addr in compilation_unit.crytic_compile.libraries.items()
69+
}
70+
71+
# Filter deployment order to only include libraries that have addresses
72+
full_deployment_order = compilation_unit.crytic_compile.deployment_order or []
73+
filtered_deployment_order = [lib for lib in full_deployment_order if lib in library_addresses]
74+
75+
# Create autolink output with deployment order and library addresses
76+
autolink_output = {
77+
"deployment_order": filtered_deployment_order,
78+
"library_addresses": library_addresses,
79+
}
80+
81+
with open(autolink_path, "w", encoding="utf8") as file_desc:
82+
json.dump(autolink_output, file_desc, indent=2)
83+
84+
6085
def export_to_solc_from_compilation_unit(
6186
compilation_unit: "CompilationUnit", key: str, export_dir: str
62-
) -> Optional[str]:
87+
) -> Optional[List[str]]:
6388
"""Export the compilation unit to the standard solc output format.
6489
The exported file will be $key.json
6590
@@ -69,7 +94,7 @@ def export_to_solc_from_compilation_unit(
6994
export_dir (str): Export directory
7095
7196
Returns:
72-
Optional[str]: path to the file generated
97+
Optional[List[str]]: path to the files generated
7398
"""
7499
contracts = _build_contract_data(compilation_unit)
75100

@@ -88,7 +113,16 @@ def export_to_solc_from_compilation_unit(
88113

89114
with open(path, "w", encoding="utf8") as file_desc:
90115
json.dump(output, file_desc)
91-
return path
116+
117+
paths = [path]
118+
119+
# Export link info if compile_autolink or compile_libraries was used
120+
if compilation_unit.crytic_compile.libraries:
121+
link_path = os.path.join(export_dir, f"{key}.link")
122+
_export_link_info(compilation_unit, key, link_path)
123+
paths.append(link_path)
124+
125+
return paths
92126
return None
93127

94128

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

111145
if len(crytic_compile.compilation_units) == 1:
112146
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]
147+
paths = export_to_solc_from_compilation_unit(compilation_unit, "combined_solc", export_dir)
148+
if paths:
149+
return paths
116150
return []
117151

118-
paths = []
152+
all_paths = []
119153
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
154+
paths = export_to_solc_from_compilation_unit(compilation_unit, key, export_dir)
155+
if paths:
156+
all_paths.extend(paths)
157+
158+
return all_paths
124159

125160

126161
class Solc(AbstractPlatform):

crytic_compile/utils/libraries.py

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