Skip to content

Commit aaeb2a3

Browse files
authored
Merge pull request #583 from crytic/dev
Sync Master <> Dev
2 parents 20df04f + 2d02b9d commit aaeb2a3

File tree

14 files changed

+169
-211
lines changed

14 files changed

+169
-211
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@ jobs:
6363
solc-select use 0.5.7 --always-install
6464
- name: Set up nix
6565
if: matrix.type == 'dapp'
66-
uses: cachix/install-nix-action@v25
66+
uses: cachix/install-nix-action@v30
6767
- name: Set up cachix
6868
if: matrix.type == 'dapp'
69-
uses: cachix/cachix-action@v14
69+
uses: cachix/cachix-action@v15
7070
with:
7171
name: dapp
7272
- name: Install Foundry
@@ -75,7 +75,6 @@ jobs:
7575
- name: Run Tests
7676
env:
7777
TEST_TYPE: ${{ matrix.type }}
78-
GITHUB_ETHERSCAN: ${{ secrets.GITHUB_ETHERSCAN }}
7978
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
8079
shell: bash
8180
run: |

.github/workflows/etherscan.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ jobs:
4242
- name: Run Tests
4343
env:
4444
TEST_TYPE: ${{ matrix.type }}
45-
GITHUB_ETHERSCAN: ${{ secrets.GITHUB_ETHERSCAN }}
45+
ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}
4646
shell: bash
4747
run: |
4848
bash "scripts/ci_test_${TEST_TYPE}.sh"

.github/workflows/publish.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ jobs:
4545
path: dist/
4646

4747
- name: publish
48-
uses: pypa/gh-action-pypi-publish@v1.8.14
48+
uses: pypa/gh-action-pypi-publish@v1.12.3
4949

