Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions crytic_compile/compilation_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ def __init__(self, crytic_compile: "CryticCompile", unique_id: str):
)

# if the compilation unit comes from etherscan-like service and is a proxy,
# store the implementation address
self._implementation_address: str | None = None
# store the implementation addresses (e.g., diamond proxies can have multiple)
self._implementation_addresses: set[str] = set()

self._crytic_compile: CryticCompile = crytic_compile

Expand Down Expand Up @@ -137,22 +137,24 @@ def create_source_unit(self, filename: Filename) -> SourceUnit:
return self._source_units[filename]

@property
def implementation_address(self) -> str | None:
"""Return the implementation address if the compilation unit is a proxy
def implementation_addresses(self) -> set[str]:
"""Return the implementation addresses if the compilation unit is a proxy.

Diamond proxies can have multiple implementation addresses.

Returns:
Optional[str]: Implementation address
set[str]: Set of implementation addresses (empty if not a proxy)
"""
return self._implementation_address
return self._implementation_addresses

@implementation_address.setter
def implementation_address(self, implementation: str) -> None:
"""Set the implementation address
@implementation_addresses.setter
def implementation_addresses(self, implementations: set[str]) -> None:
"""Set the implementation addresses.

Args:
implementation (str): Implementation address
implementations: Set of implementation addresses
"""
self._implementation_address = implementation
self._implementation_addresses = implementations

# endregion
###################################################################################
Expand Down
2 changes: 1 addition & 1 deletion crytic_compile/platform/etherscan.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,7 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None:
implementation = str(result["Implementation"])
if target.startswith(tuple(SUPPORTED_NETWORK)):
implementation = f"{target[: target.find(':')]}:{implementation}"
compilation_unit.implementation_address = implementation
compilation_unit.implementation_addresses.add(implementation)

solc_standard_json.standalone_compile(
filenames,
Expand Down
49 changes: 47 additions & 2 deletions crytic_compile/platform/sourcify.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from typing import TYPE_CHECKING, Any
from urllib.error import HTTPError, URLError

from Crypto.Hash import keccak

from crytic_compile.compilation_unit import CompilationUnit
from crytic_compile.compiler.compiler import CompilerVersion
from crytic_compile.platform import solc_standard_json
Expand Down Expand Up @@ -57,6 +59,32 @@ def _parse_chain_id(chain_id_str: str) -> str:
return chain_id_str


def _to_checksum_address(addr: str) -> str:
"""Convert address to EIP-55 checksummed format.

Args:
addr: Hex address (with or without correct checksum)

Returns:
str: Properly checksummed address per EIP-55
"""
addr_lower = addr.lower().removeprefix("0x")
hash_obj = keccak.new(digest_bits=256)
hash_obj.update(addr_lower.encode("utf-8"))
addr_hash = hash_obj.hexdigest()

checksummed = []
for i, char in enumerate(addr_lower):
if char in "0123456789":
checksummed.append(char)
elif int(addr_hash[i], 16) >= 8:
checksummed.append(char.upper())
else:
checksummed.append(char.lower())

return "0x" + "".join(checksummed)


def _write_source_files(
sources: dict[str, dict[str, str]],
addr: str,
Expand Down Expand Up @@ -132,6 +160,7 @@ def _fetch_sourcify_data(chain_id: str, addr: str) -> dict[str, Any]:
"compilation.compilerVersion",
"compilation.compilerSettings",
"compilation.name",
"proxyResolution",
]
)
url = f"{SOURCIFY_API_BASE}/{chain_id}/{addr}?fields={fields}"
Expand All @@ -147,7 +176,12 @@ def _fetch_sourcify_data(chain_id: str, addr: str) -> dict[str, Any]:
raise InvalidCompilation(
f"Contract not verified on Sourcify: chain={chain_id} address={addr}"
) from e
raise InvalidCompilation(f"Sourcify API error: {e}") from e
try:
error = json.loads(e.readline().decode("utf-8"))
err_str = f"{error['customCode']}. {error['message']}; via {e}"
except Exception:
err_str = str(e)
raise InvalidCompilation(f"Sourcify API error: {err_str}") from e
except URLError as e:
raise InvalidCompilation(f"Failed to fetch from Sourcify: {e}") from e

Expand Down Expand Up @@ -224,7 +258,7 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None:
raise InvalidCompilation(f"Invalid Sourcify target format: {self._target}")

chain_id = _parse_chain_id(match.group(1))
addr = match.group(2)
addr = _to_checksum_address(match.group(2))

# Prepare export directory
export_dir = os.path.join(kwargs.get("export_dir", "crytic-export"), "sourcify-contracts")
Expand Down Expand Up @@ -265,6 +299,17 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None:
)
compilation_unit.compiler_version.look_for_installed_version()

# Handle proxy resolution if available
proxy_resolution = data.get("proxyResolution")
if proxy_resolution and proxy_resolution.get("isProxy"):
implementations = proxy_resolution.get("implementations", [])
for impl in implementations:
impl_addr = impl.get("address")
if impl_addr:
# Format as sourcify-{chainId}:{address} for direct use as crytic-compile target
formatted_addr = f"sourcify-{chain_id}:{_to_checksum_address(impl_addr)}"
compilation_unit.implementation_addresses.add(formatted_addr)

solc_standard_json.standalone_compile(
filenames,
compilation_unit,
Expand Down
Loading