5050
- name: sign
51-
uses: sigstore/gh-action-sigstore-python@v2.1.1
51+
uses: sigstore/gh-action-sigstore-python@v3.0.0
5252
with:
5353
inputs: ./dist/*.tar.gz ./dist/*.whl
5454
release-signing-artifacts: true

.github/workflows/pytest.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,7 @@ jobs:
4141
solc-select use latest --always-install
4242
4343
- name: Run Tests
44+
env:
45+
ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}
4446
run: |
4547
pytest tests

crytic_compile/cryticparser/cryticparser.py

Lines changed: 0 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -329,30 +329,6 @@ def _init_etherscan(parser: ArgumentParser) -> None:
329329
default=DEFAULTS_FLAG_IN_CONFIG["etherscan_api_key"],
330330
)
331331

332-
group_etherscan.add_argument(
333-
"--arbiscan-apikey",
334-
help="Etherscan API key.",
335-
action="store",
336-
dest="arbiscan_api_key",
337-
default=DEFAULTS_FLAG_IN_CONFIG["etherscan_api_key"],
338-
)
339-
340-
group_etherscan.add_argument(
341-
"--polygonscan-apikey",
342-
help="Etherscan API key.",
343-
action="store",
344-
dest="polygonscan_api_key",
345-
default=DEFAULTS_FLAG_IN_CONFIG["etherscan_api_key"],
346-
)
347-
348-
group_etherscan.add_argument(
349-
"--test-polygonscan-apikey",
350-
help="Etherscan API key.",
351-
action="store",
352-
dest="test_polygonscan_api_key",
353-
default=DEFAULTS_FLAG_IN_CONFIG["etherscan_api_key"],
354-
)
355-
356332
group_etherscan.add_argument(
357333
"--avax-apikey",
358334
help="Etherscan API key.",
@@ -361,62 +337,6 @@ def _init_etherscan(parser: ArgumentParser) -> None:
361337
default=DEFAULTS_FLAG_IN_CONFIG["etherscan_api_key"],
362338
)
363339

364-
group_etherscan.add_argument(
365-
"--ftmscan-apikey",
366-
help="Etherscan API key.",
367-
action="store",
368-
dest="ftmscan_api_key",
369-
default=DEFAULTS_FLAG_IN_CONFIG["etherscan_api_key"],
370-
)
371-
372-
group_etherscan.add_argument(
373-
"--bscan-apikey",
374-
help="Etherscan API key.",
375-
action="store",
376-
dest="bscan_api_key",
377-
default=DEFAULTS_FLAG_IN_CONFIG["etherscan_api_key"],
378-
)
379-
380-
group_etherscan.add_argument(
381-
"--optim-apikey",
382-
help="Optimistic API key.",
383-
action="store",
384-
dest="optim_api_key",
385-
default=DEFAULTS_FLAG_IN_CONFIG["etherscan_api_key"],
386-
)
387-
388-
group_etherscan.add_argument(
389-
"--base-apikey",
390-
help="Basescan API key.",
391-
action="store",
392-
dest="base_api_key",
393-
default=DEFAULTS_FLAG_IN_CONFIG["etherscan_api_key"],
394-
)
395-
396-
group_etherscan.add_argument(
397-
"--gno-apikey",
398-
help="Gnosisscan API key.",
399-
action="store",
400-
dest="gno_api_key",
401-
default=DEFAULTS_FLAG_IN_CONFIG["etherscan_api_key"],
402-
)
403-
404-
group_etherscan.add_argument(
405-
"--polyzk-apikey",
406-
help="zkEVM Polygonscan API key.",
407-
action="store",
408-
dest="polyzk_api_key",
409-
default=DEFAULTS_FLAG_IN_CONFIG["etherscan_api_key"],
410-
)
411-
412-
group_etherscan.add_argument(
413-
"--blast-apikey",
414-
help="Blastscan API key.",
415-
action="store",
416-
dest="blast_api_key",
417-
default=DEFAULTS_FLAG_IN_CONFIG["etherscan_api_key"],
418-
)
419-
420340
group_etherscan.add_argument(
421341
"--etherscan-export-directory",
422342
help="Directory in which to save the analyzed contracts.",

crytic_compile/platform/etherscan.py

Lines changed: 122 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -27,33 +27,102 @@
2727
LOGGER = logging.getLogger("CryticCompile")
2828

2929

30-
ETHERSCAN_BASE = "https://api%s/api?module=contract&action=getsourcecode&address=%s"
30+
# Etherscan v1 API style (per-scanner URL)
31+
ETHERSCAN_BASE_V1 = "https://api%s/api?module=contract&action=getsourcecode&address=%s"
3132

33+
# Etherscan v2 API style (unified)
34+
ETHERSCAN_BASE_V2 = (
35+
"https://api.etherscan.io/v2/api?chainid=%s&module=contract&action=getsourcecode&address=%s"
36+
)
37+
38+
# Bytecode URL style (for scraping)
3239
ETHERSCAN_BASE_BYTECODE = "https://%s/address/%s#code"
3340

34-
SUPPORTED_NETWORK = {
35-
# Key, (prefix_base, perfix_bytecode)
36-
"mainet:": (".etherscan.io", "etherscan.io"),
37-
"optim:": ("-optimistic.etherscan.io", "optimistic.etherscan.io"),
38-
"goerli:": ("-goerli.etherscan.io", "goerli.etherscan.io"),
39-
"sepolia:": ("-sepolia.etherscan.io", "sepolia.etherscan.io"),
40-
"tobalaba:": ("-tobalaba.etherscan.io", "tobalaba.etherscan.io"),
41-
"bsc:": (".bscscan.com", "bscscan.com"),
42-
"testnet.bsc:": ("-testnet.bscscan.com", "testnet.bscscan.com"),
43-
"arbi:": (".arbiscan.io", "arbiscan.io"),
44-
"testnet.arbi:": ("-testnet.arbiscan.io", "testnet.arbiscan.io"),
45-
"poly:": (".polygonscan.com", "polygonscan.com"),
46-
"mumbai:": ("-testnet.polygonscan.com", "testnet.polygonscan.com"),
47-
"avax:": (".snowtrace.io", "snowtrace.io"),
48-
"testnet.avax:": ("-testnet.snowtrace.io", "testnet.snowtrace.io"),
49-
"ftm:": (".ftmscan.com", "ftmscan.com"),
50-
"goerli.base:": ("-goerli.basescan.org", "goerli.basescan.org"),
51-
"base:": (".basescan.org", "basescan.org"),
52-
"gno:": (".gnosisscan.io", "gnosisscan.io"),
53-
"polyzk:": ("-zkevm.polygonscan.com", "zkevm.polygonscan.com"),
54-
"blast:": (".blastscan.io", "blastscan.io"),
41+
# v1 style scanners
42+
SUPPORTED_NETWORK_V1: Dict[str, Tuple[str, str]] = {
43+
# None at this time. External tracer instances not operated by Etherscan would be here
44+
}
45+
46+
# v2 style scanners
47+
SUPPORTED_NETWORK_V2: Dict[str, Tuple[str, str]] = {
48+
# Key, (chainid, perfix_bytecode)
49+
"mainnet": ("1", "etherscan.io"),
50+
"sepolia": ("11155111", "sepolia.etherscan.io"),
51+
"holesky": ("17000", "holesky.etherscan.io"),
52+
"bsc": ("56", "bscscan.com"),
53+
"testnet.bsc": ("97", "testnet.bscscan.com"),
54+
"poly": ("137", "polygonscan.com"),
55+
"amoy.poly": ("80002", "amoy.polygonscan.com"),
56+
"polyzk": ("1101", "zkevm.polygonscan.com"),
57+
"cardona.polyzk": ("2442", "cardona-zkevm.polygonscan.com"),
58+
"base": ("8453", "basescan.org"),
59+
"sepolia.base": ("84532", "sepolia.basescan.org"),
60+
"arbi": ("42161", "arbiscan.io"),
61+
"nova.arbi": ("42170", "nova.arbiscan.io"),
62+
"sepolia.arbi": ("421614", "sepolia.arbiscan.io"),
63+
"linea": ("59144", "lineascan.build"),
64+
"sepolia.linea": ("59141", "sepolia.lineascan.build"),
65+
"ftm": ("250", "ftmscan.com"),
66+
"testnet.ftm": ("4002", "testnet.ftmscan.com"),
67+
"blast": ("81457", "blastscan.io"),
68+
"sepolia.blast": ("168587773", "sepolia.blastscan.io"),
69+
"optim": ("10", "optimistic.etherscan.io"),
70+
"sepolia.optim": ("11155420", "sepolia-optimism.etherscan.io"),
71+
"avax": ("43114", "snowscan.xyz"),
72+
"testnet.avax": ("43113", "testnet.snowscan.xyz"),
73+
"bttc": ("199", "bttcscan.com"),
74+
"testnet.bttc": ("1028", "testnet.bttcscan.com"),
75+
"celo": ("42220", "celoscan.io"),
76+
"alfajores.celo": ("44787", "alfajores.celoscan.io"),
77+
"cronos": ("25", "cronoscan.com"),
78+
"frax": ("252", "fraxscan.com"),
79+
"holesky.frax": ("2522", "holesky.fraxscan.com"),
80+
"gno": ("100", "gnosisscan.io"),
81+
"kroma": ("255", "kromascan.com"),
82+
"sepolia.kroma": ("2358", "sepolia.kromascan.com"),
83+
"mantle": ("5000", "mantlescan.xyz"),
84+
"sepolia.mantle": ("5003", "sepolia.mantlescan.xyz"),
85+
"moonbeam": ("1284", "moonbeam.moonscan.io"),
86+
"moonriver": ("1285", "moonriver.moonscan.io"),
87+
"moonbase": ("1287", "moonbase.moonscan.io"),
88+
"opbnb": ("204", "opbnb.bscscan.com"),
89+
"testnet.opbnb": ("5611", "opbnb-testnet.bscscan.com"),
90+
"scroll": ("534352", "scrollscan.com"),
91+
"sepolia.scroll": ("534351", "sepolia.scrollscan.com"),
92+
"taiko": ("167000", "taikoscan.io"),
93+
"hekla.taiko": ("167009", "hekla.taikoscan.io"),
94+
"wemix": ("1111", "wemixscan.com"),
95+
"testnet.wemix": ("1112", "testnet.wemixscan.com"),
96+
"era.zksync": ("324", "era.zksync.network"),
97+
"sepoliaera.zksync": ("300", "sepolia-era.zksync.network"),
98+
"xai": ("660279", "xaiscan.io"),
99+
"sepolia.xai": ("37714555429", "sepolia.xaiscan.io"),
55100
}
56101

102+
SUPPORTED_NETWORK = {**SUPPORTED_NETWORK_V1, **SUPPORTED_NETWORK_V2}
103+
104+
105+
def generate_supported_network_v2_list() -> None:
106+
"""Manual function to generate a dictionary for updating the SUPPORTED_NETWORK_V2 array"""
107+
108+
with urllib.request.urlopen("https://api.etherscan.io/v2/chainlist") as response:
109+
items = response.read()
110+
networks = json.loads(items)
111+
112+
id2name = {}
113+
for name, (chainid, _) in SUPPORTED_NETWORK_V2.items():
114+
id2name[chainid] = name
115+
116+
results = {}
117+
for network in networks["result"]:
118+
name = id2name.get(network["chainid"], f"{network['chainid']}")
119+
results[name] = (
120+
network["chainid"],
121+
network["blockexplorer"].replace("https://", "").strip("/"),
122+
)
123+
124+
print(results)
125+
57126

58127
def _handle_bytecode(crytic_compile: "CryticCompile", target: str, result_b: bytes) -> None:
59128
"""Parse the bytecode and populate CryticCompile info
@@ -215,15 +284,24 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None:
215284

216285
target = self._target
217286

218-
if target.startswith(tuple(SUPPORTED_NETWORK)):
219-
prefix: Union[None, str] = SUPPORTED_NETWORK[target[: target.find(":") + 1]][0]
220-
prefix_bytecode = SUPPORTED_NETWORK[target[: target.find(":") + 1]][1]
287+
api_key_required = None
288+
289+
if target.startswith(tuple(SUPPORTED_NETWORK_V2)):
290+
api_key_required = 2
291+
prefix, addr = target.split(":", 2)
292+
chainid, prefix_bytecode = SUPPORTED_NETWORK_V2[prefix]
293+
etherscan_url = ETHERSCAN_BASE_V2 % (chainid, addr)
294+
etherscan_bytecode_url = ETHERSCAN_BASE_BYTECODE % (prefix_bytecode, addr)
295+
elif target.startswith(tuple(SUPPORTED_NETWORK_V1)):
296+
api_key_required = 1
297+
prefix = SUPPORTED_NETWORK_V1[target[: target.find(":") + 1]][0]
298+
prefix_bytecode = SUPPORTED_NETWORK_V1[target[: target.find(":") + 1]][1]
221299
addr = target[target.find(":") + 1 :]
222-
etherscan_url = ETHERSCAN_BASE % (prefix, addr)
300+
etherscan_url = ETHERSCAN_BASE_V1 % (prefix, addr)
223301
etherscan_bytecode_url = ETHERSCAN_BASE_BYTECODE % (prefix_bytecode, addr)
224-
225302
else:
226-
etherscan_url = ETHERSCAN_BASE % (".etherscan.io", target)
303+
api_key_required = 2
304+
etherscan_url = ETHERSCAN_BASE_V2 % ("1", target)
227305
etherscan_bytecode_url = ETHERSCAN_BASE_BYTECODE % ("etherscan.io", target)
228306
addr = target
229307
prefix = None
@@ -232,75 +310,36 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None:
232310
only_bytecode = kwargs.get("etherscan_only_bytecode", False)
233311

234312
etherscan_api_key = kwargs.get("etherscan_api_key", None)
235-
arbiscan_api_key = kwargs.get("arbiscan_api_key", None)
236-
polygonscan_api_key = kwargs.get("polygonscan_api_key", None)
237-
test_polygonscan_api_key = kwargs.get("test_polygonscan_api_key", None)
238-
avax_api_key = kwargs.get("avax_api_key", None)
239-
ftmscan_api_key = kwargs.get("ftmscan_api_key", None)
240-
bscan_api_key = kwargs.get("bscan_api_key", None)
241-
optim_api_key = kwargs.get("optim_api_key", None)
242-
base_api_key = kwargs.get("base_api_key", None)
243-
gno_api_key = kwargs.get("gno_api_key", None)
244-
polyzk_api_key = kwargs.get("polyzk_api_key", None)
245-
blast_api_key = kwargs.get("blast_api_key", None)
313+
if etherscan_api_key is None:
314+
etherscan_api_key = os.getenv("ETHERSCAN_API_KEY")
246315

247316
export_dir = kwargs.get("export_dir", "crytic-export")
248317
export_dir = os.path.join(
249318
export_dir, kwargs.get("etherscan_export_dir", "etherscan-contracts")
250319
)
251320

252-
if etherscan_api_key and "etherscan" in etherscan_url:
321+
if api_key_required == 2 and etherscan_api_key:
253322
etherscan_url += f"&apikey={etherscan_api_key}"
254323
etherscan_bytecode_url += f"&apikey={etherscan_api_key}"
255-
if arbiscan_api_key and "arbiscan" in etherscan_url:
256-
etherscan_url += f"&apikey={arbiscan_api_key}"
257-
etherscan_bytecode_url += f"&apikey={arbiscan_api_key}"
258-
if polygonscan_api_key and "polygonscan" in etherscan_url:
259-
etherscan_url += f"&apikey={polygonscan_api_key}"
260-
etherscan_bytecode_url += f"&apikey={polygonscan_api_key}"
261-
if test_polygonscan_api_key and "polygonscan" in etherscan_url:
262-
etherscan_url += f"&apikey={test_polygonscan_api_key}"
263-
etherscan_bytecode_url += f"&apikey={test_polygonscan_api_key}"
264-
if avax_api_key and "snowtrace" in etherscan_url:
265-
etherscan_url += f"&apikey={avax_api_key}"
266-
etherscan_bytecode_url += f"&apikey={avax_api_key}"
267-
if ftmscan_api_key and "ftmscan" in etherscan_url:
268-
etherscan_url += f"&apikey={ftmscan_api_key}"
269-
etherscan_bytecode_url += f"&apikey={ftmscan_api_key}"
270-
if bscan_api_key and "bscscan" in etherscan_url:
271-
etherscan_url += f"&apikey={bscan_api_key}"
272-
etherscan_bytecode_url += f"&apikey={bscan_api_key}"
273-
if optim_api_key and "optim" in etherscan_url:
274-
etherscan_url += f"&apikey={optim_api_key}"
275-
etherscan_bytecode_url += f"&apikey={optim_api_key}"
276-
if base_api_key and "base" in etherscan_url:
277-
etherscan_url += f"&apikey={base_api_key}"
278-
etherscan_bytecode_url += f"&apikey={base_api_key}"
279-
if gno_api_key and "gno" in etherscan_url:
280-
etherscan_url += f"&apikey={gno_api_key}"
281-
etherscan_bytecode_url += f"&apikey={gno_api_key}"
282-
if polyzk_api_key and "zkevm" in etherscan_url:
283-
etherscan_url += f"&apikey={polyzk_api_key}"
284-
etherscan_bytecode_url += f"&apikey={polyzk_api_key}"
285-
if blast_api_key and "blast" in etherscan_url:
286-
etherscan_url += f"&apikey={blast_api_key}"
287-
etherscan_bytecode_url += f"&apikey={blast_api_key}"
324+
# API key handling for external tracers would be here e.g.
325+
# elif api_key_required == 1 and avax_api_key and "snowtrace" in etherscan_url:
326+
# etherscan_url += f"&apikey={avax_api_key}"
327+
# etherscan_bytecode_url += f"&apikey={avax_api_key}"
288328

289329
source_code: str = ""
290330
result: Dict[str, Union[bool, str, int]] = {}
291331
contract_name: str = ""
292332

293333
if not only_bytecode:
294-
if "polygon" in etherscan_url or "basescan" in etherscan_url:
295-
# build object with headers, then send request
296-
new_etherscan_url = urllib.request.Request(
297-
etherscan_url, headers={"User-Agent": "Mozilla/5.0"}
298-
)
299-
with urllib.request.urlopen(new_etherscan_url) as response:
300-
html = response.read()
301-
else:
302-
with urllib.request.urlopen(etherscan_url) as response:
303-
html = response.read()
334+
# build object with headers, then send request
335+
new_etherscan_url = urllib.request.Request(
336+
etherscan_url,
337+
headers={
338+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.80 Safari/537.36 crytic-compile/0"
339+
},
340+
)
341+
with urllib.request.urlopen(new_etherscan_url) as response:
342+
html = response.read()
304343

305344
info = json.loads(html)
306345

0 commit comments

Comments
 (0)