From a51fed772b92bc0562849d57854e9309e68164f3 Mon Sep 17 00:00:00 2001 From: trangnv Date: Mon, 5 May 2025 20:20:45 +0700 Subject: [PATCH 1/8] refactor --- .gitignore | 5 +- .python-version | 1 + README.md | 16 +- example/contracts.json | 14 +- example/run_scanner.py | 45 ++ src/main.py => old_main.py | 211 ++++++--- pyproject.toml | 14 + src/etherscan.py | 47 -- src/get_rpc_url.py | 76 --- src/markdown_generator.py | 62 --- src/parse.py | 42 -- src/permission_scanner/__init__.py | 4 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 376 bytes .../scanner/__init__.py} | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 188 bytes .../__pycache__/etherscan.cpython-311.pyc | Bin 0 -> 4462 bytes .../__pycache__/scanner.cpython-311.pyc | Bin 0 -> 21359 bytes src/permission_scanner/scanner/scanner.py | 431 ++++++++++++++++++ src/permission_scanner/utils/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 204 bytes .../block_explorer.cpython-311.pyc | Bin 0 -> 4955 bytes .../__pycache__/contract.cpython-311.pyc | Bin 0 -> 6733 bytes .../__pycache__/etherscan.cpython-311.pyc | Bin 0 -> 4213 bytes .../__pycache__/function.cpython-311.pyc | Bin 0 -> 4195 bytes .../utils/__pycache__/logger.cpython-311.pyc | Bin 0 -> 2274 bytes .../markdown_generator.cpython-311.pyc | Bin 0 -> 4817 bytes .../__pycache__/reporter.cpython-311.pyc | Bin 0 -> 10798 bytes .../__pycache__/reporters.cpython-311.pyc | Bin 0 -> 9044 bytes .../scanner_config.cpython-311.pyc | Bin 0 -> 2868 bytes .../__pycache__/validators.cpython-311.pyc | Bin 0 -> 2447 bytes .../utils/block_explorer.py | 85 ++++ .../utils/block_explorer_config.json | 4 + src/permission_scanner/utils/logger.py | 56 +++ .../utils/markdown_generator.py | 101 ++++ uv.lock | 8 + 35 files changed, 912 insertions(+), 310 deletions(-) create mode 100644 .python-version create mode 100644 example/run_scanner.py rename src/main.py => old_main.py (66%) create mode 100644 pyproject.toml delete mode 100644 src/etherscan.py delete mode 100644 src/get_rpc_url.py delete mode 100644 src/markdown_generator.py delete mode 100644 src/parse.py create mode 100644 src/permission_scanner/__init__.py create mode 100644 src/permission_scanner/__pycache__/__init__.cpython-311.pyc rename src/{access_control.py => permission_scanner/scanner/__init__.py} (100%) create mode 100644 src/permission_scanner/scanner/__pycache__/__init__.cpython-311.pyc create mode 100644 src/permission_scanner/scanner/__pycache__/etherscan.cpython-311.pyc create mode 100644 src/permission_scanner/scanner/__pycache__/scanner.cpython-311.pyc create mode 100644 src/permission_scanner/scanner/scanner.py create mode 100644 src/permission_scanner/utils/__init__.py create mode 100644 src/permission_scanner/utils/__pycache__/__init__.cpython-311.pyc create mode 100644 src/permission_scanner/utils/__pycache__/block_explorer.cpython-311.pyc create mode 100644 src/permission_scanner/utils/__pycache__/contract.cpython-311.pyc create mode 100644 src/permission_scanner/utils/__pycache__/etherscan.cpython-311.pyc create mode 100644 src/permission_scanner/utils/__pycache__/function.cpython-311.pyc create mode 100644 src/permission_scanner/utils/__pycache__/logger.cpython-311.pyc create mode 100644 src/permission_scanner/utils/__pycache__/markdown_generator.cpython-311.pyc create mode 100644 src/permission_scanner/utils/__pycache__/reporter.cpython-311.pyc create mode 100644 src/permission_scanner/utils/__pycache__/reporters.cpython-311.pyc create mode 100644 src/permission_scanner/utils/__pycache__/scanner_config.cpython-311.pyc create mode 100644 src/permission_scanner/utils/__pycache__/validators.cpython-311.pyc create mode 100644 src/permission_scanner/utils/block_explorer.py create mode 100644 src/permission_scanner/utils/block_explorer_config.json create mode 100644 src/permission_scanner/utils/logger.py create mode 100644 src/permission_scanner/utils/markdown_generator.py create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore index cb03f4d..0befd64 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,7 @@ src/__pycache__ /results # markdown generation outputs *.md -*.json \ No newline at end of file + +.venv +logs +.cursor \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..2c07333 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/README.md b/README.md index aa9792e..ab31e47 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,14 @@ Create and activate a virtual Python environment, and install the required Pytho python3 -m venv venv source venv/bin/activate pip install -r requirements.txt +pip install -e . # to install the this repo as a package name permission_scanner +``` + +Or use uv + +```shell +uv venv +uv pip install -r requirements.txt ``` Copy the `.env.example` file to `.env` and, depending on the network where the contracts are deployed on, fill in your RPC provider's url and a valid block explorer api key. Then load the variables with @@ -49,7 +57,13 @@ source .env Then execute the scanner script with 🚀 ```shell -python src/main.py +python example/run_scanner.py +``` + +Or use uv + +```shell +uv run example/run_scanner.py ``` ### Results diff --git a/example/contracts.json b/example/contracts.json index ef2b537..fb9c24a 100644 --- a/example/contracts.json +++ b/example/contracts.json @@ -1,17 +1,5 @@ { "Chain_Name": "mainnet", "Project_Name": "Lido-governance", - "Contracts": [ - "0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc", - "0x2e59A20f205bB85a89C53f1936454680651E618e", - "0xf73a1260d222f447210581DDf212D915c09a3249", - "0xB9E5CBB9CA5b0d659238807E84D0176930753d86", - "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", - "0x9895f0f17cc1d1891b6f18ee0b483b6f221b37bb", - "0x0cb113890b04b49455dfe06554e2d784598a29c9", - "0x4ee3118e3858e8d7164a634825bfe0f73d99c792", - "0xF5Dc67E54FC96F993CD06073f71ca732C1E654B1", - "0x0D97E876ad14DB2b183CFeEB8aa1A5C788eB1831", - "0x2325b0a607808dE42D918DB07F925FFcCfBb2968" - ] + "Contracts": ["0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc"] } diff --git a/example/run_scanner.py b/example/run_scanner.py new file mode 100644 index 0000000..a070eda --- /dev/null +++ b/example/run_scanner.py @@ -0,0 +1,45 @@ +import os +import json +from dotenv import load_dotenv +from permission_scanner import ContractScanner, BlockExplorer + +load_dotenv() + + +def load_config_from_file(file_path: str) -> dict: + with open(file_path, "r") as file: + return json.load(file) + + +def main(): + # load contracts from json + config_json = load_config_from_file("example/contracts.json") + contracts_addresses = config_json["Contracts"] + project_name = config_json["Project_Name"] + chain_name = config_json["Chain_Name"] + + # setup environment variables + api_key = os.getenv("ETHERSCAN_API_KEY") + rpc_url = os.getenv("RPC_URL") + + # initiate the BlockExplorer object + # specify and it will find the right base_url based on src/permission_scanner/utils/block_explorer_config.json + block_explorer = BlockExplorer(api_key, chain_name) + + # initiate the ContractScanner + contract_scanner = ContractScanner( + rpc_url, + block_explorer, + export_dir=f"results/{project_name}", + ) + + # Scan all contracts + for contract in contracts_addresses: + contract_scanner.scan_contract(contract) + + # Generate reports, save in result/{project_name}/reports + contract_scanner.generate_reports(project_name) + + +if __name__ == "__main__": + main() diff --git a/src/main.py b/old_main.py similarity index 66% rename from src/main.py rename to old_main.py index f11634d..bf4d98a 100644 --- a/src/main.py +++ b/old_main.py @@ -2,10 +2,14 @@ from slither.core.declarations.function import Function from slither.core.declarations.contract import Contract -from slither.tools.read_storage.read_storage import SlitherReadStorage, RpcInfo, get_storage_data +from slither.tools.read_storage.read_storage import ( + SlitherReadStorage, + RpcInfo, + get_storage_data, +) import json -from typing import List +from typing import List import urllib.error from parse import init_args @@ -19,22 +23,33 @@ def load_config_from_file(file_path: str) -> dict: - with open(file_path, 'r') as file: + with open(file_path, "r") as file: return json.load(file) def is_valid_eth_address(address: str) -> bool: return bool(re.fullmatch(r"0x[a-fA-F0-9]{40}", address)) + # check for msg.sender checks def get_msg_sender_checks(function: Function) -> List[str]: all_functions = ( [f for f in function.all_internal_calls() if isinstance(f, Function)] - + [m for f in function.all_internal_calls() if isinstance(f, Function) for m in f.modifiers] + + [ + m + for f in function.all_internal_calls() + if isinstance(f, Function) + for m in f.modifiers + ] + [function] + [m for m in function.modifiers if isinstance(m, Function)] + [call for call in function.all_library_calls() if isinstance(call, Function)] - + [m for call in function.all_library_calls() if isinstance(call, Function) for m in call.modifiers] + + [ + m + for call in function.all_library_calls() + if isinstance(call, Function) + for m in call.modifiers + ] ) all_nodes_ = [f.nodes for f in all_functions] @@ -51,12 +66,15 @@ def get_msg_sender_checks(function: Function) -> List[str]: return all_conditional_nodes_on_msg_sender -def get_permissions(contract: Contract, result: dict, all_state_variables_read: List[str], isProxy: bool, index: int): - - temp = { - "Contract_Name": contract.name, - "Functions": [] - } +def get_permissions( + contract: Contract, + result: dict, + all_state_variables_read: List[str], + isProxy: bool, + index: int, +): + + temp = {"Contract_Name": contract.name, "Functions": []} for function in contract.functions: # 1) list all modifiers in function @@ -71,12 +89,11 @@ def get_permissions(contract: Contract, result: dict, all_state_variables_read: listOfModifiers = sorted([m.name for m in set(modifiers)]) - # 2) detect conditions on msg.sender # in the full function scope msg_sender_condition = get_msg_sender_checks(function) - if (len(modifiers) == 0 and len(msg_sender_condition) == 0): + if len(modifiers) == 0 and len(msg_sender_condition) == 0: # no permission detected continue @@ -84,18 +101,22 @@ def get_permissions(contract: Contract, result: dict, all_state_variables_read: # the variables available in storage will be read state_variables_read_inside_modifiers = [ v.name - for modifier in modifiers if modifier is not None - for v in modifier.all_variables_read() if v is not None and v.name + for modifier in modifiers + if modifier is not None + for v in modifier.all_variables_read() + if v is not None and v.name ] state_variables_read_inside_function = [ v.name for v in function.all_state_variables_read() if v.name ] - + all_state_variables_read_this_func = [] all_state_variables_read_this_func.extend(state_variables_read_inside_modifiers) all_state_variables_read_this_func.extend(state_variables_read_inside_function) - all_state_variables_read_this_func = list(set(all_state_variables_read_this_func)) + all_state_variables_read_this_func = list( + set(all_state_variables_read_this_func) + ) all_state_variables_read.extend(all_state_variables_read_this_func) @@ -105,14 +126,16 @@ def get_permissions(contract: Contract, result: dict, all_state_variables_read: ] # 4) write everything to dict - temp['Functions'].append({ - "Function": function.name, - "Modifiers": listOfModifiers, - "msg.sender_conditions": msg_sender_condition, - "state_variables_read": all_state_variables_read_this_func, - "state_variables_written": state_variables_written - }) - + temp["Functions"].append( + { + "Function": function.name, + "Modifiers": listOfModifiers, + "msg.sender_conditions": msg_sender_condition, + "state_variables_read": all_state_variables_read_this_func, + "state_variables_written": state_variables_written, + } + ) + # dump to result dict if isProxy and index == 0: result["proxy_permissions"] = temp @@ -122,17 +145,18 @@ def get_permissions(contract: Contract, result: dict, all_state_variables_read: # is normal contract result["permissions"] = temp + def main(): load_dotenv() # Load environment variables from .env file # load contracts from json config_json = load_config_from_file("contracts.json") - + contracts_addresses = config_json["Contracts"] contract_data_for_markdown = [] project_name = config_json["Project_Name"] chain_name = config_json["Chain_Name"] - + rpc_url = get_rpc_url(chain_name) platform_key = get_etherscan_url() @@ -142,42 +166,64 @@ def main(): rpc_info = RpcInfo(rpc_url, "latest") for contract_address in contracts_addresses: - contract_result = fetch_contract_metadata(address=contract_address, apikey=platform_key, chainid=get_chain_id(chain_name)) + contract_result = fetch_contract_metadata( + address=contract_address, + apikey=platform_key, + chainid=get_chain_id(chain_name), + ) contract_name = contract_result["ContractName"] isProxy = contract_result["Proxy"] == 1 implementation_address = contract_result["Implementation"] implementation_name = "" - contract_data_for_markdown.append({"name": contract_name, "address": contract_address}) - + contract_data_for_markdown.append( + {"name": contract_name, "address": contract_address} + ) + if isProxy and implementation_address: - if not isinstance(implementation_address, str) or not is_valid_eth_address(implementation_address): - raise ValueError(f"Invalid implementation address for proxy: {implementation_address}") + if not isinstance(implementation_address, str) or not is_valid_eth_address( + implementation_address + ): + raise ValueError( + f"Invalid implementation address for proxy: {implementation_address}" + ) try: implementation_result = fetch_contract_metadata( address=implementation_address, apikey=platform_key, - chainid=get_chain_id(chain_name) + chainid=get_chain_id(chain_name), ) implementation_name = implementation_result.get("ContractName") or "" - contract_data_for_markdown.append({"name": implementation_name, "address": implementation_address}) + contract_data_for_markdown.append( + {"name": implementation_name, "address": implementation_address} + ) except Exception as e: raise f"Failed to get Implementation contract from Etherscan. \n\n\n + {e}" - - target_storage_vars = [] # target storage variables of this contract + target_storage_vars = [] # target storage variables of this contract temp_global = {} # setup args for slither - args = init_args(project_name, contract_address, chain_name, rpc_url, platform_key, contract_name) + args = init_args( + project_name, + contract_address, + chain_name, + rpc_url, + platform_key, + contract_name, + ) target = args.contract_source - + try: slither = Slither(target, **vars(args)) except urllib.error.HTTPError as e: - print(f"\033[33mFailed to compile contract at {contract_address} due to HTTP error: {e}\033[0m") + print( + f"\033[33mFailed to compile contract at {contract_address} due to HTTP error: {e}\033[0m" + ) continue # Skip this contract and move to the next one except Exception as e: - print(f"\033[33mAn error occurred while analyzing {contract_address}: {e}\033[0m") + print( + f"\033[33mAn error occurred while analyzing {contract_address}: {e}\033[0m" + ) continue # retrieved contracts from the address (inherited and interacted contracts) @@ -186,11 +232,15 @@ def main(): # only take the one contract that is in the key # this filters out interacted contracts (we dont need the permissions of them) # does not exclude inherited contracts - target_contract = [contract for contract in contracts if contract.name == contract_name] + target_contract = [ + contract for contract in contracts if contract.name == contract_name + ] if len(target_contract) == 0: - raise Exception(f"\033[31m\n \nThe contract name supplied in contract.json does not match any of the found contract names for this address: {contract_address}\033[0m") - + raise Exception( + f"\033[31m\n \nThe contract name supplied in contract.json does not match any of the found contract names for this address: {contract_address}\033[0m" + ) + srs = SlitherReadStorage(target_contract, args.max_depth, rpc_info) srs.unstructured = False # Remove target prefix "mainnet:" e.g. mainnet:0x0 -> 0x0. @@ -198,21 +248,29 @@ def main(): srs.storage_address = address if isProxy: - # step 1: create slither object again, but with implementation address + # step 1: create slither object again, but with implementation address # -> run analysis of storage layout and permissions of implementation address # step 2: read storage from proxy contract (location of storage) contract_address["address"] # scan the implementation address - slither = Slither(f'{chain_name}:{implementation_address}', **vars(args)) - + slither = Slither(f"{chain_name}:{implementation_address}", **vars(args)) + # get all the instantiated contracts (includes also interacted contracts) from the implementation contract implementation_contracts = slither.contracts_derived # find the instantiated/main implementation contract - - target_contract.extend([contract for contract in implementation_contracts if contract.name == implementation_name]) + + target_contract.extend( + [ + contract + for contract in implementation_contracts + if contract.name == implementation_name + ] + ) if len(target_contract) == 1: - raise Exception(f"\033[31m\n \nThe implementation name supplied in contract.json does not match any of the found implementation contract names for this address: {contract_address['address']}\033[0m") + raise Exception( + f"\033[31m\n \nThe implementation name supplied in contract.json does not match any of the found implementation contract names for this address: {contract_address['address']}\033[0m" + ) temp_global["Implementation_Address"] = implementation_address temp_global["Proxy_Address"] = contract_address @@ -229,13 +287,12 @@ def main(): ################################################## # start analysis - # start analysis of main contract (can be proxy, then also the implementation contract is analysed) for i, contract in enumerate(target_contract): # get permissions and store inside target_storage_vars get_permissions(contract, temp_global, target_storage_vars, isProxy, i) - target_storage_vars = list(set(target_storage_vars)) # remove duplicates + target_storage_vars = list(set(target_storage_vars)) # remove duplicates # Three steps to retrieve storage variables with slither # 1. set target variables @@ -249,51 +306,69 @@ def main(): if var.name in target_storage_vars: # achieve step 1. srs._target_variables.append((contract, var)) - + # add all constant and immutable variable to a list to do the required look-up if not var.is_stored: - + # functionData is a dict for functionData in temp_global["permissions"]["Functions"]: - # check if e.g storage variable owner is part of this function + # check if e.g storage variable owner is part of this function if var.name in functionData["state_variables_read"]: # check if already added some constants/immutables # Ensure key exists if "immutables_and_constants" not in functionData: functionData["immutables_and_constants"] = [] - + # Check if the variable has an expression and is not the proxy marker - if var.expression and str(var.expression) != "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": + if ( + var.expression + and str(var.expression) + != "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc" + ): try: raw_value = get_storage_data( - srs.rpc_info.web3, contract_address, - str(var.expression), srs.rpc_info.block + srs.rpc_info.web3, + contract_address, + str(var.expression), + srs.rpc_info.block, + ) + value = srs.convert_value_to_type( + raw_value, 160, 0, "address" ) - value = srs.convert_value_to_type(raw_value, 160, 0, "address") functionData["immutables_and_constants"].append( - {"name": var.name, "slot": str(var.expression), "value": value} + { + "name": var.name, + "slot": str(var.expression), + "value": value, + } ) except Exception: functionData["immutables_and_constants"].append( {"name": var.name, "slot": str(var.expression)} ) else: - functionData["immutables_and_constants"].append({"name": var.name}) + functionData["immutables_and_constants"].append( + {"name": var.name} + ) - # step 2. computes storage keys for target variables + # step 2. computes storage keys for target variables srs.get_target_variables() # step 3. get the values of the target variables and their slots try: srs.walk_slot_info(srs.get_slot_values) except urllib.error.HTTPError as e: - print(f"\033[33mFailed to fetch storage from contract at {contract_address} due to HTTP error: {e}\033[0m") + print( + f"\033[33mFailed to fetch storage from contract at {contract_address} due to HTTP error: {e}\033[0m" + ) continue # Skip this contract and move to the next one except Exception as e: - print(f"\033[33mAn error occurred while fetching storage slots from contract {contract_address}: {e}\033[0m") + print( + f"\033[33mAn error occurred while fetching storage slots from contract {contract_address}: {e}\033[0m" + ) continue - + storageValues = {} # merge storage retrieval with contracts for key, value in srs.slot_info.items(): @@ -301,20 +376,20 @@ def main(): storageValues[value.name] = value.value # contractDict["Functions"] is a list, functionData a dict for functionData in contractDict["Functions"]: - # check if e.g storage variable owner is part of this function + # check if e.g storage variable owner is part of this function if value.name in functionData["state_variables_read"]: # if so, add a key value pair to the functionData object, to improve readability of report functionData[value.name] = value.value if len(storageValues.values()): contractDict["storage_values"] = storageValues - + if len(implementation_name) > 0: result[implementation_name] = temp_global else: result[contract_name] = temp_global - with open("permissions.json","w") as file: + with open("permissions.json", "w") as file: json.dump(result, file, indent=4) content = generate_full_markdown("", contract_data_for_markdown, result) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9adcd38 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "permission_scanner" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/permission_scanner"] diff --git a/src/etherscan.py b/src/etherscan.py deleted file mode 100644 index dff298b..0000000 --- a/src/etherscan.py +++ /dev/null @@ -1,47 +0,0 @@ -import os - -import urllib.request -import urllib.parse -import json - -def get_etherscan_url() -> str: - etherscan_url = os.getenv("ETHERSCAN_API_KEY") - - if etherscan_url is None: - raise KeyError("Please set a etherscan api key in your .env") - - return etherscan_url - - -def fetch_contract_metadata(address, apikey, chainid=1): - base_url = "https://api.etherscan.io/v2/api" - params = { - "chainid": chainid, - "module": "contract", - "action": "getsourcecode", - "address": address, - "apikey": apikey - } - url = f"{base_url}?{urllib.parse.urlencode(params)}" - - try: - with urllib.request.urlopen(url) as response: - if response.status != 200: - raise Exception(f"HTTP error {response.status}") - data = json.load(response) - except Exception as e: - raise RuntimeError(f"Request failed: {e}") - - if data.get("status") != "1": - raise ValueError(f"API error: {data.get('message', 'Unknown error')}") - - result = data.get("result", []) - if not result: - raise ValueError("No contract data found") - - contract_info = result[0] - return { - "ContractName": contract_info.get("ContractName"), - "Proxy": contract_info.get("Proxy") == "1", - "Implementation": contract_info.get("Implementation") - } diff --git a/src/get_rpc_url.py b/src/get_rpc_url.py deleted file mode 100644 index 476b91a..0000000 --- a/src/get_rpc_url.py +++ /dev/null @@ -1,76 +0,0 @@ -import os - -def get_rpc_url(network: str) -> str: - rpc_urls = { - "mainnet": os.getenv("MAINNET_RPC"), - "bsc": os.getenv("BSC_RPC"), - "poly": os.getenv("POLYGON_RPC"), - "polyzk": os.getenv("POLYGON_ZK_RPC"), - "base": os.getenv("BASE_RPC"), - "arbi": os.getenv("ARBITRUM_RPC"), - "nova.arbi": os.getenv("NOVA_ARBITRUM_RPC"), - "linea": os.getenv("LINEA_RPC"), - "ftm": os.getenv("FANTOM_RPC"), - "blast": os.getenv("BLAST_RPC"), - "optim": os.getenv("OPTIMISTIC_RPC"), - "avax": os.getenv("AVAX_RPC"), - "bttc": os.getenv("BTTC_RPC"), - "celo": os.getenv("CELO_RPC"), - "cronos": os.getenv("CRONOS_RPC"), - "frax": os.getenv("FRAX_RPC"), - "gno": os.getenv("GNOSIS_RPC"), - "kroma": os.getenv("KROMA_RPC"), - "mantle": os.getenv("MANTLE_RPC"), - "moonbeam": os.getenv("MOONBEAM_RPC"), - "moonriver": os.getenv("MOONRIVER_RPC"), - "opbnb": os.getenv("OPBNB_RPC"), - "scroll": os.getenv("SCROLL_RPC"), - "taiko": os.getenv("TAIKO_RPC"), - "wemix": os.getenv("WEMIX_RPC"), - "era.zksync": os.getenv("ZKSYNC_ERA_RPC"), - "xai": os.getenv("XAI_RPC"), - } - - rpc_url = rpc_urls.get(network) - - if rpc_url is None: - raise KeyError(f"Network '{network}' not found in pre-configured chains. Please set your network in get_rpc_url.py") - - return rpc_url - - -def get_chain_id(network: str) -> int: - chain_ids = { - "mainnet": 1, - "bsc": 56, - "poly": 137, - "polyzk": 1101, - "base": 8453, - "arbi": 42161, - "nova.arbi": 42170, - "linea": 59144, - "ftm": 250, - "blast": 81457, - "optim": 10, - "avax": 43114, - "bttc": 199, - "celo": 42220, - "cronos": 25, - "frax": 252, - "gno": 100, - "kroma": 255, - "mantle": 5000, - "moonbeam": 1284, - "moonriver": 1285, - "opbnb": 204, - "scroll": 534352, - "taiko": 167000, - "wemix": 1111, - "era.zksync": 324, - "xai": 660279, - } - - if network not in chain_ids: - raise ValueError(f"Unknown network name: {network}") - - return chain_ids[network] diff --git a/src/markdown_generator.py b/src/markdown_generator.py deleted file mode 100644 index 0461757..0000000 --- a/src/markdown_generator.py +++ /dev/null @@ -1,62 +0,0 @@ - -def generate_contracts_table(contracts_object_list): - """ - """ - md_content = "## Contracts\n| Contract Name | Address |\n" - md_content += "|--------------|--------------|\n" - - for contract in contracts_object_list: - md_content += f"| {contract['name']} | {contract['address']} |\n" - # try: - # md_content += f"| {contract['implementation_name']} | ... |\n" - # except KeyError: - # # not a proxy but a standard contract - # pass - - return md_content - -def generate_permissions_table(permissions): - """ - """ - md_content = "## Permission\n| Contract | Function | Impact | Owner |\n" - md_content += "|-------------|------------|-------------------------|-------------------|\n" - - for contract, entries in permissions.items(): - try: - proxy_permissions = entries["proxy_permissions"] - contract_name = proxy_permissions["Contract_Name"] - permissioned_functions = proxy_permissions["Functions"] - for permissioned_function in permissioned_functions: - owner = "" - try: - owner = permissioned_function['_owner'] - except KeyError: - owner = permissioned_function['Modifiers'] - pass - - md_content += f"| {contract_name} | {permissioned_function['Function']} | ... | {owner} |\n" - except KeyError: - # just a normal contract - pass - # will do the normal contract or the implementation contract - proxy_permissions = entries["permissions"] - contract_name = proxy_permissions["Contract_Name"] - permissioned_functions = proxy_permissions["Functions"] - for permissioned_function in permissioned_functions: - owner = "" - try: - owner = permissioned_function['_owner'] - except KeyError: - # no simple owner found - owner = permissioned_function['Modifiers'] - pass - - md_content += f"| {contract_name} | {permissioned_function['Function']} | ... | {owner} |\n" - - return md_content - -def generate_full_markdown(protocol_metadata, contracts, permissions) -> str: - - return f"{generate_contracts_table(contracts)}\n\n{generate_permissions_table(permissions)}" - - diff --git a/src/parse.py b/src/parse.py deleted file mode 100644 index ac977fa..0000000 --- a/src/parse.py +++ /dev/null @@ -1,42 +0,0 @@ -from crytic_compile import cryticparser -from argparse import ArgumentParser, Namespace - - -def init_args(project_name: str, contract_address: str, chain_name: str, rpc_url: str, platform_key: str, contract_name: str) -> Namespace: - """Parse the underlying arguments for the program. - Returns: - The arguments for the program. - """ - - # create a ArgumentParser for cryticparser.init - parser = ArgumentParser( - description="Read a variable's value from storage for a deployed contract", - usage=("\nProvide secrets in env file\n"), - ) - - # Add arguments (this step is required before setting defaults) - parser.add_argument("--contract_source", nargs="+", help="Contract address or project directory") - parser.add_argument("--export-dir", help="where downloaded files should be stored") - parser.add_argument("--rpc-url", help="RPC endpoint URL") - parser.add_argument("--etherscan-api-key", help="Etherscan API key") - parser.add_argument("--max-depth", help="Max depth to search in data structure.", default=20) - parser.add_argument( - "--block", - help="The block number to read storage from. Requires an archive node to be provided as the RPC url.", - default="latest", - ) - - - # Set defaults for arguments programmatically - # Hyphens (-) in argument names are automatically converted to underscores (_) - parser.set_defaults( - contract_source=f'{chain_name}:{contract_address}', - rpc_url=rpc_url, - etherscan_api_key=platform_key, - export_dir=f'results/{project_name}/{contract_name}' - ) - - # requires a ArgumentParser instance - cryticparser.init(parser) - - return parser.parse_args() diff --git a/src/permission_scanner/__init__.py b/src/permission_scanner/__init__.py new file mode 100644 index 0000000..a20fbc5 --- /dev/null +++ b/src/permission_scanner/__init__.py @@ -0,0 +1,4 @@ +from .scanner.scanner import ContractScanner +from .utils.block_explorer import BlockExplorer + +__all__ = ["ContractScanner", "BlockExplorer"] diff --git a/src/permission_scanner/__pycache__/__init__.cpython-311.pyc b/src/permission_scanner/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..596fa21b212e19e4ed94416545199027ffcbea64 GIT binary patch literal 376 zcmZ3^%ge<81X9~2GJ1jZV-N=hn4pZ$YCy(xh7^Vr#vF!R#wbQc5SuB7DVI5l8OUZ% zVM%9-Vo6~QX3%7N$p}=e$#{$3IX|zYC^5MtI5{ydFSQ6L#OsukpPcPlQIL~glv-58 z3{+Ue0wnx2S#R+dLzL-3X|U`qk=B4YWC*>q273-&@re*3Dr52|q7A0rs7o-;DW)>G`=I7}`?a?nT zN=6cnhY82WXXa&=#K-FuRQ}?y$<0qG%}KQ@;s)9T@?WtzkodsN$jEquLFWQ0y1}4- W0Tn&qmYI=%flIG}9R!OwfqDTozihz( literal 0 HcmV?d00001 diff --git a/src/access_control.py b/src/permission_scanner/scanner/__init__.py similarity index 100% rename from src/access_control.py rename to src/permission_scanner/scanner/__init__.py diff --git a/src/permission_scanner/scanner/__pycache__/__init__.cpython-311.pyc b/src/permission_scanner/scanner/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..99185b15e4eaf29c98d9b3aafb0c585f29b25429 GIT binary patch literal 188 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 H_4$7RJqS~a literal 0 HcmV?d00001 diff --git a/src/permission_scanner/scanner/scanner.py b/src/permission_scanner/scanner/scanner.py new file mode 100644 index 0000000..1243e43 --- /dev/null +++ b/src/permission_scanner/scanner/scanner.py @@ -0,0 +1,431 @@ +import json +from typing import List, Dict, Any +import urllib.error +import os +import re + +from slither.slither import Slither +from slither.core.declarations.function import Function +from slither.core.declarations.contract import Contract +from slither.tools.read_storage.read_storage import ( + SlitherReadStorage, + RpcInfo, + get_storage_data, +) + +from ..utils.block_explorer import BlockExplorer +from ..utils.logger import setup_logger +from ..utils.markdown_generator import generate_full_markdown + +logger = setup_logger(__name__, "logs/scanner_new.log") + + +class ContractScanner: + """Service for scanning smart contracts for permissions and storage.""" + + def __init__( + self, rpc_url: str, block_explorer: BlockExplorer, export_dir: str = "results" + ): + """Initialize the ContractScanner. + + Args: + rpc_url (str): The RPC URL for the blockchain network + block_explorer (BlockExplorer): The block explorer instance for fetching contract metadata + export_dir (str): Directory to save Solidity files and crytic_compile.config.json + """ + self.rpc_url = rpc_url + self.block_explorer = block_explorer + self.slither = None + self.storage_reader = None + self.export_dir = export_dir + self.contract_data_for_markdown = [] + self.scan_results = {} + logger.info("Initialized ContractScanner") + + @staticmethod + def is_valid_eth_address(address: str) -> bool: + """Check if a string is a valid Ethereum address.""" + return bool(re.fullmatch(r"0x[a-fA-F0-9]{40}", address)) + + @staticmethod + def get_msg_sender_checks(function: Function) -> List[str]: + """Get all msg.sender checks in a function and its internal calls.""" + all_functions = ( + [f for f in function.all_internal_calls() if isinstance(f, Function)] + + [ + m + for f in function.all_internal_calls() + if isinstance(f, Function) + for m in f.modifiers + ] + + [function] + + [m for m in function.modifiers if isinstance(m, Function)] + + [ + call + for call in function.all_library_calls() + if isinstance(call, Function) + ] + + [ + m + for call in function.all_library_calls() + if isinstance(call, Function) + for m in call.modifiers + ] + ) + + all_nodes_ = [f.nodes for f in all_functions] + all_nodes = [item for sublist in all_nodes_ for item in sublist] + + all_conditional_nodes = [ + n for n in all_nodes if n.contains_if() or n.contains_require_or_assert() + ] + all_conditional_nodes_on_msg_sender = [ + str(n.expression) + for n in all_conditional_nodes + if "msg.sender" in [v.name for v in n.solidity_variables_read] + ] + return all_conditional_nodes_on_msg_sender + + def get_permissions( + self, + contract: Contract, + result: Dict[str, Any], + all_state_variables_read: List[str], + is_proxy: bool, + index: int, + ) -> None: + """Analyze permissions in a contract and store results. + + Args: + contract (Contract): The contract to analyze + result (Dict[str, Any]): Dictionary to store results + all_state_variables_read (List[str]): List of state variables read + is_proxy (bool): Whether the contract is a proxy + index (int): Index for proxy/implementation contract + """ + temp = {"Contract_Name": contract.name, "Functions": []} + + for function in contract.functions: + # Get all modifiers + modifiers = function.modifiers + for call in function.all_internal_calls(): + if isinstance(call, Function): + modifiers += call.modifiers + for call in function.all_library_calls(): + if isinstance(call, Function): + modifiers += call.modifiers + + list_of_modifiers = sorted([m.name for m in set(modifiers)]) + + # Get msg.sender conditions + msg_sender_condition = self.get_msg_sender_checks(function) + + if len(modifiers) == 0 and len(msg_sender_condition) == 0: + continue + + # Get state variables read + state_variables_read_inside_modifiers = [ + v.name + for modifier in modifiers + if modifier is not None + for v in modifier.all_variables_read() + if v is not None and v.name + ] + + state_variables_read_inside_function = [ + v.name for v in function.all_state_variables_read() if v.name + ] + + all_state_variables_read_this_func = [] + all_state_variables_read_this_func.extend( + state_variables_read_inside_modifiers + ) + all_state_variables_read_this_func.extend( + state_variables_read_inside_function + ) + all_state_variables_read_this_func = list( + set(all_state_variables_read_this_func) + ) + + all_state_variables_read.extend(all_state_variables_read_this_func) + + # Get state variables written + state_variables_written = [ + v.name for v in function.all_state_variables_written() if v.name + ] + + # Store results + temp["Functions"].append( + { + "Function": function.name, + "Modifiers": list_of_modifiers, + "msg.sender_conditions": msg_sender_condition, + "state_variables_read": all_state_variables_read_this_func, + "state_variables_written": state_variables_written, + } + ) + + # Store in result dict + if is_proxy and index == 0: + result["proxy_permissions"] = temp + elif is_proxy and index == 1: + result["permissions"] = temp + else: + result["permissions"] = temp + + def scan_contract(self, address: str) -> Dict[str, Any]: + """Scan a contract for permissions and storage. + + Args: + address (str): The contract address to scan + + Returns: + Dict[str, Any]: The scan results for this contract + """ + logger.info(f"Starting scan for contract at {address}") + + try: + # Fetch contract metadata + contract_result = self.block_explorer.fetch_contract_metadata(address) + contract_name = contract_result["ContractName"] + is_proxy = contract_result["Proxy"] == 1 + implementation_address = contract_result["Implementation"] + implementation_name = "" + + # Add contract to markdown data + self.contract_data_for_markdown.append( + {"name": contract_name, "address": address} + ) + + result = {} + target_storage_vars = [] + temp_global = {} + + # Setup RPC info + rpc_info = RpcInfo(self.rpc_url, "latest") + + # Create etherscan-contracts directory for this contract + contract_dir = os.path.join(self.export_dir, contract_name) + os.makedirs(contract_dir, exist_ok=True) + + # Handle proxy contracts + if is_proxy and implementation_address: + if not self.is_valid_eth_address(implementation_address): + raise ValueError( + f"Invalid implementation address for proxy: {implementation_address}" + ) + + try: + implementation_result = self.block_explorer.fetch_contract_metadata( + implementation_address + ) + implementation_name = implementation_result.get("ContractName", "") + + # Add implementation contract to markdown data + if implementation_name: + self.contract_data_for_markdown.append( + { + "name": implementation_name, + "address": implementation_address, + } + ) + except Exception as e: + raise RuntimeError(f"Failed to get Implementation contract: {e}") + + # Initialize Slither with export directory + try: + self.slither = Slither( + f"{self.block_explorer.chain_name}:{address}", + export_dir=contract_dir, + etherscan_api_key=self.block_explorer.api_key, + ) + except urllib.error.HTTPError as e: + logger.error( + f"Failed to compile contract at {address} due to HTTP error: {e}" + ) + raise + except Exception as e: + logger.error(f"An error occurred while analyzing {address}: {e}") + raise + + # Get target contract + contracts = self.slither.contracts + target_contract = [c for c in contracts if c.name == contract_name] + + if not target_contract: + raise ValueError( + f"Contract name {contract_name} not found at address {address}" + ) + + # Initialize storage reader + self.storage_reader = SlitherReadStorage(target_contract, 10, rpc_info) + self.storage_reader.unstructured = False + address = address[address.find(":") + 1 :] if ":" in address else address + self.storage_reader.storage_address = address + + # Handle proxy implementation + if is_proxy: + # Use the same directory for implementation contract + self.slither = Slither( + f"{self.block_explorer.chain_name}:{implementation_address}", + export_dir=contract_dir, + etherscan_api_key=self.block_explorer.api_key, + ) + implementation_contracts = self.slither.contracts_derived + target_contract.extend( + [ + c + for c in implementation_contracts + if c.name == implementation_name + ] + ) + + if len(target_contract) == 1: + raise ValueError( + f"Implementation name {implementation_name} not found" + ) + + temp_global["Implementation_Address"] = implementation_address + temp_global["Proxy_Address"] = address + else: + temp_global["Address"] = address + + # Analyze permissions + for i, contract in enumerate(target_contract): + self.get_permissions( + contract, temp_global, target_storage_vars, is_proxy, i + ) + + target_storage_vars = list(set(target_storage_vars)) + + # Read storage + self._read_storage( + target_contract, target_storage_vars, temp_global, address + ) + + # Store results + if implementation_name: + self.scan_results[implementation_name] = temp_global + else: + self.scan_results[contract_name] = temp_global + + logger.info(f"Completed scan for contract {contract_name}") + return temp_global + + except Exception as e: + logger.error( + f"Unexpected error while scanning contract {address}: {str(e)}" + ) + raise + + def _read_storage( + self, + target_contract: List[Contract], + target_storage_vars: List[str], + temp_global: Dict[str, Any], + contract_address: str, + ) -> None: + """Read storage values for the contract. + + Args: + target_contract (List[Contract]): List of contracts to analyze + target_storage_vars (List[str]): List of storage variables to read + temp_global (Dict[str, Any]): Dictionary to store results + contract_address (str): The contract address + """ + # Set target variables + for contract in self.storage_reader._contracts: + for var in contract.state_variables_ordered: + if var.name in target_storage_vars: + self.storage_reader._target_variables.append((contract, var)) + + if not var.is_stored: + for function_data in temp_global["permissions"]["Functions"]: + if var.name in function_data["state_variables_read"]: + if "immutables_and_constants" not in function_data: + function_data["immutables_and_constants"] = [] + + if ( + var.expression + and str(var.expression) + != "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc" + ): + try: + raw_value = get_storage_data( + self.storage_reader.rpc_info.web3, + contract_address, + str(var.expression), + self.storage_reader.rpc_info.block, + ) + value = self.storage_reader.convert_value_to_type( + raw_value, 160, 0, "address" + ) + function_data["immutables_and_constants"].append( + { + "name": var.name, + "slot": str(var.expression), + "value": value, + } + ) + except Exception: + function_data["immutables_and_constants"].append( + {"name": var.name, "slot": str(var.expression)} + ) + else: + function_data["immutables_and_constants"].append( + {"name": var.name} + ) + + # Compute storage keys and get values + self.storage_reader.get_target_variables() + try: + self.storage_reader.walk_slot_info(self.storage_reader.get_slot_values) + except Exception as e: + logger.error(f"Failed to read storage: {e}") + raise + + # Store storage values + storage_values = {} + for key, value in self.storage_reader.slot_info.items(): + contract_dict = temp_global["permissions"] + storage_values[value.name] = value.value + + for function_data in contract_dict["Functions"]: + if value.name in function_data["state_variables_read"]: + function_data[value.name] = value.value + + if storage_values: + contract_dict["storage_values"] = storage_values + + def generate_reports(self, project_name: str) -> None: + """Generate JSON and markdown reports from scan results. + + Args: + project_name (str): Name of the project being scanned + """ + # Create reports directory + reports_dir = os.path.join(self.export_dir, "reports") + os.makedirs(reports_dir, exist_ok=True) + + # Save JSON report + json_path = os.path.join(reports_dir, f"permissions_{project_name}.json") + with open(json_path, "w") as f: + json.dump(self.scan_results, f, indent=4) + logger.info(f"Generated JSON report: {json_path}") + + # Generate and save markdown report + markdown_content = generate_full_markdown( + project_name, self.contract_data_for_markdown, self.scan_results + ) + markdown_path = os.path.join(reports_dir, f"permissions_{project_name}.md") + with open(markdown_path, "w") as f: + f.write(markdown_content) + logger.info(f"Generated Markdown report: {markdown_path}") + + def get_scan_results(self) -> Dict[str, Any]: + """Get the current scan results. + + Returns: + Dict[str, Any]: The accumulated scan results + """ + return self.scan_results diff --git a/src/permission_scanner/utils/__init__.py b/src/permission_scanner/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/permission_scanner/utils/__pycache__/__init__.cpython-311.pyc b/src/permission_scanner/utils/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..efa5ee5667bf2def7e110be1bd7d9b9f71bb2ea2 GIT binary patch literal 204 zcmZ3^%ge<81fJp&86f&Gh=2h`DC095kTIPhg&~+hlhJP_LlF~@{~09tD_%deIJKx) zKd-E$C^0WxKRqcYF{xNTB{eNmwUX5FE6!7zqlwF zNjM%RTw0QuQ>-5!pP83g5+AQuQ2C3)CO1E&G$+-rh!tob$VJ8cK;i>4BO~Jn1{hJq H3={(Zzi~C~ literal 0 HcmV?d00001 diff --git a/src/permission_scanner/utils/__pycache__/block_explorer.cpython-311.pyc b/src/permission_scanner/utils/__pycache__/block_explorer.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..460a3e980d66d9e76e32335114e8b032002867da GIT binary patch literal 4955 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 LJnDTg$N~Ky2Vloz literal 0 HcmV?d00001 diff --git a/src/permission_scanner/utils/__pycache__/contract.cpython-311.pyc b/src/permission_scanner/utils/__pycache__/contract.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f6b7a3ea79ed2c3961fb72f1c45edf6258ad822f GIT binary patch literal 6733 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 literal 0 HcmV?d00001 diff --git a/src/permission_scanner/utils/__pycache__/function.cpython-311.pyc b/src/permission_scanner/utils/__pycache__/function.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..42e55020b1e7a2f5aa7579699efbb9ef496cd523 GIT binary patch literal 4195 zcmcgv&2JmW6`v)CpIS+Dtj|cUSG7J&eKD|QSqf4IlI0jqZ7GOkG~pqO6=x|i;D} zH*X$iXMXQvW`AgJ4-+W=?djY6SBQ{*W5+GNI&<;|U|tYLn3N_NDK2F^agT(3PuiQI zahmbPeWLG8`!j)fKq4OUkTCk3FrOZH=_TYfJhc)J>XI1s!)V~82gcy3m6y=Gb~_{< zV!>4s4gCitQI8X1nyDqynqfG;WJ*snkkQ8}!*qfxTV^Vk)zVSPp%1e8OrkLfFjBp9 z@+)9o5S_#&M&ceO#l6fEr_3AoF&g(XAH2-(bbO)nOv*4|zO|h#y)c8+P;yCSt1)0G z=B8#U2`#Iv>q;`0&oU*IRg(E^0@qFa8C_MKu&QQqET2YRR@L9rda-8Q+F-H2_hr?qt>#`I)ritC21@x*5AbQe|_I~*K@2;v9ExB;Zt_*LjVi{3lFGFbe)B1n68@iYAKZueg; zRtbO<%Si$3b83~R;AC;HPIVvid;-HBqWks0U%f9u>$Pi10!|dtX}_2N324(xw8Q7L zeV1srozwPTqAj1(4xly*#@qD{y|ZPFq54P!Mp{=N79(vsWiksUDQkaD<5w7H)%%^$ z|A0jr{VUMF+SLC5`duNydRR#BWLI0~7jyVo*K=RIo5^}l(!;tzzgL7;uM_;N?ze@t zFZC^Sl&onenqp)$4o;rSf;T11hIcE^Qs$c5VuaGNstra7<6Ci__=Q8c{b5VKVUX*{Wd{|C@pASpn=mizyzPP2WG4`tOCrDxANp0dGgg)WqH5)+v8+=52N{>iU_)DocTj*(r@E>Rmv57Qc?wA-t?DO~6@)hd{K#gQOh z05>Fl-pMekxB?{Vt;MVmd;|gm7ofO_uEWqx^>pq+Zv&jxfoc3FX1pS)28j~;j@rS& ziZX)jHA~c*_cS&DxTr@SlgV<)^E{YJtQBm0?@CQ@t9FgsY@RLSbR& zsI(mI=#oVT zN^1u<_P@3$@JnU7RQYJ8xU%T?x@R*iJXzHq6lN}&0_q-If!vE^vkX={9* z>ITQ0Q1d81Tg9~>;*Kw#0U#J`-zve|a!5I(vCHM(k?ckg69{&_ZF`$gU0jYW= zNve>W)+l3wd(gtJ+RMnWN+;qN`F2biV#m*)pJV%6wZ@(DEk@7J1^)x K??0m{0Qw)UZVuA` literal 0 HcmV?d00001 diff --git a/src/permission_scanner/utils/__pycache__/logger.cpython-311.pyc b/src/permission_scanner/utils/__pycache__/logger.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6e8caeebbbcbc1aec801631d67d119372973be05 GIT binary patch literal 2274 zcmb7F&1)M+6rYt=tJP|y_#=|-v`yDdY)f@*#Z6D91e#FOlEf}a3&qfiwRR-0y&qy` zmD)iDQ3A~&hfo6X(I>aXIkeaQ6_Q!-vQP+=9CA}gbIGZ1R@$`{H;0b2vv1z~=6%g? zW`D_KBn0h`KgZXv3JCqp24Tzeg_m0(JVqMQI0LQr-rOn&EZ@&CRP(5 za~N%atzOf3Em7q)0VlO2PT=H3IDs?~i&_dE37#~pNrh8YS(EQ2Rwa$Qjf$D)Ouby> z{Lx#sqdK}(Tha~ug=%RACZHBq>W*$(s-Yvahn^<@?emsuVqY}uTDb~fKVjfI*zi-P zx>a7=b};qjHMO$YsFy2t!*YCqV5dQ>3R@gDCdc7a!y?rMG07JQe@jGL=pvfs%rH3pQL*pxwrf(x@w{0#vJE zqWqvc>q^D8sBK^cD5@~}DeFD5(s?OZ^cktq#Xw>v7*!XQW#|>VsyOSoZz}?XEM8Q; zQo*`oGeFO9!R~Xvc#LLDUsP^DJaAN$fcVj5#Q#O*rn;q@4O5}|-N12Va}8H*f)x@R zHrtBUo;)xRZ^K-Jd?DhVK8T01;5IR?_wu|I9I6wkUbC&LUTYAnh4s=c>Zn!)2VU-e zGF4D%#nDY%q{^H!RbaL0i$NkM#wMmzt-)SzL;7aZ( z$Oqt@uuj~?;LA+NM2 zKko3CUH)>LzYJcE_}n3%duVs~X_udF^V3}ci9=l^B?`|*&OVxXT>81RSLlq)x+Amq zlU{nHi@4OKzvS$L>$}NcFSg}UM=rT?34~{3AA0iH-R<9vZag+Fy+F`GG~?2J8H}cK zN8;q6IJr095f@x>p)D?WV)jU!I20##KkA4xt~k>cXP%~qTQ^@qX&4`d;6>PN=nt{N z0UzhX?F%U53(j^Ow1j;TeE!Sjr4>I;9pcAz_Tjq`kR~YhZ-`uA#wMYAEq-d+TEoCs z$Se%m_l-V*t{dk#&O_N&@bgfr75qGOsXh4j&^ztHe^)~9U+{*WPzVBw3mYMjkF39Fz D($Y9} literal 0 HcmV?d00001 diff --git a/src/permission_scanner/utils/__pycache__/markdown_generator.cpython-311.pyc b/src/permission_scanner/utils/__pycache__/markdown_generator.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..19c0f46b1408685f42800018c63d2f2d9a595631 GIT binary patch literal 4817 zcmd5Eo96(+?YMg3W}+Fo(i;uiu6#Vm%btO zL(5W8bb+>`(c{dUH#2YE%zW?7&jJB21xf#P_ri|?6!iyEsV2Tsc=a17d_swoNN1@W zJwxYQGp-yn!_b81%Ay%0G7>GhBvxYQTs_tPRJ|l3tCexKibR?dxgVk#u4OwC-O%n9 zd5Qgx=io&TjKPbpThyTUk3_pc#zZ$VvSOkunY3aum-3G%%|N=4S8yt=B*m1HGW~Ko zl}}yv8G8?Bl1;IhI$4od|q2+b2*XQJ2Q5P?mR8> zD_qM}c~tZnC3;oXq8w1)Q}^CcXzIhH=r7jh-lJKARbVFr%l{ac@)AxdlAtU|LUs3K zq3}?`4>QsuK}pSKr6I3ZfOHAZ%L%JQYE@xttA!{z3wOy;xGO98Edj{AdxHsqu!X{0 zO)|rn3Zx;UP&pLLjJW&6<#wi&1;vSnzR=#7nfRG zl=7m<%u8^`9uzY9K{w`LL$Mo(=_clf?j~Q|2gl_j8ekt0t6xY379f+ZWbYuo&cweh zOIVKQAHt2z&&TIyv#D7*E=qHm7%W;!;q*d$QNp>5EQ5+M%j6P{%Q)>|C#&owC6kro zxr)t`^Y*za;GxCGW_P6~C9BHHN$X(YFet>rU;aIiCsetc3U)jhFCpKLCx0-xc3ww) z2I|vLUxlNE&gm#&poE4JHP#s&oi)%|4V~TXI$EZ9c7nF)DqrdriU*DE=xU(U*}Hzu z=p0yempQ6EvJ-q`JNQO1uz~g9h!Gsw37**wp4t4q9vnA<<7LJb=vy6M9pCLZT6R;O zgJmz}Y5%D0gEsBZ_4NrLkT>S-?^ewI0@e8&27g1d@~$uV(YX)Kt-Y)J4jaD18an(6 zmPnKLcd?ajkVB^23#Rx`%)A1YbDum&jBY}Nj5>h=4kZ{^m$&>2@vwn-vNzR zTq}T*D)W%Si~H~rHAf+loo~QL80}jttje~K=Njb!AvxJEx>XkDby{qzt+TD(XT`0$Tj>42Tc`W0tnEnh5RoZ$A1y<9SYfyAM^s|Pt`z8ULJI!zAAiUDW4{Q3{VUR4S@_4ZaxAR z3D`ea2m4slVwI_y9g7y|JEk{1)uCn6Gf@yTa~S|%lS_hwk}wW}xy?Y;86}DP!R>^{ zSGz5HK$W4PA)vT}wD=sO<4)iVA{-(OY(|lCGVX#hK0x@qE^6pvjRjDD!9W)@bYVC8_D(dh9Zl%bF(W!wrVg;37SZ`*tAVwZUEh&nO!LJd zmxP!$bjc7dYyQKfNNgu^YCCdj^W+xRBbSZHGTbJKE9HkV}zc#XCl1XzlBz_CuevecV>+TF`pl+_+%$jA~HqT6n>~ z>@_alwkujvtQ5Ky{>r#II{!uiDc6aY^7nwd@C`yP^`6)W7`6jTNZ9E^gj?h-0v=-8 zvfy&iTkl@TCxqpWf%jqqx!8ax3|vhNOe6+wEqkWydzBDu){b2JhBrW-42gxV>k8ilo6iN z_-V)hE-VKa$Z~*zK7o zWp%VH_L?8d6~v{ibRLgD6S;EopMaDZnx;!spVm5;sNfUpSLVX>ky7Z;I$FO~>|ek0 zlQtuCqRbHH+0m_cUWB%;{_1;HeV66SEbz6e>Y??tVuz-Otm1EHU0_R C$b)DA literal 0 HcmV?d00001 diff --git a/src/permission_scanner/utils/__pycache__/reporter.cpython-311.pyc b/src/permission_scanner/utils/__pycache__/reporter.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4111be2716df6adb2ea74bb9bfd8238207ff7019 GIT binary patch literal 10798 zcmc&)Z)_V!cAw=g|7s;sA|+FQD6cKa5oL>tW66;f#*S?zmjB6iDsl4EGzvJs!4;J&L`;$cRKdYVWI>K4gl{M)2L>w% z^5Q}`o|>Bz#B>X!(y%Bjq(oT|gYXp98q}ywe|Y#VWLA-Y@Hl2HJT*h}7J-?y@br7k zjFo2u7yRuYq^LDBXqT zmq}lb2uVSV$SE;B_KF;f$K)$qG#-&8ZZ;)yb6R06Imc-ulek!tyA%;)sYOyF5s{@} z!IfH+7Z&9(9}}Y`MZ-ApMAGp8Fii%Xp)x28IW5SzL=W?&w=xt@eQq&N425JveNaDZ zEDz&llJazh`YHO@@(E1NXLy20z>5P0%S@fc6DVAUnGC1iO7A(EjLES`JeC%Cu1XEu zQcS+cX#?V5KtdFBb7hI3zn2ra3msc2~Z{#Y})J>m{@W)g_n#Z#AiuG2@i?m z9*2itkpxj1PF|A5NOEp??#+1QO=*}HW@AI5APEsMdU1F`5EC&;ilvf6QZ$l;85x$u zXazfLuoq#DDwf&8m4YK2j=^xk;k2)6C_$s75xOEB0CE-OJNs6c51Q{eTpu)CJGSms z9KoC;s5*j>x#wv5u;Kj%#e44dAP|T-$9dIpUNPcB5P=G|1ra)bxxg-nsd-ROI2lO@ z1)B)gASR;~vx=-xoy`B6kXc1?XY$Bo>0{#{LWaiGDuZKiPk>f4V@01GI>{ zTA-7urGi6}#aUcCz=$ot727ar$K}qF5D70r4uaT<3tGdvr3hi8)%5mFv$3fTeph-H z$W`>UxBH_LKbiVy>aKU^9q-QdQ#o%)^@diQ`BtCOJPL6wq^0WtEma(&q8B&rz)dbh zu9Qfi^D9b2YRFOnUlil`5XdzKlKL_CR`0?*)` zfw#)o3iOa+%?uW(CZ3aV=RCe$3~(w#3wV5)fh+7gjdD&*1@*kbGjCbb|W3szDpHbZYk zH;{t!#miA)L5BUIU=>6$B^GE{rLZND1=@q_ZN>Gd*#ad>Wi~6OvHt?#?t;x z>$0+V3%o1+H;}8S$k?2%_gdPnUCh$CmOizmuZXC|ZR=Ft-Lh)GZeO$K+??v>6gO9- zp@5VU@&MO+gh`oKEBEcU-_~M(&9;7c{QcvK@9+&D5QsT;LS-iuBYu0Y*;hmsXKR_< zgLh86e?sXvws8>%#4meqMsDr+-45+3#~xSN;|hB`&$d9b-xjU#I;7GwFxGe72Yb-< z_7QrWh0H%QWA+;~Wd4mBvwy)s=2nmEP#1k`Ab6;q{%yMz@^JXWJ`F0B7UQyHGB-O2 zBM-0?P+uZF9+>`I1RxXtTU!#)+Wynnsaf zD$QXY_n8FP4(QJrvmEmig)$5=MZ7Te0H*l#vQ@_0nI5v_c4s^pDHtJMUwX(`HyPok z?FEd`wrqXJT499t^v;S+{@C=XDZ|E6vz&ZU;PA`0&`>^GrL;9TFQt;-U- zG2eci+W{nC(~NZxGstN|_HBGXiNt^hpg_X+9Adh!B?3%jOMV&NHrh##bCg z#_DX%v+lcW&mFdB?eaf)KkwW)al1tc9M1)gt3cTkDtkg6n0+V_#I+7Zvu!YC8lFfNr6k2X~=cyIhBM(6{#PJhYAe?KUgqP4&Z>_VNv~H?ducw}x`r5f{uiIm9#?H4@_U&WK zT-7BMdS3aU^AS%eant_92Mtoe|os$ z+)YHpxA@c&K4`JWgR6D+io2l2$8<2kr?>TTr%M=@JD7~buShXT$KJ%%P@r6on~Vl{ zE|nbT9=G&g80t?9_48c+J;5aIUPm8I`SJG|0@G{AAM*+Hv;K+d6b=r3{u@Dy$a4R6sTvJYH;ynv^ETU|p#L#}jR4J=}qn7-FyJmN0n5pfur5%CJs&>&T6lWDk5%*w0We0-;OpTSliw!j%q&8TEeL;(P_T_r#q=)db5xZ@jG@7j1X=i8_H_7%~XbMOHsE0cNF^WoI{ zQ}?|GvlrKYtnN6Vcn?C%d!NlZ);m;gkK%n+iy*P3^#N^dYC4gqjn9lJiZc0A`9P(1^i$!(m*@PaRsU%idPgS=y{#RF-qsF72hxPRJ}ge@-jnl;sGgA` za#qM2FVeP-&LU&%g7R`pdDB?kRl=8lI&-4o;|8( zkGZe=^uFS4XJbOycS;?BgXm1oe@6A6`4gQT&3X2!p1o#)AzdK0VJz@vbz~Z{IsaMJ zf3}Xm79XDEwJSMyKy?QccK|XEPEs%##nY4-aIbrR)$xxV8_(tfgua%@OtLkb)E zlW2SLOZw))TejcXl#WS=IrgZ^!V*7P3vK_taq#FE`u&*e*faDWEIW@4(SI1SLf(Y7 zS+GKaw#S+OZ?s*BumKsDFgEnC6nMHC>6!1k;HAQD89nd}V1Xx=RE^52B->1qtv{e- zJ8vU?20Qo}95QzK>S4Zw_04#(4qkSedkmfnEYZa`WURy=z-DYDZOqv9vZguvvg6Ou z&>KHrc4i#en#hN5&Nz8@8F!JkT7cJD#q*eE+F56&z2804So+*%UqU@;ZD!p7Vs~Wm z+TRrTcIfez${zDR;8b8}&|T-pRag$tTRnMQ=9Ve+!7m7UtoU3}?gXGmdmZ#xh3e{{ z$L_K$f*xHmKHT+?t?=m@Jn&C-%f02NjLY0#tYUEr}`*DHy8p>c5>hzaLlR~o*YJbWFi%HzT5Xl8a2AFIab;0?AmMV+jH(A)jgDBgDM-u079!r zDpwJ5%`~YHc63dJ6ObvXuxptDwE<^y;E`EPzuLl6x~jT)_&^8WQJ;smM7%Xm1t_8_ z@ilm;)RBlM?E7fvT`Uj-GebKJ9yJiqPQH1I>2hTUcAjl4(ncLpv5IIH@a?r-6w~@r z#XpA-@wZxW43m98((QoIjdBdf0Px15k}KUk#GOsak$A~FA!#lT__+am1?)3Q4QRjF z69Bb5L%Msy@M?e~1A8q>GPzTi1o2W#SaPKq>?#nS!(D0^D*G(N5a#p@eTU0ooFj{y zOk28jh%NNcV}ie{)I=!_bx}&8S;mUTo&(KGWH2yVA)bUnGyH7f@|AK8^5v9RvW8~x*;PU_ z2AjquF7F9o1r;rLc!fJF0tL43Kultl|j;Mi2#rrnI+mp&G3H3x$djk4^wqSq;1U^#(0Zd8& z+S)TYwnt@q6cVdHwf&_x|0BPVZtni|r7tci9Y-MM*h!V0R9Jwk=I`CuE%+a!gGV^@ z73Z4lq`w;6IoU?vZnHu@z~WyM;ZKs`aKRN0gIjPhj`M6d{Ptobu9w)uVLlZNhsk$M z5jz_-TPOC4!Gbt~$vI4TOt7PkY_gg+3Ex^pOz=lysRd5xtEIm@b4Xzh0SL6dN@W4W z;j{*I(bm1|9$c{Z#$l4&)Dn`fATEcsH5&ztXl>nq8j#$WAW1dxx}b))h#Fca(yfXNw4@x1kR>RnF)P?a@TfS%UBavnk33;kPVM@iKZ>Aqkv1?jrB!7 zJ=^3f3-Dt+ktmtLY~t-9Uz+iSNE9$3`ZeqPu_L5S=6-P=s^CkJ^mjnuhZ_`?M-5lW zUmiKHlD|CiE4AWgejag(Io@}+t#n_zsBn97&Joo)a@AI}y>6jkthMO_!hck!lqlkV E0oa|^=Kufz literal 0 HcmV?d00001 diff --git a/src/permission_scanner/utils/__pycache__/reporters.cpython-311.pyc b/src/permission_scanner/utils/__pycache__/reporters.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6c8583cba48280b75ce1cb98dc76dc5c934d8d78 GIT binary patch literal 9044 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 literal 0 HcmV?d00001 diff --git a/src/permission_scanner/utils/__pycache__/scanner_config.cpython-311.pyc b/src/permission_scanner/utils/__pycache__/scanner_config.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..411ecdf38c1f4786516d924e43ccd4953905160b GIT binary patch literal 2868 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 literal 0 HcmV?d00001 diff --git a/src/permission_scanner/utils/block_explorer.py b/src/permission_scanner/utils/block_explorer.py new file mode 100644 index 0000000..5765806 --- /dev/null +++ b/src/permission_scanner/utils/block_explorer.py @@ -0,0 +1,85 @@ +import requests +from typing import Dict, Optional, Any +import json +from pathlib import Path +from .logger import setup_logger + +logger = setup_logger(__name__, "logs/block_explorer.log") + +# Read the config file from the same directory as this script +with open(Path(__file__).parent / "block_explorer_config.json", "r") as f: + block_explore_config = json.load(f) + + +class BlockExplorer: + """Service for interacting with Etherscan API.""" + + def __init__(self, api_key: str, chain_name: str): + if not api_key: + raise ValueError("API key is required") + self.api_key = api_key + self.chain_name = chain_name + try: + self.base_url = block_explore_config[chain_name]["base_url"] + except KeyError: + raise ValueError( + f"Unsupported chain: {chain_name}. Supported chains are: {', '.join(block_explore_config.keys())}" + ) + + logger.info(f"Initialized BlockExplorer for chain: {chain_name}") + + def _make_request( + self, module: str, action: str, address: str, chainid: Optional[int] = None + ) -> Dict[str, Any]: + """ + Make a request to the BeraScan API. + + Args: + module (str): The API module to call. + action (str): The action to perform. + address (str): The contract address. + chain_id (int, optional): The chain ID, etherscan has v2 api that supports 50+ chains + Returns: + dict: The API response data. + + Raises: + requests.exceptions.RequestException: If the API request fails. + ValueError: If the response indicates an error. + """ + params = { + "module": module, + "action": action, + "address": address, + "apikey": self.api_key, + } + if chainid: + params["chainid"] = chainid + + try: + with requests.get(self.base_url, params=params) as response: + if response.status_code != 200: + raise ValueError(f"API Error: {response.text}") + data = response.json() + except Exception as e: + raise RuntimeError(f"Request failed: {e}") + + if data["status"] != "1": + raise ValueError(f"API Error: {data.get('message', 'Unknown error')}") + + return data["result"] + + def fetch_contract_metadata(self, address: str) -> Dict: + """ + Fetch contract metadata from Etherscan, + including contract name, proxy status, and implementation address. + """ + chainid = block_explore_config[self.chain_name]["chainid"] or None + result = self._make_request( + module="contract", action="getsourcecode", address=address, chainid=chainid + ) + contract_info = result[0] + return { + "ContractName": contract_info.get("ContractName"), + "Proxy": contract_info.get("Proxy") == "1", + "Implementation": contract_info.get("Implementation"), + } diff --git a/src/permission_scanner/utils/block_explorer_config.json b/src/permission_scanner/utils/block_explorer_config.json new file mode 100644 index 0000000..b7811d4 --- /dev/null +++ b/src/permission_scanner/utils/block_explorer_config.json @@ -0,0 +1,4 @@ +{ + "mainnet": { "base_url": "https://api.etherscan.io/v2/api", "chainid": 1 }, + "bsc": { "base_url": "https://api.etherscan.io/v2/api", "chainid": 56 } +} diff --git a/src/permission_scanner/utils/logger.py b/src/permission_scanner/utils/logger.py new file mode 100644 index 0000000..3c75f29 --- /dev/null +++ b/src/permission_scanner/utils/logger.py @@ -0,0 +1,56 @@ +import logging +import os +from logging.handlers import RotatingFileHandler +from typing import Optional + + +def setup_logger( + name: str, + log_file: Optional[str] = None, + level: int = logging.INFO, + max_bytes: int = 10 * 1024 * 1024, # 10MB + backup_count: int = 5, +) -> logging.Logger: + """ + Set up a logger with console and file handlers. + + Args: + name: Name of the logger + log_file: Path to log file (optional) + level: Logging level + max_bytes: Maximum size of log file before rotation + backup_count: Number of backup files to keep + + Returns: + Configured logger instance + """ + logger = logging.getLogger(name) + logger.setLevel(level) + + # Create formatters + console_formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + ) + file_formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s" + ) + + # Console handler + console_handler = logging.StreamHandler() + console_handler.setFormatter(console_formatter) + logger.addHandler(console_handler) + + # File handler (if log_file is provided) + if log_file: + # Create logs directory if it doesn't exist + log_dir = os.path.dirname(log_file) + if log_dir: + os.makedirs(log_dir, exist_ok=True) + + file_handler = RotatingFileHandler( + log_file, maxBytes=max_bytes, backupCount=backup_count + ) + file_handler.setFormatter(file_formatter) + logger.addHandler(file_handler) + + return logger diff --git a/src/permission_scanner/utils/markdown_generator.py b/src/permission_scanner/utils/markdown_generator.py new file mode 100644 index 0000000..ccef468 --- /dev/null +++ b/src/permission_scanner/utils/markdown_generator.py @@ -0,0 +1,101 @@ +from typing import List, Dict, Any +import datetime + + +def generate_contracts_table( + contract_data: List[Dict[str, str]], scan_results: Dict[str, Any] +) -> str: + """Generate the contracts overview table. + + Args: + contract_data (List[Dict[str, str]]): List of contract metadata + scan_results (Dict[str, Any]): Results from contract scanning + + Returns: + str: Markdown table of contracts + """ + content = [] + content.append("## Contracts") + content.append("\n| Contract Name | Address | Type |") + content.append("|--------------|---------|------|") + + for contract in contract_data: + contract_name = contract["name"] + address = contract["address"] + contract_type = ( + "Proxy" + if scan_results.get(contract_name, {}).get("Proxy_Address") + else "Implementation" + ) + content.append(f"| {contract_name} | {address} | {contract_type} |") + + return "\n".join(content) + + +def generate_permissions_table(scan_results: Dict[str, Any]) -> str: + """Generate the permissions table. + + Args: + scan_results (Dict[str, Any]): Results from contract scanning + + Returns: + str: Markdown table of permissions + """ + content = [] + content.append("\n## Permissions") + content.append("\n| Contract | Function | Impact | Owner |") + content.append("|----------|----------|---------|-------|") + + for contract_name, contract_data in scan_results.items(): + # Handle proxy permissions if they exist + if "proxy_permissions" in contract_data: + proxy_permissions = contract_data["proxy_permissions"] + for function in proxy_permissions.get("Functions", []): + owner = function.get("Modifiers", []) + if not owner and "_owner" in function: + owner = function["_owner"] + content.append( + f"| {proxy_permissions['Contract_Name']} | {function['Function']} | ... | {owner} |" + ) + + # Handle implementation permissions + if "permissions" in contract_data: + permissions = contract_data["permissions"] + for function in permissions.get("Functions", []): + owner = function.get("Modifiers", []) + if not owner and "_owner" in function: + owner = function["_owner"] + content.append( + f"| {permissions['Contract_Name']} | {function['Function']} | ... | {owner} |" + ) + + return "\n".join(content) + + +def generate_full_markdown( + project_name: str, contract_data: List[Dict[str, str]], scan_results: Dict[str, Any] +) -> str: + """Generate a full markdown report from scan results. + + Args: + project_name (str): Name of the project being scanned + contract_data (List[Dict[str, str]]): List of contract metadata + scan_results (Dict[str, Any]): Results from contract scanning + + Returns: + str: Generated markdown content + """ + content = [] + + # Add header + content.append("# Permission Scanner Report") + content.append( + f"\nGenerated on: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + content.append(f"Project: {project_name}\n") + + # Generate tables + content.append(generate_contracts_table(contract_data, scan_results)) + content.append(generate_permissions_table(scan_results)) + + return "\n".join(content) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..d4b7daa --- /dev/null +++ b/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 2 +requires-python = ">=3.11" + +[[package]] +name = "permission-scanner" +version = "0.1.0" +source = { editable = "." } From a155680156224cef9ee032ff3a6dd87dcaa10293 Mon Sep 17 00:00:00 2001 From: trangnv Date: Mon, 5 May 2025 20:23:16 +0700 Subject: [PATCH 2/8] refactor --- old_main.py | 401 ---------------------------------------------------- 1 file changed, 401 deletions(-) delete mode 100644 old_main.py diff --git a/old_main.py b/old_main.py deleted file mode 100644 index bf4d98a..0000000 --- a/old_main.py +++ /dev/null @@ -1,401 +0,0 @@ -from slither.slither import Slither -from slither.core.declarations.function import Function -from slither.core.declarations.contract import Contract - -from slither.tools.read_storage.read_storage import ( - SlitherReadStorage, - RpcInfo, - get_storage_data, -) - -import json -from typing import List -import urllib.error - -from parse import init_args -from get_rpc_url import get_rpc_url, get_chain_id -from etherscan import get_etherscan_url, fetch_contract_metadata -from dotenv import load_dotenv - -import re - -from markdown_generator import generate_full_markdown - - -def load_config_from_file(file_path: str) -> dict: - with open(file_path, "r") as file: - return json.load(file) - - -def is_valid_eth_address(address: str) -> bool: - return bool(re.fullmatch(r"0x[a-fA-F0-9]{40}", address)) - - -# check for msg.sender checks -def get_msg_sender_checks(function: Function) -> List[str]: - all_functions = ( - [f for f in function.all_internal_calls() if isinstance(f, Function)] - + [ - m - for f in function.all_internal_calls() - if isinstance(f, Function) - for m in f.modifiers - ] - + [function] - + [m for m in function.modifiers if isinstance(m, Function)] - + [call for call in function.all_library_calls() if isinstance(call, Function)] - + [ - m - for call in function.all_library_calls() - if isinstance(call, Function) - for m in call.modifiers - ] - ) - - all_nodes_ = [f.nodes for f in all_functions] - all_nodes = [item for sublist in all_nodes_ for item in sublist] - - all_conditional_nodes = [ - n for n in all_nodes if n.contains_if() or n.contains_require_or_assert() - ] - all_conditional_nodes_on_msg_sender = [ - str(n.expression) - for n in all_conditional_nodes - if "msg.sender" in [v.name for v in n.solidity_variables_read] - ] - return all_conditional_nodes_on_msg_sender - - -def get_permissions( - contract: Contract, - result: dict, - all_state_variables_read: List[str], - isProxy: bool, - index: int, -): - - temp = {"Contract_Name": contract.name, "Functions": []} - - for function in contract.functions: - # 1) list all modifiers in function - # for output analysis - modifiers = function.modifiers - for call in function.all_internal_calls(): - if isinstance(call, Function): - modifiers += call.modifiers - for call in function.all_library_calls(): - if isinstance(call, Function): - modifiers += call.modifiers - - listOfModifiers = sorted([m.name for m in set(modifiers)]) - - # 2) detect conditions on msg.sender - # in the full function scope - msg_sender_condition = get_msg_sender_checks(function) - - if len(modifiers) == 0 and len(msg_sender_condition) == 0: - # no permission detected - continue - - # list all state variables that are read - # the variables available in storage will be read - state_variables_read_inside_modifiers = [ - v.name - for modifier in modifiers - if modifier is not None - for v in modifier.all_variables_read() - if v is not None and v.name - ] - - state_variables_read_inside_function = [ - v.name for v in function.all_state_variables_read() if v.name - ] - - all_state_variables_read_this_func = [] - all_state_variables_read_this_func.extend(state_variables_read_inside_modifiers) - all_state_variables_read_this_func.extend(state_variables_read_inside_function) - all_state_variables_read_this_func = list( - set(all_state_variables_read_this_func) - ) - - all_state_variables_read.extend(all_state_variables_read_this_func) - - # 3) list all state variables that are written to inside this function - state_variables_written = [ - v.name for v in function.all_state_variables_written() if v.name - ] - - # 4) write everything to dict - temp["Functions"].append( - { - "Function": function.name, - "Modifiers": listOfModifiers, - "msg.sender_conditions": msg_sender_condition, - "state_variables_read": all_state_variables_read_this_func, - "state_variables_written": state_variables_written, - } - ) - - # dump to result dict - if isProxy and index == 0: - result["proxy_permissions"] = temp - elif isProxy and index == 1: - result["permissions"] = temp - else: - # is normal contract - result["permissions"] = temp - - -def main(): - load_dotenv() # Load environment variables from .env file - - # load contracts from json - config_json = load_config_from_file("contracts.json") - - contracts_addresses = config_json["Contracts"] - contract_data_for_markdown = [] - project_name = config_json["Project_Name"] - chain_name = config_json["Chain_Name"] - - rpc_url = get_rpc_url(chain_name) - platform_key = get_etherscan_url() - - result = {} - - # instantiate slither rpc class - rpc_info = RpcInfo(rpc_url, "latest") - - for contract_address in contracts_addresses: - contract_result = fetch_contract_metadata( - address=contract_address, - apikey=platform_key, - chainid=get_chain_id(chain_name), - ) - contract_name = contract_result["ContractName"] - isProxy = contract_result["Proxy"] == 1 - implementation_address = contract_result["Implementation"] - implementation_name = "" - contract_data_for_markdown.append( - {"name": contract_name, "address": contract_address} - ) - - if isProxy and implementation_address: - if not isinstance(implementation_address, str) or not is_valid_eth_address( - implementation_address - ): - raise ValueError( - f"Invalid implementation address for proxy: {implementation_address}" - ) - try: - implementation_result = fetch_contract_metadata( - address=implementation_address, - apikey=platform_key, - chainid=get_chain_id(chain_name), - ) - implementation_name = implementation_result.get("ContractName") or "" - contract_data_for_markdown.append( - {"name": implementation_name, "address": implementation_address} - ) - except Exception as e: - raise f"Failed to get Implementation contract from Etherscan. \n\n\n + {e}" - - target_storage_vars = [] # target storage variables of this contract - temp_global = {} - - # setup args for slither - args = init_args( - project_name, - contract_address, - chain_name, - rpc_url, - platform_key, - contract_name, - ) - target = args.contract_source - - try: - slither = Slither(target, **vars(args)) - except urllib.error.HTTPError as e: - print( - f"\033[33mFailed to compile contract at {contract_address} due to HTTP error: {e}\033[0m" - ) - continue # Skip this contract and move to the next one - except Exception as e: - print( - f"\033[33mAn error occurred while analyzing {contract_address}: {e}\033[0m" - ) - continue - - # retrieved contracts from the address (inherited and interacted contracts) - contracts = slither.contracts - - # only take the one contract that is in the key - # this filters out interacted contracts (we dont need the permissions of them) - # does not exclude inherited contracts - target_contract = [ - contract for contract in contracts if contract.name == contract_name - ] - - if len(target_contract) == 0: - raise Exception( - f"\033[31m\n \nThe contract name supplied in contract.json does not match any of the found contract names for this address: {contract_address}\033[0m" - ) - - srs = SlitherReadStorage(target_contract, args.max_depth, rpc_info) - srs.unstructured = False - # Remove target prefix "mainnet:" e.g. mainnet:0x0 -> 0x0. - address = target[target.find(":") + 1 :] - srs.storage_address = address - - if isProxy: - # step 1: create slither object again, but with implementation address - # -> run analysis of storage layout and permissions of implementation address - # step 2: read storage from proxy contract (location of storage) contract_address["address"] - - # scan the implementation address - slither = Slither(f"{chain_name}:{implementation_address}", **vars(args)) - - # get all the instantiated contracts (includes also interacted contracts) from the implementation contract - implementation_contracts = slither.contracts_derived - - # find the instantiated/main implementation contract - - target_contract.extend( - [ - contract - for contract in implementation_contracts - if contract.name == implementation_name - ] - ) - if len(target_contract) == 1: - raise Exception( - f"\033[31m\n \nThe implementation name supplied in contract.json does not match any of the found implementation contract names for this address: {contract_address['address']}\033[0m" - ) - temp_global["Implementation_Address"] = implementation_address - temp_global["Proxy_Address"] = contract_address - - if not isProxy: - temp_global["Address"] = contract_address - - # end setup - ################################################## - ################################################## - ################################################## - - ################################################## - ################################################## - ################################################## - # start analysis - - # start analysis of main contract (can be proxy, then also the implementation contract is analysed) - for i, contract in enumerate(target_contract): - # get permissions and store inside target_storage_vars - get_permissions(contract, temp_global, target_storage_vars, isProxy, i) - - target_storage_vars = list(set(target_storage_vars)) # remove duplicates - - # Three steps to retrieve storage variables with slither - # 1. set target variables - # 2. compute storage keys - # 3. retrieve slots from the keys - - # sets target variables - # adapted logic, extracted from method `get_all_storage_variables` of SlitherReadStorage class - for contract in srs._contracts: - for var in contract.state_variables_ordered: - if var.name in target_storage_vars: - # achieve step 1. - srs._target_variables.append((contract, var)) - - # add all constant and immutable variable to a list to do the required look-up - if not var.is_stored: - - # functionData is a dict - for functionData in temp_global["permissions"]["Functions"]: - # check if e.g storage variable owner is part of this function - if var.name in functionData["state_variables_read"]: - # check if already added some constants/immutables - - # Ensure key exists - if "immutables_and_constants" not in functionData: - functionData["immutables_and_constants"] = [] - - # Check if the variable has an expression and is not the proxy marker - if ( - var.expression - and str(var.expression) - != "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc" - ): - try: - raw_value = get_storage_data( - srs.rpc_info.web3, - contract_address, - str(var.expression), - srs.rpc_info.block, - ) - value = srs.convert_value_to_type( - raw_value, 160, 0, "address" - ) - functionData["immutables_and_constants"].append( - { - "name": var.name, - "slot": str(var.expression), - "value": value, - } - ) - except Exception: - functionData["immutables_and_constants"].append( - {"name": var.name, "slot": str(var.expression)} - ) - else: - functionData["immutables_and_constants"].append( - {"name": var.name} - ) - - # step 2. computes storage keys for target variables - srs.get_target_variables() - - # step 3. get the values of the target variables and their slots - try: - srs.walk_slot_info(srs.get_slot_values) - except urllib.error.HTTPError as e: - print( - f"\033[33mFailed to fetch storage from contract at {contract_address} due to HTTP error: {e}\033[0m" - ) - continue # Skip this contract and move to the next one - except Exception as e: - print( - f"\033[33mAn error occurred while fetching storage slots from contract {contract_address}: {e}\033[0m" - ) - continue - - storageValues = {} - # merge storage retrieval with contracts - for key, value in srs.slot_info.items(): - contractDict = temp_global["permissions"] - storageValues[value.name] = value.value - # contractDict["Functions"] is a list, functionData a dict - for functionData in contractDict["Functions"]: - # check if e.g storage variable owner is part of this function - if value.name in functionData["state_variables_read"]: - # if so, add a key value pair to the functionData object, to improve readability of report - functionData[value.name] = value.value - - if len(storageValues.values()): - contractDict["storage_values"] = storageValues - - if len(implementation_name) > 0: - result[implementation_name] = temp_global - else: - result[contract_name] = temp_global - - with open("permissions.json", "w") as file: - json.dump(result, file, indent=4) - - content = generate_full_markdown("", contract_data_for_markdown, result) - - with open("markdown.md", "w") as file: - file.write(content) - - -main() From 3e29fdb20613bdefeb2ffa1c4736d60aa5a0a53c Mon Sep 17 00:00:00 2001 From: trangnv Date: Mon, 5 May 2025 20:33:05 +0700 Subject: [PATCH 3/8] bring back whole list of contracts in example/contracts.json --- example/contracts.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/example/contracts.json b/example/contracts.json index fb9c24a..ef2b537 100644 --- a/example/contracts.json +++ b/example/contracts.json @@ -1,5 +1,17 @@ { "Chain_Name": "mainnet", "Project_Name": "Lido-governance", - "Contracts": ["0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc"] + "Contracts": [ + "0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc", + "0x2e59A20f205bB85a89C53f1936454680651E618e", + "0xf73a1260d222f447210581DDf212D915c09a3249", + "0xB9E5CBB9CA5b0d659238807E84D0176930753d86", + "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", + "0x9895f0f17cc1d1891b6f18ee0b483b6f221b37bb", + "0x0cb113890b04b49455dfe06554e2d784598a29c9", + "0x4ee3118e3858e8d7164a634825bfe0f73d99c792", + "0xF5Dc67E54FC96F993CD06073f71ca732C1E654B1", + "0x0D97E876ad14DB2b183CFeEB8aa1A5C788eB1831", + "0x2325b0a607808dE42D918DB07F925FFcCfBb2968" + ] } From 5d766e2eaea8f782dd4baaf29807a9cac4f6aa7d Mon Sep 17 00:00:00 2001 From: trangnv Date: Fri, 9 May 2025 16:32:01 +0700 Subject: [PATCH 4/8] old method with block_explorer --- .DS_Store | Bin 0 -> 6148 bytes .gitignore | 5 +- example/contracts.json | 14 +- example/contracts_full.json | 17 + example/run_scanner.py | 104 ++-- .../__pycache__/__init__.cpython-311.pyc | Bin 376 -> 376 bytes .../__pycache__/scanner.cpython-311.pyc | Bin 21359 -> 23779 bytes src/permission_scanner/scanner/scanner.py | 520 ++++++++++-------- .../block_explorer.cpython-311.pyc | Bin 4955 -> 11425 bytes .../utils/__pycache__/logger.cpython-311.pyc | Bin 2274 -> 2353 bytes .../markdown_generator.cpython-311.pyc | Bin 4817 -> 2416 bytes .../utils/block_explorer.py | 222 +++++++- .../utils/block_explorer_config.json | 5 +- src/permission_scanner/utils/logger.py | 56 -- .../utils/markdown_generator.py | 145 ++--- tests/metadata.py | 32 ++ 16 files changed, 671 insertions(+), 449 deletions(-) create mode 100644 .DS_Store create mode 100644 example/contracts_full.json delete mode 100644 src/permission_scanner/utils/logger.py create mode 100644 tests/metadata.py diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..1fca7aaf248e7932445b8948c2e295145554a6c4 GIT binary patch literal 6148 zcmeHKJx{|h5PgOYl~^(|`VTm)LoA3HnkJMY2vw6X=cn`T&VV8!!Bkb~uCm|x zKJ3ejYR3S=yelt&48W94Fo-fDVjguJrC`CJbF5I|9xJ?|9GU14O;US?h81_%vGVWw zE1ct5J@vLNuDhn)G4>L7v{j64Fhh-ZwA7c>)*P=<;Dw_?y(QxwvB4Q5D$QEz8c&Se z>Kw21HvDb4e$BXs706t*dS`#E?IzXs>vJ693-Fqp0cXG&a0d3y0MBfZ>4~BD&VV!E z416&l`$J?C%oFy8x^=MAD*$mqvkG;&OGr*km?!KF>7fKmC0c4oSBzlkw8s>eC+rO^ z9YI&-JZa_6%S+JJX^$F?kQ;jM3^)Tr299l*$@zc9zs%?*f0*JUXTTZwXAFeN;&w6T zL#4Cz$K%Oa8`&P%L?o^kg+hJs5x{}$Bd6*#e-xc@dBWaMW)Zul6a7O#3GvPu*aHI} Db}c*% literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore index 0befd64..d8fd434 100644 --- a/.gitignore +++ b/.gitignore @@ -5,10 +5,11 @@ src/__pycache__ /contracts.json /permissions.json -/results +/results/ # markdown generation outputs *.md .venv logs -.cursor \ No newline at end of file +.cursor +temp/ \ No newline at end of file diff --git a/example/contracts.json b/example/contracts.json index ef2b537..7a32b29 100644 --- a/example/contracts.json +++ b/example/contracts.json @@ -1,17 +1,5 @@ { "Chain_Name": "mainnet", "Project_Name": "Lido-governance", - "Contracts": [ - "0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc", - "0x2e59A20f205bB85a89C53f1936454680651E618e", - "0xf73a1260d222f447210581DDf212D915c09a3249", - "0xB9E5CBB9CA5b0d659238807E84D0176930753d86", - "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", - "0x9895f0f17cc1d1891b6f18ee0b483b6f221b37bb", - "0x0cb113890b04b49455dfe06554e2d784598a29c9", - "0x4ee3118e3858e8d7164a634825bfe0f73d99c792", - "0xF5Dc67E54FC96F993CD06073f71ca732C1E654B1", - "0x0D97E876ad14DB2b183CFeEB8aa1A5C788eB1831", - "0x2325b0a607808dE42D918DB07F925FFcCfBb2968" - ] + "Contracts": ["0x2e59A20f205bB85a89C53f1936454680651E618e"] } diff --git a/example/contracts_full.json b/example/contracts_full.json new file mode 100644 index 0000000..ef2b537 --- /dev/null +++ b/example/contracts_full.json @@ -0,0 +1,17 @@ +{ + "Chain_Name": "mainnet", + "Project_Name": "Lido-governance", + "Contracts": [ + "0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc", + "0x2e59A20f205bB85a89C53f1936454680651E618e", + "0xf73a1260d222f447210581DDf212D915c09a3249", + "0xB9E5CBB9CA5b0d659238807E84D0176930753d86", + "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", + "0x9895f0f17cc1d1891b6f18ee0b483b6f221b37bb", + "0x0cb113890b04b49455dfe06554e2d784598a29c9", + "0x4ee3118e3858e8d7164a634825bfe0f73d99c792", + "0xF5Dc67E54FC96F993CD06073f71ca732C1E654B1", + "0x0D97E876ad14DB2b183CFeEB8aa1A5C788eB1831", + "0x2325b0a607808dE42D918DB07F925FFcCfBb2968" + ] +} diff --git a/example/run_scanner.py b/example/run_scanner.py index a070eda..b6dd249 100644 --- a/example/run_scanner.py +++ b/example/run_scanner.py @@ -1,44 +1,86 @@ import os import json +import logging from dotenv import load_dotenv from permission_scanner import ContractScanner, BlockExplorer - -load_dotenv() +from permission_scanner.utils.markdown_generator import generate_full_markdown def load_config_from_file(file_path: str) -> dict: - with open(file_path, "r") as file: - return json.load(file) + """Load configuration from a JSON file. + + Args: + file_path (str): Path to the configuration file + + Returns: + dict: Configuration data + + Raises: + FileNotFoundError: If the config file doesn't exist + json.JSONDecodeError: If the config file is not valid JSON + """ + try: + with open(file_path, "r") as file: + return json.load(file) + except FileNotFoundError: + raise + except json.JSONDecodeError as e: + raise def main(): - # load contracts from json - config_json = load_config_from_file("example/contracts.json") - contracts_addresses = config_json["Contracts"] - project_name = config_json["Project_Name"] - chain_name = config_json["Chain_Name"] - - # setup environment variables - api_key = os.getenv("ETHERSCAN_API_KEY") - rpc_url = os.getenv("RPC_URL") - - # initiate the BlockExplorer object - # specify and it will find the right base_url based on src/permission_scanner/utils/block_explorer_config.json - block_explorer = BlockExplorer(api_key, chain_name) - - # initiate the ContractScanner - contract_scanner = ContractScanner( - rpc_url, - block_explorer, - export_dir=f"results/{project_name}", - ) - - # Scan all contracts - for contract in contracts_addresses: - contract_scanner.scan_contract(contract) - - # Generate reports, save in result/{project_name}/reports - contract_scanner.generate_reports(project_name) + """Main function to run the contract scanner.""" + try: + # Load environment variables + load_dotenv() + + # Load contracts from json + config_json = load_config_from_file("example/contracts.json") + contracts_addresses = config_json["Contracts"] + project_name = config_json["Project_Name"] + chain_name = config_json["Chain_Name"] + + # Setup environment variables + api_key = os.getenv("ETHERSCAN_API_KEY") + rpc_url = os.getenv("RPC_URL") + + if not api_key or not rpc_url: + raise ValueError("Missing required environment variables") + + # Initiate the BlockExplorer object + block_explorer = BlockExplorer(api_key, chain_name) + + # Initialize scanner + export_dir = f"results/{project_name}" + + # Scan each contract + all_scan_results = {} + all_contract_data_for_markdown = [] + + for address in contracts_addresses: + # initiate scanner for each address + scanner = ContractScanner( + block_explorer=block_explorer, + rpc_url=rpc_url, + export_dir=export_dir, + address=address, + ) + final_result, contract_data_for_markdown = scanner.scan() + all_scan_results.update(final_result) + all_contract_data_for_markdown += contract_data_for_markdown + + json_path = os.path.join(export_dir, "permissions.json") + with open(json_path, "w") as f: + json.dump(all_scan_results, f, indent=4) + + markdown_path = os.path.join(export_dir, "markdown.md") + markdown_content = generate_full_markdown( + project_name, all_contract_data_for_markdown, all_scan_results + ) + with open(markdown_path, "w") as f: + f.write(markdown_content) + except Exception as e: + raise if __name__ == "__main__": diff --git a/src/permission_scanner/__pycache__/__init__.cpython-311.pyc b/src/permission_scanner/__pycache__/__init__.cpython-311.pyc index 596fa21b212e19e4ed94416545199027ffcbea64..b5a9b5ec48e39c9a3230edd1feef2a947c89e0d5 100644 GIT binary patch delta 20 acmeyt^n;0eIWI340}!aZl-kIh%Lo8GFa;a{ delta 20 acmeyt^n;0eIWI340}x1Um)OXi%Lo8Ev;^J& diff --git a/src/permission_scanner/scanner/__pycache__/scanner.cpython-311.pyc b/src/permission_scanner/scanner/__pycache__/scanner.cpython-311.pyc index 7883d7b25aad7a7227443a40eaeeae8799999b32..1e53fee014badbde351acc89c8f2e022e3cb6e1a 100644 GIT binary patch literal 23779 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

&-iJZgOBy+uGB0=~%ky{<&NQ z3-WSdd;p3$E4j+*rP%WD(zQFAa#bvdD}zU!o7|WA6$!Qj&WF1n%;q@qZvYSR-{jqg z`DN+eaN2oq_g$o90UN&#Im2#jn`Z!HMbTpF-YoxbymSvtUT~BctG2K>i58LRAP3{h zEL!@QL5uuvDf}M^kc>}v5mPFYP+zaIKB$cEQu!!_Lg2p=U<~q>JmZItrONR>DUOdX z<3q^!CWq2z8n+m>|DTi4`)i3tT za&@Zt%W}Rq*k6&OUgd1REZ45ue_5_ZHGk_|ShfGM+?ZB86a@{pvnyt d|FYb3s{NPqoU7omIyZ(-S@1#3mS7ccQbmapDT%sEhb>W-b)Swe`H~ge@*&$v)a~ZbEJ~(LQBIMv9Szl; zX=Kn!#xqg(co1#Iv(4FYG793|2)kLpyO;(%y-C_Xc4vx0EmSDLfPh7?$Ul0MEIbV? zuiO~!c;2Bn zilbxH6uwO{de$^$nl(?EXDw3}T1m4`*+?Gyl%2dCQx15WW6oLEl#ArC#ENE_DTes1 zG52ioR59_}VkNVlDbH-_ROxKlR2jtCIY-Pp>zncszcW@oTQOBZ{H|E#tbfW+Qzj}> z5vk&e-ZxRyhw!hsscI87Msds=6z48Tsp05JDp33dW}FC^Wy{HEA}L#rM8iqhJQ{y3 zK+E>YSTuPh!h_dwY(5@NM(5(_KRg#t@}Y1NN|mWu&P77pWO9xV%|vATx$EKa`1G9Y znTaHW2_-hjg_5B!uwj6~eJC~;zIOE1_1GLw&@+iha{hWSHa9av!WA=-c!UonBf;tU zSS&aj;;(UYH{%OF$dTwughO#i4#p!lJ0T*h^i_e38u;=MjnfpNrf80uGI8{j*-3G= zoH=6QEDn6_}vH!SZ1iVt<1Q%*>CMO=_x6tPt4@sPJ@ zOJ0U!B5uwdDUMhoCDW!{Zm!r^nk$JoI8VgGIk?hkhAVr+GF7U!VoO=5$qVUaoDXog z^Kh0I;H=oD-b(Bl)Zya%fWymGsbLFO4PhVjYHNKat_IS|xl&jlb#jR|-IL0&FN{t` z_#4r1gq@z_Suz*V_zatXNlLO|b!j9>;`InW8%-o&*(BIdoMY9s&?(#bNMb&gOn~Ql zI5^M8v{15G^HEgVk@M$#oaue2^FzH}*qFGr%%S}dH6D*9 zqoG)IA;Q8|VhhHy)8%4SH_Fc>_UIvuRELIbOCLZd*R$kGPsD23|ja~ zC>m$uk>t%e{+a=UaZK4Z!l02+7?rb!kNQUC-kz@#WvZ0#X zRB3HJ66GUd*!Zup$vHL=x)EU~=VDPVntY9&j>aO&Y6$bMC8ObBcy9JO#C1X))6tpE ztBJX|j(uXGCO3dw;phdNycec{_W_XY358h6#Tv!ofhI+Gz8q7lwH^>U6oi$l6BU`k zH5}Adq)j10vLy;6EMSo>iAZcZR}Vp#z?Fkj^xHq`x{!$QiLUq!posCAu9?fR(B(uI z7nzRk28@vqAHLF++XlN;($$sV!+Fs`JzCjf8inh;{+jFv2BWZsg24rU;o{L!s-ZQB za{ykavOdb+uw=Q-Nc9~{7ST~7Rn{oMCM8(i0>Kigousx(we=WX_1IDTcF|i!g0Cmz z=oKBkg7$m~SYd2K90$Eg$TlA6G#?M^YaLQ?r{KQ&@)&@3sAOJRQr~YBb=9yFjfatO z|G?fcEp#5f5(!^pqtk2%$Oey#Fbd0ty#afPV~^so6q%o8)rNI0ly=|xL1_2%=@^9IzoLq4iZ(LXocuAZbmpgu6ku4qCd)Vv)KLX@eqTc)QIhyjicT-U#K z?TrCXhxB#-!bst2M&1lM7F0SGR65qV7f#_&EU_OvTG2s3m1qUC51 z$DK&VLUzv1anb20kmZ1x@51PA05UzBz+DDZn~V8xxa zoAZ0@?E&oVFaTT#7F-A#*!1~vA#m><&v-rZR{q(~lL)~w|qdB36if3OY1_d$cT z?J)v=o-BgzbAuegae2lDDI6DdgVf)x|MnXM+wO(`FKYp)RK~eTr|7E?Rvb`Jmr5}} z3bn@PxJZKUgtYH-^`At1$dUoQEFVywbmMa9&Ws-1I^!0|zis0U4B-yl><_lH$D3Ir~b{Uzde2Lbk+=M($Mh6(B%5iWM=4sICMdYW8CDO zE4OuEQC20(<%i%=2Q~nVk-|LYt@uLb<_7gqc|KPTy8}rfaYim2_DVb9#9cY|$8f6u ze*5hMTDHVPvk`sFZ)_d&Bj1`w$zxZL;%M8i;YEQwt37G@o=x!sB;yI;$$;O97F?wLJw#!_``_wdpt8xSku>tr3==G1~xsVQb~={)HPb{9tUrx z;YAVpQG(CcR?6*JrtdHrrcPw)Hc2GJYkevM))NFqg%J&svyShF+T>D9h(#~+A^tTb zK49b9FvX228Z(=aT^J;!F(^BAzg&)9*jOOM!-#~`EOPU<(ZRWR9&?#M<`?)baEicH zzAf{)u;aPrzZb{uC=dqNUk=KBjs~7{#j)Y6`~#wY03tHXS&=!LHEEpHW>w=x zRsVWbf2L|stQyQ(A>j#fTVEk~Vz#-9v&EFhzjXCUiQQe5^-v|_G_WQsZDMr?yfeke z#p2^nN*(TTI$Ne4-I5b;EC4_SZnnr6lOLyq0S4L z&6Kku+euXn!cmc41du5o5X%RaToR$ZtJOehGrr@Z@A#54Yte{{RLK&G@dU}uR|o`0 zUDl>Asq%JcS(gi1)&_Cc%DqWQDQ8Op|ab7_c1S;5q&EWt! z_&Ps#>owUL1)1p9L>Q-6b58IeW%%*|K-ACWPTv`zPIpaIEj*oCSntM`7z084iVdd%7rBSI$>BZ^rPVdxlypP8Fv$-F>bg_)?6qEj0Da zobJM$$(kH0!&ny^a~ea?7V|UOQcw;wy{L+?r}K2gxf+eV+%jS%DH~_cY4g*0Ya@pP z_Jtluxl%=`;#3LexN9}Gn1)?+`@NKfcc-lST-j5$l!ZMMr_mzXpoyoc>bPd}!1{SL#Ptg={3gQK79DYtsn5s@B#MKwBJAD%DlM zs55qKJ@ie}I_>n$-Lcos2Z+wj&4Q6e_z zk5?tiReypA4*Ue<%xPD76mMl)0`6ubC@4jeJW5=$ITnfYFO#&$EvS=|Ekr3%vmNQx z+GPt}zYbZU<~pq8B7&C?AR_)11XBQH3n(_O6JbSZ5-P0ZQp6ven+|H*6NV*Up{L1Rw_BTWZCqyqQ5;$?Qw$S0buF460h?i561sf@jnzp zDAPJ70%VVi>~VnsxrAzI+i2dm-n{R@l}z)P*gPgM?B^wA%Oh|8`1X&}rRyb4LP^tm z31QcPOzT0h_29$N_12?8>ru($&)Re<$p*4k2s}Z$`4s|z>B)LDiXpLW8|>&hJDOn+ zi|k=SNzksl9XQnBd)+VOj<9Nbd41Wrhqp(mn6Cue3`o zZA-_N_hpzyfjpZ9VTJw@X#v(FI$hTL-*$@nS?SNp;PzKZA#i);9oS*ee@*?(7Mlqt zUOMX7x0nA?w2|+{bDqnyvU#f%!saTy4V*>cb{EYMZ_ULoS~%NVWl$e-8^on`>klmk z-*1+tXwH6jOIxGVq7`Ho8@%mFkRvq*7-1w7$2pAh=b|HtBtdmob=fVQC$Va&co}@+P@RpuLa?q)^p`WQk)0d!IhG}6^uVk z{S?dqmEIMM&GX?dBe=c43%53MFcNwnuj-p`ky$F8ra9jmjuf*+CU&RXiE_jGD}S>T z`cpwhx1c{irIQ1>zNg$=CFoEJM_JbZaDHfy9tz?m5pg(kHz=M3OENm0xAL`gke&;q ze$GNnAl!eNM|t8<8aZl19e4>(9PGirc+ORXyX{-8BEx8-MGA4C%W$VzHDGuxmBB1j z!7LOP&VrF1&{Xnpu3)M_$D&bK-fNh(>Nm?^)@uI7Y{d0bI%P@OxmwgZz*yP2I`qY@ z#9xp81(IF;4`KH$mMnUJnl9Ljx z-p@EO8*mS7Juy@ENoos~>w7LL0jnM6$D2d}RCTEWogdU#u;23OhqlK9jSK$`^#W{2 z*HwI@<>xoqJF13PnhoLkd|ir`hUSD&T@KATVvtr~Icd}rDRz!nOw&@S_LHEX(Udj1 zf@Pt8G6{yP#G(~TplU|T#Dy^VX=iQm$D zpk^e!aw$(St~$ZHGrIW(%}rCt{Mx8P&Bd9YQTV~?Lde})D7$vGY>ZyJp& zes(S#p67XJ$IUCy993P1!?j@A!y42MxFzt2Bx;AhqE*F1iBa+Jrb%JZK5~;=Tu;8* zK~uocVcD(CK@cwyRUORhd4@ba+O*(m1mo~d(rK;Kb~>v615G2B+fU)5zW%iGq|dIs z`tgm2b?ZYD!qCLZs{oXTaK8)1S}iO#gI%a#<8w(^BlBR~41=dl+ZV{w9s7d}2LA@2 zmc7RH^8F18d|{xdXJAm^QW7kbbg)@l2da#_gTjvC^_pR!2CloPs_ISRg^)5pzJg_` z43jH2Omc-WM{ra%Ep?NfqzCP4{6g(vuwRQsK-)lezHvh?1TMtE`WS4DA(^aIby?!< z#|@UtfdG%EKVORg`7*hZm^Y(ZgQ1XgwxjGk( zE8EL48@d(&3-tt#q+WJi48`UnM{&!@X4s>$^XRQ`SP;O#KTpsY$aWiowyYhv`($H2_7W~9vMWLnFph7em)GQxuBD=OvAOD zT%uV)t7}p&)k_A!kTH5A!trs$FpC(Rk@);9`QU=Tg^_azVwmD4`cUmF4j%1V_>%wv zUPWiqh?z<>+^Q8dQQgQD1&dT|3DNM#UZAq52ho}d`qzY9gnFLfOlV2;+pKRA z>w7lp$Jgt}AGK!c&x!TtmQG8xyH+Q}+835iNtHX(H^s`XwVw6L{-xv6j(%a_sJP>p zz%)rsEdtX3dKGu6$n4r+de@m=scPrikl=X%o&v=Ms3t1>OUIN#Qg!Xp2~cJD8&;Bc zQ>!m$DhI{N!KLF6Ra<{|=$Cun-MebJU-F2P|Z-~?u)cg+pBoM?tG)Nd%dz-=o!mY9v3T*gR-Ht;&FA0KAU2MY(u=gz(Jgiy*ywm^z2hax-wAnkW1ig=f-eA7?K((rv>#q?Km2e) zIDawI{*u@ZL)r={{#~FCDX*6NwW5D$!@qyszkjps1!>o4)@rV+hx+U4pO~#x)o|xu zRSn9Ks_Lgtzh)?ZV-{3k7wLTXxEd5$=Y_N95VoGrR9_IQF9^O1Pr+r)m>WF~x4YtT z!%pa{SlKRC4hX(ccpiX#6uzZ~UDD2Wap#D*a}-Sf*fO|)XUpIMo-JFp++miPr(e4$ zU-g~6qBj6!){V4Q1&=)y?=Zh$B;N&GE?BW}Zc_2UaFhZP05D_#Y*c9lw(7CB7PVG5 zkSb7-2z6&3wgQ0X(OKo4@thSsX9dq$$y1JHvsNgBZx>Zncb5_UouEF{jzl;ZR|5bt z^{2)9(@Uo}y-hHb?y|=ftmK9#{)Ly!@yw(=;hI zO)eb~nFgt=pS(q;L)x?NVdaC)2c63^D>J{0y&GHY%hY#?^<8U?nfgAVegGa3>>(-! zAI&U{NxdU#!7g>t`Q1IA8)o2);5#D>PayQ2`5jXL{q5+!zgOHfE_m7`Ps4_1=elQS z)?_&M$O*XFI07~l#8SUF})-b_m`Mvf3wv zu0w)cmlZf>0 zjHml^rtIw#Z=DeQ$JQ1Bz>{IdL$)a$x#ROshx(vrw-TA zfcew1vZF2LPg|_$51282clWV+>a#k`^I5&`xXt`oZ{wKP{2Q+u{J&uvkJIKyv=#i1 zY{&W`(?7l7Iz4RuXKUx_KJ!2KS;5bL09EpThu|In*`u8)+Q%bdUGRitWFpM)|K(S} z5rat#brk}N%Uw_sLzq8Xgl`XW3MLZw6A(acSFt>CDHA9w8Vxv*&n7ONU#k{LrK~C2 z^cEE+;6x^bxMe!=V#>_Xx{d~nQ*wUNialVWlivz^zMAz0m>=-19EkQeXdV=!AlO^> z0%j8xlw$^+~kevbG9Fpymt%KDxln02SosKwyFbl;RvXH z{~#^kTw4|5ub=^nPg4p!Praa6&g*-$K(S8@TJ{3YKY{;0H_XAdW-{+XzdzJEPtkBQ zVJMTQc-J!-a$GId;*G13rsCaOMz(p;m2A=SQ&c){mF8kWz3kdfX+u=7t{=N{3TC)O zD9Uf`Rb6SE*43vuywq01w4+2R#=wd*?BPW>n7dVICBbyanWG|Pce;(c{1OX$JTDK- zV%}8>P@NJ3K0{u_cf0ZSu~ZRnhnn16F~)H2N%9{k30(xa@*G^dYM%h(cYO2k9VjX| z@{MM`(SE3avsaCGG&Vfnf=8pr2~=ckXHm3`^zkfPAo5Xro;46)#QR=dV}vq7p4{>s ztdz7?Yp2LJX~OjC`6!Gue+fYZ0E#=a^GU^+8NRtj@fI$&Vn|G}7B0>I;Vkx(B$v;oi%a z!(XJa9=rh|N-a>(prH(WBO9Ab%2qO`n$qjvVp@aHs6IB?n$08LfyTU}*21S%DQ z)};&O2{6(wFhZTBH^O0b3E|`1`3?u#$Gx|_QEv!N=z8Q*L3u31g0hQe7lc3TM zMbof?2%NcGR`PuY{~sVaP^?JLMm~c7w~$0cQrQgsk=>fr0r_ZIcJiT{N*f6deid_n zjFp%{v6rVJqJJ<;wKthl9``n$tm0X=X+Y&|K|oXpgm zlp01J)(H(0@JJ^o(r%&ic!nJZF`%*K9#~G-)ir(I(z_d%(xLu`9NY&#;hjZ1CAkK1;w9=<=3!KfGV(q4SxwD#--L9u6NdOXWe zyL*U8dSSieg4EWt*7tyZFanI}ZpZ>4eH5CpqgUuXDYl&2XgR;$a(=Ua=;Py$`-dKw z|Dib3KPL8%NdtR83Jid)1^|#Ac0gGGAU&wy>DR?nTMuyAjS#5;sih|$t<=;1vH#P) zhlx*qoH=|^JbY1TdkG$B3Zer5;-3n@oB(`gK%_`^jh!%JB0Ijpo>^zlNIQ0|PTqf6 zXc>gEjZILt5yUzG|MD+SPS7x=+4FFWy5S4*4M+F7qg($j9Hb*>nZ$TNjN4#3*O|`M zq}X*x>^hfWCPijaAkWjs$mWAuBL`GAU#J>d8wCJQh8Y%_VSyQzm{P*fTcLo`R!~b6 zOV#ya^=`3xXrp@HdiB0c^#QT^0F0J%F9C19xO8~AT&itdb&Iuwpdbl!-mh7jSQ)sx zC&TRAZ0ub9W3llCsNW6w0W7;Eum8@Tbe+(3RBQxG2z;dOfsc!pPo;O^KQ$lrn2CO*91CqaUA4y8=f^GEcibLf`I&7bG95~wL{Irt>_*s9; zSQYh~Di?|p&Got{Av0itbrtlKMBNwG?~nltvf)HLcmT0R3yPe0kj^gw1r!HtgDx0w zwDAUg(Ukvfan8~KEhg@pjd|gYPeTds-&v~(Sbe-dvcmlywCh8?ZHwk4(pc32J5q3~ zKR_*7^6e#Z;sNx;p_hXj{rd$sxnOAxj4$Uj)(7laQ8U#;B`h~h%56POy#nkX^ruY2 z1`L`mF682XIdP@T+c9X#ynA(WpR0=o`E4}bGcQ`d)%X={89$q0{NVR#jQz#&OIe;F zhHSA3|K3N8N!hj=G0q8=wC;ts$JF2KVPBj)JwZ&vwcqUlh6#Vn1=u})ZdOs_Kaah5 z{+7{rA-r`%8*OA7)pYi9M7yqwM{;}_{>Tie^*oQpwy*Wu#z7dk?EUXp!f`5 zgWB+8mUwbvp;GGyr*uTY0(W5M&RK4uCYO3j{Z%5hcY9bk)&Crj@qdBfZ3M``^UDBa z%N$rb!rcMWG5mC;$4LD zVY^2gX2LZ=A7r;4gSiqYgsWVDqRECIW>SA%NyWQR_|qz2HO*1#<}nKD9@AB}oRoN26=I6-9K$ zaY%F=60|3Q!s|bF*Bz=d|GLU@sLuXtmInBVr{YMj`IBDDkwN<>BQ!w61tv_ra!jEx zZPa3+A^pOuV{j==L^-@3jwheB&7b?iJ_Hxdv-5asuHO8qBtw^c;Fnx*55Wt~cJhDF zMfu?|y!BSgY`4DQ?kO+F2e`i7fCqS927bszEMWx)%d<)ac$x$%@V9vILNgeYT|u-Q zo{yoQ2?k%C55?3Jdoak&g@ZvwnNQL13LxpRZe;5!zX6Q z_QnsDjKD`>nHFo`sv}FmYwi4l=8s}T)mX*%IQVw!paQw8>hQp3GACUOm6o zyp~vNyZ<94ybZi;IYnFdZC2H-T!G&wuwnpOR^OCvzWYknPJ(c!R=v`^l2~cG^P{Yj zM7gM%`fL&LGT0n9@lsphk4%-;!}KaN2j6SwmG?HV*Jf=@x191FoO^}!u?qEnMsmCqQyA)}`0Aj@5D)i5UEYpO&iDhafEVM9Q<@V!zU#Et7oK= H_4$7RJqS~a diff --git a/src/permission_scanner/scanner/scanner.py b/src/permission_scanner/scanner/scanner.py index 1243e43..521e9f5 100644 --- a/src/permission_scanner/scanner/scanner.py +++ b/src/permission_scanner/scanner/scanner.py @@ -1,10 +1,13 @@ import json -from typing import List, Dict, Any +from typing import List, Dict, Any, Optional, Union import urllib.error import os import re +from enum import Enum +from dataclasses import dataclass +import subprocess -from slither.slither import Slither +from slither import Slither from slither.core.declarations.function import Function from slither.core.declarations.contract import Contract from slither.tools.read_storage.read_storage import ( @@ -14,17 +17,19 @@ ) from ..utils.block_explorer import BlockExplorer -from ..utils.logger import setup_logger from ..utils.markdown_generator import generate_full_markdown -logger = setup_logger(__name__, "logs/scanner_new.log") - class ContractScanner: """Service for scanning smart contracts for permissions and storage.""" def __init__( - self, rpc_url: str, block_explorer: BlockExplorer, export_dir: str = "results" + self, + # block_explorer: BlockExplorer, + block_explorer_api_key: str, + rpc_url: str, + address: str, + export_dir: str = "results", ): """Initialize the ContractScanner. @@ -33,22 +38,24 @@ def __init__( block_explorer (BlockExplorer): The block explorer instance for fetching contract metadata export_dir (str): Directory to save Solidity files and crytic_compile.config.json """ + # self.block_explorer = block_explorer + self.block_explorer_api_key = block_explorer_api_key self.rpc_url = rpc_url - self.block_explorer = block_explorer - self.slither = None - self.storage_reader = None + self.address = address self.export_dir = export_dir + self.permissions_results = {} + self.target_storage_vars = [] self.contract_data_for_markdown = [] - self.scan_results = {} - logger.info("Initialized ContractScanner") + self.scan_result = {} + self.implementation_name = None @staticmethod - def is_valid_eth_address(address: str) -> bool: + def _is_valid_eth_address(address: str) -> bool: """Check if a string is a valid Ethereum address.""" return bool(re.fullmatch(r"0x[a-fA-F0-9]{40}", address)) @staticmethod - def get_msg_sender_checks(function: Function) -> List[str]: + def _get_msg_sender_checks(function: Function) -> List[str]: """Get all msg.sender checks in a function and its internal calls.""" all_functions = ( [f for f in function.all_internal_calls() if isinstance(f, Function)] @@ -86,24 +93,81 @@ def get_msg_sender_checks(function: Function) -> List[str]: ] return all_conditional_nodes_on_msg_sender - def get_permissions( - self, - contract: Contract, - result: Dict[str, Any], - all_state_variables_read: List[str], - is_proxy: bool, - index: int, - ) -> None: + def _extract_solidity_version( + self, contract_path: str, default_version: str = "0.7.6" + ) -> str: + """Extract Solidity version from contract's pragma statement. + + Args: + contract_path (str): Path to the contract file + + Returns: + str: Solidity version (e.g. "0.4.24") + """ + try: + with open(contract_path, "r") as f: + content = f.read() + # Match pragma solidity statements like: + # pragma solidity ^0.4.24; + # pragma solidity 0.4.24; + # pragma solidity >=0.4.24 <0.5.0; + match = re.search( + r"pragma\s+solidity\s+(\^?[0-9]+\.[0-9]+\.[0-9]+)", content + ) + if match: + return match.group(1).replace("^", "") + return default_version # Default version if not found + except Exception as e: + return default_version # Default version on error + + def _switch_solidity_version(self, version: str) -> None: + """Switch to the specified Solidity version using solc-select. + + Args: + version (str): Solidity version to switch to + """ + try: + current_version_info = subprocess.run( + ["solc", "--version"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + if version in current_version_info.stdout: + return + subprocess.run(["solc-select", "use", version], check=True) + except subprocess.CalledProcessError as e: + raise f"Failed to switch Solidity version to {version}: {e}" + + def _get_contract(self, address: str) -> Contract: + """Get a contract from the block explorer and initialize Slither. + + Args: + address (str): The contract address to fetch + + Returns: + tuple: (contract_metadata, main_contract_path) + """ + contract_metadata = self.block_explorer.get_contract_metadata(address) + # contract_save_dir = os.path.join( + # self.export_dir, contract_metadata["ContractName"] + # ) + # os.makedirs(contract_save_dir, exist_ok=True) + main_contract_path = self.block_explorer.save_sourcecode( + address, self.export_dir + ) + return contract_metadata, main_contract_path + + def _scan_permissions(self, contract: Contract) -> Dict[str, Any]: """Analyze permissions in a contract and store results. Args: contract (Contract): The contract to analyze - result (Dict[str, Any]): Dictionary to store results all_state_variables_read (List[str]): List of state variables read is_proxy (bool): Whether the contract is a proxy index (int): Index for proxy/implementation contract """ - temp = {"Contract_Name": contract.name, "Functions": []} + result_dict = {"Contract_Name": contract.name, "Functions": []} for function in contract.functions: # Get all modifiers @@ -118,7 +182,7 @@ def get_permissions( list_of_modifiers = sorted([m.name for m in set(modifiers)]) # Get msg.sender conditions - msg_sender_condition = self.get_msg_sender_checks(function) + msg_sender_condition = self._get_msg_sender_checks(function) if len(modifiers) == 0 and len(msg_sender_condition) == 0: continue @@ -147,7 +211,7 @@ def get_permissions( set(all_state_variables_read_this_func) ) - all_state_variables_read.extend(all_state_variables_read_this_func) + self.target_storage_vars.extend(all_state_variables_read_this_func) # Get state variables written state_variables_written = [ @@ -155,7 +219,7 @@ def get_permissions( ] # Store results - temp["Functions"].append( + result_dict["Functions"].append( { "Function": function.name, "Modifiers": list_of_modifiers, @@ -165,186 +229,46 @@ def get_permissions( } ) - # Store in result dict - if is_proxy and index == 0: - result["proxy_permissions"] = temp - elif is_proxy and index == 1: - result["permissions"] = temp - else: - result["permissions"] = temp + return result_dict - def scan_contract(self, address: str) -> Dict[str, Any]: - """Scan a contract for permissions and storage. - - Args: - address (str): The contract address to scan - - Returns: - Dict[str, Any]: The scan results for this contract - """ - logger.info(f"Starting scan for contract at {address}") - - try: - # Fetch contract metadata - contract_result = self.block_explorer.fetch_contract_metadata(address) - contract_name = contract_result["ContractName"] - is_proxy = contract_result["Proxy"] == 1 - implementation_address = contract_result["Implementation"] - implementation_name = "" - - # Add contract to markdown data - self.contract_data_for_markdown.append( - {"name": contract_name, "address": address} - ) - - result = {} - target_storage_vars = [] - temp_global = {} - - # Setup RPC info - rpc_info = RpcInfo(self.rpc_url, "latest") - - # Create etherscan-contracts directory for this contract - contract_dir = os.path.join(self.export_dir, contract_name) - os.makedirs(contract_dir, exist_ok=True) - - # Handle proxy contracts - if is_proxy and implementation_address: - if not self.is_valid_eth_address(implementation_address): - raise ValueError( - f"Invalid implementation address for proxy: {implementation_address}" - ) - - try: - implementation_result = self.block_explorer.fetch_contract_metadata( - implementation_address - ) - implementation_name = implementation_result.get("ContractName", "") - - # Add implementation contract to markdown data - if implementation_name: - self.contract_data_for_markdown.append( - { - "name": implementation_name, - "address": implementation_address, - } - ) - except Exception as e: - raise RuntimeError(f"Failed to get Implementation contract: {e}") - - # Initialize Slither with export directory - try: - self.slither = Slither( - f"{self.block_explorer.chain_name}:{address}", - export_dir=contract_dir, - etherscan_api_key=self.block_explorer.api_key, - ) - except urllib.error.HTTPError as e: - logger.error( - f"Failed to compile contract at {address} due to HTTP error: {e}" - ) - raise - except Exception as e: - logger.error(f"An error occurred while analyzing {address}: {e}") - raise - - # Get target contract - contracts = self.slither.contracts - target_contract = [c for c in contracts if c.name == contract_name] - - if not target_contract: - raise ValueError( - f"Contract name {contract_name} not found at address {address}" - ) - - # Initialize storage reader - self.storage_reader = SlitherReadStorage(target_contract, 10, rpc_info) - self.storage_reader.unstructured = False - address = address[address.find(":") + 1 :] if ":" in address else address - self.storage_reader.storage_address = address - - # Handle proxy implementation - if is_proxy: - # Use the same directory for implementation contract - self.slither = Slither( - f"{self.block_explorer.chain_name}:{implementation_address}", - export_dir=contract_dir, - etherscan_api_key=self.block_explorer.api_key, - ) - implementation_contracts = self.slither.contracts_derived - target_contract.extend( - [ - c - for c in implementation_contracts - if c.name == implementation_name - ] - ) - - if len(target_contract) == 1: - raise ValueError( - f"Implementation name {implementation_name} not found" - ) - - temp_global["Implementation_Address"] = implementation_address - temp_global["Proxy_Address"] = address - else: - temp_global["Address"] = address - - # Analyze permissions - for i, contract in enumerate(target_contract): - self.get_permissions( - contract, temp_global, target_storage_vars, is_proxy, i - ) - - target_storage_vars = list(set(target_storage_vars)) - - # Read storage - self._read_storage( - target_contract, target_storage_vars, temp_global, address - ) - - # Store results - if implementation_name: - self.scan_results[implementation_name] = temp_global - else: - self.scan_results[contract_name] = temp_global - - logger.info(f"Completed scan for contract {contract_name}") - return temp_global - - except Exception as e: - logger.error( - f"Unexpected error while scanning contract {address}: {str(e)}" - ) - raise - - def _read_storage( + def _scan_storage( self, - target_contract: List[Contract], - target_storage_vars: List[str], - temp_global: Dict[str, Any], + storage_scanner: SlitherReadStorage, + permissions_result: Dict[str, Any], contract_address: str, - ) -> None: - """Read storage values for the contract. + ) -> Dict[str, Any]: + """Scan contract storage. Args: - target_contract (List[Contract]): List of contracts to analyze - target_storage_vars (List[str]): List of storage variables to read - temp_global (Dict[str, Any]): Dictionary to store results - contract_address (str): The contract address + storage_scanner (SlitherReadStorage): Initialized storage scanner + permissions_result (Dict[str, Any]): Results from permission scan + contract_address (str): Contract address to scan + + Returns: + Dict[str, Any]: Storage analysis results """ - # Set target variables - for contract in self.storage_reader._contracts: + # sets target variables + # adapted logic, extracted from method `get_all_storage_variables` of SlitherReadStorage class + for contract in storage_scanner._contracts: for var in contract.state_variables_ordered: - if var.name in target_storage_vars: - self.storage_reader._target_variables.append((contract, var)) + if var.name in self.target_storage_vars: + # achieve step 1. + storage_scanner._target_variables.append((contract, var)) + # add all constant and immutable variable to a list to do the required look-up if not var.is_stored: - for function_data in temp_global["permissions"]["Functions"]: - if var.name in function_data["state_variables_read"]: - if "immutables_and_constants" not in function_data: - function_data["immutables_and_constants"] = [] + # functionData is a dict + for functionData in permissions_result["Functions"]: + # check if e.g storage variable owner is part of this function + if var.name in functionData["state_variables_read"]: + # check if already added some constants/immutables + + # Ensure key exists + if "immutables_and_constants" not in functionData: + functionData["immutables_and_constants"] = [] + + # Check if the variable has an expression and is not the proxy marker if ( var.expression and str(var.expression) @@ -352,15 +276,15 @@ def _read_storage( ): try: raw_value = get_storage_data( - self.storage_reader.rpc_info.web3, + storage_scanner.rpc_info.web3, contract_address, str(var.expression), - self.storage_reader.rpc_info.block, + storage_scanner.rpc_info.block, ) - value = self.storage_reader.convert_value_to_type( + value = storage_scanner.convert_value_to_type( raw_value, 160, 0, "address" ) - function_data["immutables_and_constants"].append( + functionData["immutables_and_constants"].append( { "name": var.name, "slot": str(var.expression), @@ -368,36 +292,172 @@ def _read_storage( } ) except Exception: - function_data["immutables_and_constants"].append( + functionData["immutables_and_constants"].append( {"name": var.name, "slot": str(var.expression)} ) else: - function_data["immutables_and_constants"].append( + functionData["immutables_and_constants"].append( {"name": var.name} ) - # Compute storage keys and get values - self.storage_reader.get_target_variables() + # step 2. computes storage keys for target variables + storage_scanner.get_target_variables() + + # step 3. get the values of the target variables and their slots try: - self.storage_reader.walk_slot_info(self.storage_reader.get_slot_values) + storage_scanner.walk_slot_info(storage_scanner.get_slot_values) + except urllib.error.HTTPError as e: + print( + f"\033[33mFailed to fetch storage from contract at {contract_address} due to HTTP error: {e}\033[0m" + ) except Exception as e: - logger.error(f"Failed to read storage: {e}") - raise + print( + f"\033[33mAn error occurred while fetching storage slots from contract {contract_address}: {e}\033[0m" + ) + + storageValues = {} + # merge storage retrieval with contracts + for key, value in storage_scanner.slot_info.items(): + contractDict = permissions_result + storageValues[value.name] = value.value + # contractDict["Functions"] is a list, functionData a dict + for functionData in contractDict["Functions"]: + # check if e.g storage variable owner is part of this function + if value.name in functionData["state_variables_read"]: + # if so, add a key value pair to the functionData object, to improve readability of report + functionData[value.name] = value.value + + return storageValues + + def _check_proxy(self, contract_metadata: Dict[str, Any]): + """Handle proxy contract logic. + + Args: + contract_metadata (Dict[str, Any]): Metadata of the contract + + Returns: + tuple: (isProxy, implementation_address) + """ + isProxy = contract_metadata["Proxy"] == 1 + implementation_address = contract_metadata["Implementation"] + implementation_name = None + + if isProxy and implementation_address: + if not isinstance( + implementation_address, str + ) or not self._is_valid_eth_address(implementation_address): + raise ValueError( + f"Invalid implementation address for proxy: {implementation_address}" + ) + try: + implementation_result = self.block_explorer.get_contract_metadata( + implementation_address + ) + implementation_name = implementation_result.get("ContractName", None) + self.implementation_name = implementation_name + self.contract_data_for_markdown.append( + {"name": implementation_name, "address": implementation_address} + ) + except Exception as e: + raise f"Failed to get Implementation contract from Etherscan. \n\n\n + {e}" + + def scan(self) -> Dict[str, Any]: + """Scan a contract for permissions and storage. + + Args: + contract_address (str): The contract address to scan + + Returns: + Dict[str, Any]: The scan results containing permissions and storage analysis + """ + final_scan_result = {} + contract_metadata, main_contract_path = self._get_contract(self.address) + contract_name = contract_metadata["ContractName"] + isProxy = contract_metadata["Proxy"] == 1 + + self.contract_name = contract_name + self.contract_data_for_markdown.append( + {"name": contract_name, "address": self.address} + ) + self._check_proxy(contract_metadata) + + # Initialize scan_result structure + self.scan_result[contract_name] = {} + + # Extract and switch to the correct Solidity version + solidity_version = self._extract_solidity_version(main_contract_path) + self._switch_solidity_version(solidity_version) + + slither = Slither(main_contract_path) + + # Get target contract from slither + target_contract = [c for c in slither.contracts if c.name == contract_name] + if not target_contract: + raise ValueError(f"Contract {contract_name} not found in source code") + + # Initialize storage scanner + rpc_info = RpcInfo(self.rpc_url, "latest") + srs = SlitherReadStorage(target_contract, max_depth=5, rpc_info=rpc_info) + srs.unstructured = False + srs.storage_address = self.address + + # If proxy, scan implementation + if isProxy: + impl_address = contract_metadata["Implementation"] + _, impl_path = self._get_contract(impl_address) + + # Extract and switch to implementation contract's Solidity version + impl_solidity_version = self._extract_solidity_version(impl_path) + self._switch_solidity_version(impl_solidity_version) - # Store storage values - storage_values = {} - for key, value in self.storage_reader.slot_info.items(): - contract_dict = temp_global["permissions"] - storage_values[value.name] = value.value + impl_slither = Slither(impl_path) - for function_data in contract_dict["Functions"]: - if value.name in function_data["state_variables_read"]: - function_data[value.name] = value.value + # Get implementation contract + impl_contracts = impl_slither.contracts_derived - if storage_values: - contract_dict["storage_values"] = storage_values + # find the instantiated/main implementation contract + target_contract.extend( + [ + contract + for contract in impl_contracts + if contract.name == self.implementation_name + ] + ) + if len(target_contract) == 1: + raise Exception( + f"\033[31m\n \nThe implementation name supplied in contract.json does not match any of the found implementation contract names for this address: {self.address}\033[0m" + ) + self.scan_result["Implementation_Address"] = impl_address + self.scan_result["Proxy_Address"] = self.address + if not isProxy: + self.scan_result["Address"] = self.address + + for i, contract in enumerate(target_contract): + # get permissions and store inside target_storage_vars + _scan_permissions_result = self._scan_permissions(contract) + if isProxy and i == 0: + self.scan_result["proxy_permissions"] = _scan_permissions_result + else: + self.scan_result["permissions"] = _scan_permissions_result + + self.target_storage_vars = list( + set(self.target_storage_vars) + ) # remove duplicates + + # Scan storage + permissions_result = self.scan_result["permissions"] + storage_result = self._scan_storage(srs, permissions_result, self.address) + if len(storage_result.values()): + self.scan_result["storage_values"] = storage_result + + if self.implementation_name: + final_scan_result[self.implementation_name] = self.scan_result + else: + final_scan_result[self.contract_name] = self.scan_result - def generate_reports(self, project_name: str) -> None: + return final_scan_result, self.contract_data_for_markdown + + def generate_reports(self) -> None: """Generate JSON and markdown reports from scan results. Args: @@ -408,24 +468,20 @@ def generate_reports(self, project_name: str) -> None: os.makedirs(reports_dir, exist_ok=True) # Save JSON report - json_path = os.path.join(reports_dir, f"permissions_{project_name}.json") + json_path = os.path.join(reports_dir, "permissions.json") + final_result = {} + if self.implementation_name: + final_result[self.implementation_name] = self.scan_result + else: + final_result[self.contract_name] = self.scan_result + with open(json_path, "w") as f: - json.dump(self.scan_results, f, indent=4) - logger.info(f"Generated JSON report: {json_path}") + json.dump(final_result, f, indent=4) # Generate and save markdown report markdown_content = generate_full_markdown( - project_name, self.contract_data_for_markdown, self.scan_results + self.contract_name, self.contract_data_for_markdown, final_result ) - markdown_path = os.path.join(reports_dir, f"permissions_{project_name}.md") + markdown_path = os.path.join(reports_dir, f"markdown.md") with open(markdown_path, "w") as f: f.write(markdown_content) - logger.info(f"Generated Markdown report: {markdown_path}") - - def get_scan_results(self) -> Dict[str, Any]: - """Get the current scan results. - - Returns: - Dict[str, Any]: The accumulated scan results - """ - return self.scan_results diff --git a/src/permission_scanner/utils/__pycache__/block_explorer.cpython-311.pyc b/src/permission_scanner/utils/__pycache__/block_explorer.cpython-311.pyc index 460a3e980d66d9e76e32335114e8b032002867da..095c9960c4e6aab89d0d103d6d0acbd5a5b303e5 100644 GIT binary patch literal 11425 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 delta 2321 zcmZ`)UrbY17(ci7|81#IC`BlxPAE1YXets>r)6`B5ySpuMjl2g=N1a3t><2XlCt{X zHnNy(=D4_IZo_1AE^0zzmN2&m@oDp60n&`-E=$bbmc7*IUY5n|JGU0oOb_?`&e!id z-#O>|e&0D15UiqtSgTnI4@S(a&U(X`+)Z(9sCj zsg+OmC`A+8mN!&{tFWXCSca}ZS9M93H?5Qda4Nc3uLEiE{z=&vpH^kB1_7JP2{ zHSc$r)il8eeOO|1KX+6ngrmRk?eqyBIWgNdkV=?|STZrI<6u7v^U%{MgYh&8j*iC? zslH%Xv$VHk$!Yx%A!$Nf5E5a*TXhwg85vEd;)yY`iB1ZQeyNl~OGu>R=`e2zhMtT+ z^`&15m!uk4!T5ph5j*Qe)&0AF$6{mNzi(+c?>{pC>V?7iL3&v{8Tng7>ZXsJA39g+ zjw~GofNxb9$SVUY<sP+y29$yO+&EhMuR1o|C4T~SsO~Qh(oQ4@Qrw!m7>!g~lY2kjA_}Ds?E0SiW z+rnI686%TU8M-Clm>IJ;-B3XvEH~0Xd!608T0mzE0#D~qF@${0g?fL%7bw&>UP=~K z(W4a+z`Rs+Ay;h?aq6~0u>JNI&UP;KNmKLyTI`KR_~mvNh|3Wn+AA;j@=WgIn7r4P z4`}^C{+{aTZ{qJYi6D<|kPDkj&>3Zqo&a1%CSbV%_3WLn8y)i1D;W+u<{ViBX>I7l z0xPVW;x!=u#zq0m@mU^A&$yWrvcdvyQ~q9^hcC;w>T0z~`xqPMTP-yTc3$D(EpXBM zb?=LIGU?AXO*!GYX=d)0>y>oMB(YKRcx+0y#37PCbC$F+i+f-ylhmj56i}B)r@}nx z0X5mppcf!45Oyi;yG?d7X&cjuC0<#5l-LZR9IRFa*WKP{gxzpM1hdT~X#mOc#dULZ zoIMGpIa9hBW311%RXj6YFPL^1)-WytoI~&e)V42}U*+0j|2&E3WGa-qh% zUbA(rW^19gc_FC!|^HmT0;dTG^HUIYOsa5~} zynlbe`_j5MwB`*h%&dC5^WN^o$eMR|A<(fN=w1tSuk1Lu8i?it(V{F=L)Dc))gHmA zc=%UAjqr|~#TzCej9kJ9xA+9a%Zi7ve3X*72c&Ni>0=6(8^W%n4Q>#O?*Yzj{1t?r z6``jnxWuE}0$)UIT!cDjH;M>`MbjQ$069@B#l=Djdq_JBmSmpIBvNCg zG_b6ca?!zTawcYuCljX#yJW=6Kw+?#8HIF4Pg%;yNIa3$M@B3u6C+Tp78kdKWIBdR z)oEuh%AEQT)TEu#f0TVwX*`|OUnT=!VRzK%qSGD6T7xU7;(MTa=lvg#uLSq3s(pF2 s?;Ml}z2EV{>xVfGNPa#(wDpf)U^P6j-Lg^>ZR3~QglJe^?&JXf13XhCQvd(} diff --git a/src/permission_scanner/utils/__pycache__/logger.cpython-311.pyc b/src/permission_scanner/utils/__pycache__/logger.cpython-311.pyc index 6e8caeebbbcbc1aec801631d67d119372973be05..0e94f33b1569727ae00dae05dbb839f0e1768fea 100644 GIT binary patch delta 584 zcmaDPxKW6AIWI340}zCVN@v{M$Sc9fs54oOQC}pB2_y;vMNBoU!3;I53z#P>GKo&k zVHEUBVXR?|XMspFFqAODSPTqVtT1*8(=wp=YPei28xD2MllL;pGO|s+%_z;t4z_}a zNraID$kG-Uf|&+18SG3De*rgGRRxn+urQK54~$*InZmM$bs5kDtKpttWGIn_$ult2 zFxD{Fu&iNQ#tc*g#1K%+Rl~J_eey#lO|b2vlNFf77}+~i#;bl zJv}oo{T63>YKae!omzB@BO@^{B`3A0_!dVoP%yPDHRl#*K~a7|VtQgp>gFg`Cq^F? zpcU*uT&xWYkOqdY8Vr1*S9s-Slw9Ohxx%Z`;P!!yfrGE5{sRLetJn>G(GK1REL`pW zP5u*HFS1BoVUfDPBK1IArortBi^$~jY(E(#C!b_@kO3LHlA%ZyNEImqiC-Kxx%nxj zIjMFqLb?w1Ek+y`C| L8)O7F0&Ft?KzL_& diff --git a/src/permission_scanner/utils/__pycache__/markdown_generator.cpython-311.pyc b/src/permission_scanner/utils/__pycache__/markdown_generator.cpython-311.pyc index 19c0f46b1408685f42800018c63d2f2d9a595631..e0b3afe7e9fedd2e4a8a00ca7c87f5e536b01e39 100644 GIT binary patch literal 2416 zcmd5-O>EOv9Dn{wY&R)MX+zSFUAz^9d;NW2ae9nuAX;)NT*;X z-ln8d?BGFZu2|4;I-{wg-jbz-bY78kX;zl8qN3TBJ#2-^Q{o_-jXEkX5(L~3RdHVZ(x;$ z+wt3rx%8qMmzAaL1guR-uAu_EhVC-g&>Cr6LFb?~MTqKYM}fts z^ML+G-bRC-ECk-E&QPeU8YB0%Yy7|8=Kf2UaC

&(btd?oz8=J2|II=T-|D4VoC} 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 C$b)DA diff --git a/src/permission_scanner/utils/block_explorer.py b/src/permission_scanner/utils/block_explorer.py index 5765806..7a9ea9e 100644 --- a/src/permission_scanner/utils/block_explorer.py +++ b/src/permission_scanner/utils/block_explorer.py @@ -2,9 +2,9 @@ from typing import Dict, Optional, Any import json from pathlib import Path -from .logger import setup_logger +import os +from json import JSONDecodeError -logger = setup_logger(__name__, "logs/block_explorer.log") # Read the config file from the same directory as this script with open(Path(__file__).parent / "block_explorer_config.json", "r") as f: @@ -21,16 +21,14 @@ def __init__(self, api_key: str, chain_name: str): self.chain_name = chain_name try: self.base_url = block_explore_config[chain_name]["base_url"] + self.chainid = block_explore_config[chain_name].get("chainid", None) except KeyError: raise ValueError( f"Unsupported chain: {chain_name}. Supported chains are: {', '.join(block_explore_config.keys())}" ) + self.sourcecode = {} - logger.info(f"Initialized BlockExplorer for chain: {chain_name}") - - def _make_request( - self, module: str, action: str, address: str, chainid: Optional[int] = None - ) -> Dict[str, Any]: + def _make_request(self, module: str, action: str, address: str) -> Dict[str, Any]: """ Make a request to the BeraScan API. @@ -52,8 +50,8 @@ def _make_request( "address": address, "apikey": self.api_key, } - if chainid: - params["chainid"] = chainid + if self.chainid: + params["chainid"] = self.chainid try: with requests.get(self.base_url, params=params) as response: @@ -68,18 +66,204 @@ def _make_request( return data["result"] - def fetch_contract_metadata(self, address: str) -> Dict: + def fetch_source_code(self, address: str) -> Dict[str, Any]: + """ + Fetch the source code for a verified contract. + + Args: + contract_address (str): The address of the contract to fetch source code for. + + Returns: + Dict[str, Any]: The contract source code information including: + - SourceCode: The actual source code + - ABI: The contract ABI + - ContractName: The name of the contract + - CompilerVersion: The compiler version used + - OptimizationUsed: Whether optimization was used + - Runs: Number of optimization runs + - ConstructorArguments: Constructor arguments + - Library: Library information + - LicenseType: The license type + - Proxy: Whether the contract is a proxy + - Implementation: Implementation address if proxy + - SwarmSource: Swarm source if available + """ + sourcecode = self.sourcecode.get(address, None) + if sourcecode is None: + result = self._make_request( + module="contract", + action="getsourcecode", + address=address, + ) + if isinstance(result, list) and len(result) > 0: + self.sourcecode[address] = result[0] + else: + raise ValueError(f"No source code found for contract {address}") + + def get_contract_metadata(self, address: str) -> Dict: """ Fetch contract metadata from Etherscan, including contract name, proxy status, and implementation address. """ - chainid = block_explore_config[self.chain_name]["chainid"] or None - result = self._make_request( - module="contract", action="getsourcecode", address=address, chainid=chainid - ) - contract_info = result[0] - return { - "ContractName": contract_info.get("ContractName"), - "Proxy": contract_info.get("Proxy") == "1", - "Implementation": contract_info.get("Implementation"), + self.fetch_source_code(address) + all_data = self.sourcecode.get(address) + if all_data is None: + raise ValueError(f"No source code found for contract {address}") + metadata = { + "ContractName": all_data.get("ContractName"), + "Proxy": all_data.get("Proxy") == "1", + "Implementation": all_data.get("Implementation"), + "CompilerVersion": all_data.get("CompilerVersion"), + "Library": all_data.get("Library"), } + return metadata + + def save_sourcecode(self, address: str, save_dir: str) -> str: + """ + Fetch contract source code from Berascan and save it locally. + + Args: + address (str): The contract address + save_dir (str): Directory to save the source code + + Returns: + str: Path to the saved source code file + """ + self.fetch_source_code(address) + source_code = self.sourcecode[address]["SourceCode"] + contract_name = self.sourcecode[address]["ContractName"] + + # Create export directory + export_dir = os.path.join(save_dir, f"{address}-{contract_name}") + if not os.path.exists(export_dir): + os.makedirs(export_dir) + + # Handle different source code formats + dict_source_code = None + try: + # Try to parse as double-braced JSON + dict_source_code = json.loads(source_code[1:-1]) + assert isinstance(dict_source_code, dict) + except (JSONDecodeError, AssertionError): + try: + # Try to parse as single-braced JSON + dict_source_code = json.loads(source_code) + assert isinstance(dict_source_code, dict) + except (JSONDecodeError, AssertionError): + # Handle as single file + filename = os.path.join(export_dir, f"{contract_name}.sol") + with open(filename, "w", encoding="utf8") as f: + f.write(source_code) + return filename + + # Handle multiple files case + if "sources" in dict_source_code: + source_codes = dict_source_code["sources"] + else: + source_codes = dict_source_code + + filtered_paths = [] + for filename, source_code in source_codes.items(): + path_filename = Path(filename) + + # Only keep solidity files + if path_filename.suffix not in [".sol", ".vy"]: + continue + + # Handle contracts directory imports + if "contracts" == path_filename.parts[0] and not filename.startswith("@"): + path_filename = Path( + *path_filename.parts[path_filename.parts.index("contracts") :] + ) + + # Convert absolute paths to relative + if path_filename.is_absolute(): + path_filename = Path(*path_filename.parts[1:]) + + filtered_paths.append(path_filename.as_posix()) + path_filename_disk = Path(export_dir, path_filename) + + # Ensure path is within allowed directory + allowed_path = os.path.abspath(export_dir) + if ( + os.path.commonpath((allowed_path, os.path.abspath(path_filename_disk))) + != allowed_path + ): + raise IOError( + f"Path '{path_filename_disk}' is outside of the allowed directory: {allowed_path}" + ) + + # Create directory if needed + os.makedirs(path_filename_disk.parent, exist_ok=True) + + # Write file + with open(path_filename_disk, "w", encoding="utf8") as f: + f.write(source_code["content"]) + + # Handle remappings + remappings = dict_source_code.get("settings", {}).get("remappings", []) + if remappings: + remappings_path = os.path.join(export_dir, "remappings.txt") + with open(remappings_path, "w", encoding="utf8") as f: + for remapping in remappings: + if "=" in remapping: + origin, dest = remapping.split("=", 1) + # Always use a trailing slash for the destination + f.write(f"{origin}={str(Path(dest) / '_')[:-1]}\n") + + # Create metadata config + metadata_config = { + "solc_remaps": remappings if remappings else {}, + "solc_solcs_select": self.sourcecode[address].get("CompilerVersion", ""), + "solc_args": " ".join( + filter( + None, + [ + ( + "--via-ir" + if dict_source_code.get("settings", {}).get("viaIR") + else "" + ), + ( + f"--optimize --optimize-runs {self.sourcecode[address].get('Runs', '')}" + if self.sourcecode[address].get("OptimizationUsed") == "1" + else "" + ), + ( + f"--evm-version {self.sourcecode[address].get('EVMVersion')}" + if self.sourcecode[address].get("EVMVersion") + and self.sourcecode[address].get("EVMVersion") != "Default" + else "" + ), + ], + ) + ), + } + + with open( + os.path.join(export_dir, "crytic_compile.config.json"), "w", encoding="utf8" + ) as f: + json.dump(metadata_config, f) + + # Find main contract file + main_contract_path = None + for path in filtered_paths: + path_filename = Path(path) + if path_filename.stem == contract_name: + main_contract_path = os.path.join(export_dir, path) + break + elif path_filename.stem.lower() == contract_name.lower(): + main_contract_path = os.path.join(export_dir, path) + break + + # If no main contract found, use first .sol file + if main_contract_path is None: + for root, _, files in os.walk(export_dir): + for file in files: + if file.endswith(".sol"): + main_contract_path = os.path.join(root, file) + break + if main_contract_path: + break + + return main_contract_path diff --git a/src/permission_scanner/utils/block_explorer_config.json b/src/permission_scanner/utils/block_explorer_config.json index b7811d4..0444a89 100644 --- a/src/permission_scanner/utils/block_explorer_config.json +++ b/src/permission_scanner/utils/block_explorer_config.json @@ -1,4 +1,7 @@ { "mainnet": { "base_url": "https://api.etherscan.io/v2/api", "chainid": 1 }, - "bsc": { "base_url": "https://api.etherscan.io/v2/api", "chainid": 56 } + "bsc": { "base_url": "https://api.etherscan.io/v2/api", "chainid": 56 }, + "berachain": { + "base_url": "https://api.berascan.com/api" + } } diff --git a/src/permission_scanner/utils/logger.py b/src/permission_scanner/utils/logger.py deleted file mode 100644 index 3c75f29..0000000 --- a/src/permission_scanner/utils/logger.py +++ /dev/null @@ -1,56 +0,0 @@ -import logging -import os -from logging.handlers import RotatingFileHandler -from typing import Optional - - -def setup_logger( - name: str, - log_file: Optional[str] = None, - level: int = logging.INFO, - max_bytes: int = 10 * 1024 * 1024, # 10MB - backup_count: int = 5, -) -> logging.Logger: - """ - Set up a logger with console and file handlers. - - Args: - name: Name of the logger - log_file: Path to log file (optional) - level: Logging level - max_bytes: Maximum size of log file before rotation - backup_count: Number of backup files to keep - - Returns: - Configured logger instance - """ - logger = logging.getLogger(name) - logger.setLevel(level) - - # Create formatters - console_formatter = logging.Formatter( - "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - ) - file_formatter = logging.Formatter( - "%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s" - ) - - # Console handler - console_handler = logging.StreamHandler() - console_handler.setFormatter(console_formatter) - logger.addHandler(console_handler) - - # File handler (if log_file is provided) - if log_file: - # Create logs directory if it doesn't exist - log_dir = os.path.dirname(log_file) - if log_dir: - os.makedirs(log_dir, exist_ok=True) - - file_handler = RotatingFileHandler( - log_file, maxBytes=max_bytes, backupCount=backup_count - ) - file_handler.setFormatter(file_formatter) - logger.addHandler(file_handler) - - return logger diff --git a/src/permission_scanner/utils/markdown_generator.py b/src/permission_scanner/utils/markdown_generator.py index ccef468..8f8bb9f 100644 --- a/src/permission_scanner/utils/markdown_generator.py +++ b/src/permission_scanner/utils/markdown_generator.py @@ -1,101 +1,56 @@ -from typing import List, Dict, Any -import datetime +def generate_contracts_table(contracts_object_list): + """ """ + md_content = "## Contracts\n| Contract Name | Address |\n" + md_content += "|--------------|--------------|\n" + for contract in contracts_object_list: + md_content += f"| {contract['name']} | {contract['address']} |\n" -def generate_contracts_table( - contract_data: List[Dict[str, str]], scan_results: Dict[str, Any] -) -> str: - """Generate the contracts overview table. + return md_content - Args: - contract_data (List[Dict[str, str]]): List of contract metadata - scan_results (Dict[str, Any]): Results from contract scanning - Returns: - str: Markdown table of contracts - """ - content = [] - content.append("## Contracts") - content.append("\n| Contract Name | Address | Type |") - content.append("|--------------|---------|------|") - - for contract in contract_data: - contract_name = contract["name"] - address = contract["address"] - contract_type = ( - "Proxy" - if scan_results.get(contract_name, {}).get("Proxy_Address") - else "Implementation" - ) - content.append(f"| {contract_name} | {address} | {contract_type} |") - - return "\n".join(content) - - -def generate_permissions_table(scan_results: Dict[str, Any]) -> str: - """Generate the permissions table. - - Args: - scan_results (Dict[str, Any]): Results from contract scanning - - Returns: - str: Markdown table of permissions - """ - content = [] - content.append("\n## Permissions") - content.append("\n| Contract | Function | Impact | Owner |") - content.append("|----------|----------|---------|-------|") - - for contract_name, contract_data in scan_results.items(): - # Handle proxy permissions if they exist - if "proxy_permissions" in contract_data: - proxy_permissions = contract_data["proxy_permissions"] - for function in proxy_permissions.get("Functions", []): - owner = function.get("Modifiers", []) - if not owner and "_owner" in function: - owner = function["_owner"] - content.append( - f"| {proxy_permissions['Contract_Name']} | {function['Function']} | ... | {owner} |" - ) - - # Handle implementation permissions - if "permissions" in contract_data: - permissions = contract_data["permissions"] - for function in permissions.get("Functions", []): - owner = function.get("Modifiers", []) - if not owner and "_owner" in function: - owner = function["_owner"] - content.append( - f"| {permissions['Contract_Name']} | {function['Function']} | ... | {owner} |" - ) - - return "\n".join(content) - - -def generate_full_markdown( - project_name: str, contract_data: List[Dict[str, str]], scan_results: Dict[str, Any] -) -> str: - """Generate a full markdown report from scan results. - - Args: - project_name (str): Name of the project being scanned - contract_data (List[Dict[str, str]]): List of contract metadata - scan_results (Dict[str, Any]): Results from contract scanning - - Returns: - str: Generated markdown content - """ - content = [] - - # Add header - content.append("# Permission Scanner Report") - content.append( - f"\nGenerated on: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" +def generate_permissions_table(permissions): + """ """ + md_content = "## Permission\n| Contract | Function | Impact | Owner |\n" + md_content += ( + "|-------------|------------|-------------------------|-------------------|\n" ) - content.append(f"Project: {project_name}\n") - - # Generate tables - content.append(generate_contracts_table(contract_data, scan_results)) - content.append(generate_permissions_table(scan_results)) - return "\n".join(content) + for contract, entries in permissions.items(): + try: + proxy_permissions = entries["proxy_permissions"] + contract_name = proxy_permissions["Contract_Name"] + permissioned_functions = proxy_permissions["Functions"] + for permissioned_function in permissioned_functions: + owner = "" + try: + owner = permissioned_function["_owner"] + except KeyError: + owner = permissioned_function["Modifiers"] + pass + + md_content += f"| {contract_name} | {permissioned_function['Function']} | ... | {owner} |\n" + except KeyError: + # just a normal contract + pass + # will do the normal contract or the implementation contract + proxy_permissions = entries["permissions"] + contract_name = proxy_permissions["Contract_Name"] + permissioned_functions = proxy_permissions["Functions"] + for permissioned_function in permissioned_functions: + owner = "" + try: + owner = permissioned_function["_owner"] + except KeyError: + # no simple owner found + owner = permissioned_function["Modifiers"] + pass + + md_content += f"| {contract_name} | {permissioned_function['Function']} | ... | {owner} |\n" + + return md_content + + +def generate_full_markdown(protocol_metadata, contracts, permissions) -> str: + + return f"{generate_contracts_table(contracts)}\n\n{generate_permissions_table(permissions)}" diff --git a/tests/metadata.py b/tests/metadata.py new file mode 100644 index 0000000..dc455de --- /dev/null +++ b/tests/metadata.py @@ -0,0 +1,32 @@ +from slither import Slither +from permission_scanner import ContractScanner, BlockExplorer +import os +from dotenv import load_dotenv + +load_dotenv() + + +def main(): + # Setup environment variables + api_key = os.getenv("ETHERSCAN_API_KEY") + rpc_url = os.getenv("RPC_URL") + + if not api_key or not rpc_url: + raise ValueError("Missing required environment variables") + + # Initiate the BlockExplorer object + block_explorer = BlockExplorer(api_key, "mainnet") + address = "0xf165148978Fa3cE74d76043f833463c340CFB704" + + block_explorer.save_sourcecode(address, save_dir="sourcecode") + + +if __name__ == "__main__": + main() + + +""" +sourcecode.keys() +dict_keys(['SourceCode', 'ABI', 'ContractName', 'CompilerVersion', 'CompilerType', 'OptimizationUsed', 'Runs', 'ConstructorArguments', +'EVMVersion', 'Library', 'LicenseType', 'Proxy', 'Implementation', 'SwarmSource', 'SimilarMatch']) +""" From 31d27d939ad19310241638e1d6bed62d744c0f9b Mon Sep 17 00:00:00 2001 From: trangnv Date: Fri, 9 May 2025 20:20:48 +0700 Subject: [PATCH 5/8] Update scanner and block explorer implementation --- .DS_Store | Bin 6148 -> 6148 bytes example/contracts.json | 14 +- example/contracts_full.json | 17 --- example/run_scanner.py | 28 ++-- .../__pycache__/scanner.cpython-311.pyc | Bin 23779 -> 19118 bytes src/permission_scanner/scanner/scanner.py | 140 +++--------------- .../block_explorer.cpython-311.pyc | Bin 11425 -> 13643 bytes .../utils/block_explorer.py | 4 +- tests/metadata.py | 32 ---- 9 files changed, 49 insertions(+), 186 deletions(-) delete mode 100644 example/contracts_full.json delete mode 100644 tests/metadata.py diff --git a/.DS_Store b/.DS_Store index 1fca7aaf248e7932445b8948c2e295145554a6c4..959929bfa6d6cd9bd28a368033d7437e7a3b05ea 100644 GIT binary patch delta 84 zcmZoMXfc=&!BbF@HV6C2yahQQ729Dn%%y9yQZ delta 153 zcmZoMXfc=&!NZWukjfAbB=Z?kCU0b1KG{HoMNxpE7|1VWC_1CE+{Q|;Cq*f$+J-+G9SW)_s-k}1uJ3#f zNt)T+-^}-#`R3c1`Q~r0(w}^nI$v}+tOUxI59*Je?tb1G%$)`591)2~(!Tu)V4A0%(!B%j63Ge zcw!#lnM8BioAJebl<3G#B3e!n(RxisHjp_TI)o+W7j^qc#C8{nh)@*zQ?gPNqN$`( z)NdbqrfB%WXGu_gt7 zA916~p=#%JVlDLJ7X$DE>UgvMOF3)uXd*QhA4_DUqJ4smk4Q;Hqf7}=WRffwYmcPI zlcRCz_(Xb~Ni3e2NX18`XNo2^k&I8WbkPc;ai+w@6ib>kpHvh8_3*v_7Qi`@)n!Rs z6(D5wKv4{^TCx<#${NWEK%uE7GXRC1X_OHtLzzkupp0c|7^#X2DrGLqR832n5^+2$ zd18@P0%ZYsMn;x~rZ;r3eyf%*nAyv^B852W^Zh~}ZPOJ^i-BQYkVhQM2Nm-pAcNH) z)FK4Y>UQtYn;GQxoqno@vDa2;0Pdj#M<0_ue3)t7rs zzo&lwj;Wt+$hVoF)loBl*?O3Eakp&_+P}7q(yhGT6}tL_ozf_eI^Lsw`3=q>rF($= zfV({gzT16^?#p{T8!3G%ztfwe^dP_G`w<`4qxlC0s^n)@ucUN} zzx4Qj=yUwV$Y1C={`ECI=7TV?h=G~;?KQ_>+@985#t^WIA%3{k&#$xw=pcW!^_NuS zO50~?JpWqT@61LtVo82xV;bi0U}GcA^5CWm!U^4$CjN^}?R9Dop16`=!;%s|mSCyG zk+dYU4FB(@I%?;&o$GByOW*ONG@+!%$9R9|Xc!|5|Fj9ao1sA256tR&8~IOL zYWU}R1G&nUnsIl*c!~#Lv4C!6Q(YvY0d32&i{!E%Xf+oh59CImZA)^|G~)&>!wvKD zT{b05yN4*8+vcn=V&}8wU~bBgHOM{%f8Jsdt!OOTF6*yBLYRY3E6-A;zT74R26UAv z(f*-fj7oD^1{>cXIddTxiJAb)% z1%D%I%XO3u2ob&2GlGEn9t~*L3_6unAo_6?R9h{R)}S=2mt7yLQEOC}(Ry^p9VJh9 z9)dM9Wt+0kkl8wZXI%}qMm>WNP*2t-21eBd#cWx}vSEVQ$JzD|aqCp>W@Y2j1eMOE zHuMn29l`5&cvgqY{AK;tRsBFGI8+Y~)f%(VgkACF9RZI^X)hbXJYBO55bhTKs~w&k z&Uh(GE6^3-sQ(Ww81Af!JWH!(s5`dIUX@ptA<9)wB@qtcH7azXv@e+$Yf3c1S;P14 z{O6kQ;XoG=E+Kp$;33`ekanD~wU^H$xg$a$4Q=z(@q@d*MR)SMyJXWhKywGXta7uv z`y6;0Mf7Yv_AfwdAD~8*U;e@q( zhDlaUi$zPt6E=%XJQB3zXyq67wmhXCg-hqB92X^)Iwpy11Qm}WoCYXbq_N42#1e|c z6l9Jf3?tw9zPypv#)XPjR+EbZ{bbRs7!)N1dDsb^hn>)Q z*g4%_vgYp%yi2VEu!?Z9ks98$f#Hf}cw^K45vBw6&oFh3 z;N^|oLcm?cRwCe`%-m(bA>`K~lqV}sLD)Ml?1hMiR+U`!25X4`{M8@WRWcxDB!Lwr z6SmC6TT`-N%SxJ8m2B9uBhP^?C#eZ8*{hvs(9NTRlCGcfwgWHa4p2ic{f=}#I_sNF z&IT_HT#w%L-%P%;;pWkwH7vAkn-6bWsNG&7s@|ulv^nDRA~i*$^2|hPY*>rgQlu~{ zMLXQC)2ZN*wGw0FjGe)NtVZ|^f{9;@Jr0+mi?K#)c{?!+|5vO%r-oSFQ+QV``X`lC zT3);Ss;S;TwIDAK{6lkBW?Y<1OWo{Eu!gu38L_0Nlom*2{{OuNKk?0%-vY6mR3Gj? bl-*l0G6U_UC0Gw1SKZTO_dh{J4dDL(*hNEs delta 7946 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 bool: @@ -93,71 +92,6 @@ def _get_msg_sender_checks(function: Function) -> List[str]: ] return all_conditional_nodes_on_msg_sender - def _extract_solidity_version( - self, contract_path: str, default_version: str = "0.7.6" - ) -> str: - """Extract Solidity version from contract's pragma statement. - - Args: - contract_path (str): Path to the contract file - - Returns: - str: Solidity version (e.g. "0.4.24") - """ - try: - with open(contract_path, "r") as f: - content = f.read() - # Match pragma solidity statements like: - # pragma solidity ^0.4.24; - # pragma solidity 0.4.24; - # pragma solidity >=0.4.24 <0.5.0; - match = re.search( - r"pragma\s+solidity\s+(\^?[0-9]+\.[0-9]+\.[0-9]+)", content - ) - if match: - return match.group(1).replace("^", "") - return default_version # Default version if not found - except Exception as e: - return default_version # Default version on error - - def _switch_solidity_version(self, version: str) -> None: - """Switch to the specified Solidity version using solc-select. - - Args: - version (str): Solidity version to switch to - """ - try: - current_version_info = subprocess.run( - ["solc", "--version"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True, - ) - if version in current_version_info.stdout: - return - subprocess.run(["solc-select", "use", version], check=True) - except subprocess.CalledProcessError as e: - raise f"Failed to switch Solidity version to {version}: {e}" - - def _get_contract(self, address: str) -> Contract: - """Get a contract from the block explorer and initialize Slither. - - Args: - address (str): The contract address to fetch - - Returns: - tuple: (contract_metadata, main_contract_path) - """ - contract_metadata = self.block_explorer.get_contract_metadata(address) - # contract_save_dir = os.path.join( - # self.export_dir, contract_metadata["ContractName"] - # ) - # os.makedirs(contract_save_dir, exist_ok=True) - main_contract_path = self.block_explorer.save_sourcecode( - address, self.export_dir - ) - return contract_metadata, main_contract_path - def _scan_permissions(self, contract: Contract) -> Dict[str, Any]: """Analyze permissions in a contract and store results. @@ -361,7 +295,7 @@ def _check_proxy(self, contract_metadata: Dict[str, Any]): except Exception as e: raise f"Failed to get Implementation contract from Etherscan. \n\n\n + {e}" - def scan(self) -> Dict[str, Any]: + def scan(self) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]: """Scan a contract for permissions and storage. Args: @@ -371,7 +305,7 @@ def scan(self) -> Dict[str, Any]: Dict[str, Any]: The scan results containing permissions and storage analysis """ final_scan_result = {} - contract_metadata, main_contract_path = self._get_contract(self.address) + contract_metadata = self.block_explorer.get_contract_metadata(self.address) contract_name = contract_metadata["ContractName"] isProxy = contract_metadata["Proxy"] == 1 @@ -384,11 +318,10 @@ def scan(self) -> Dict[str, Any]: # Initialize scan_result structure self.scan_result[contract_name] = {} - # Extract and switch to the correct Solidity version - solidity_version = self._extract_solidity_version(main_contract_path) - self._switch_solidity_version(solidity_version) - - slither = Slither(main_contract_path) + slither = Slither( + f"{self.chain_name}:{self.address}", + export_dir=f"{self.export_dir}/{self.project_name}-contracts/{contract_name}", + ) # Get target contract from slither target_contract = [c for c in slither.contracts if c.name == contract_name] @@ -404,14 +337,10 @@ def scan(self) -> Dict[str, Any]: # If proxy, scan implementation if isProxy: impl_address = contract_metadata["Implementation"] - _, impl_path = self._get_contract(impl_address) - - # Extract and switch to implementation contract's Solidity version - impl_solidity_version = self._extract_solidity_version(impl_path) - self._switch_solidity_version(impl_solidity_version) - - impl_slither = Slither(impl_path) - + impl_slither = Slither( + f"{self.chain_name}:{impl_address}", + export_dir=f"{self.export_dir}/{self.project_name}-contracts/{self.implementation_name}", + ) # Get implementation contract impl_contracts = impl_slither.contracts_derived @@ -456,32 +385,3 @@ def scan(self) -> Dict[str, Any]: final_scan_result[self.contract_name] = self.scan_result return final_scan_result, self.contract_data_for_markdown - - def generate_reports(self) -> None: - """Generate JSON and markdown reports from scan results. - - Args: - project_name (str): Name of the project being scanned - """ - # Create reports directory - reports_dir = os.path.join(self.export_dir, "reports") - os.makedirs(reports_dir, exist_ok=True) - - # Save JSON report - json_path = os.path.join(reports_dir, "permissions.json") - final_result = {} - if self.implementation_name: - final_result[self.implementation_name] = self.scan_result - else: - final_result[self.contract_name] = self.scan_result - - with open(json_path, "w") as f: - json.dump(final_result, f, indent=4) - - # Generate and save markdown report - markdown_content = generate_full_markdown( - self.contract_name, self.contract_data_for_markdown, final_result - ) - markdown_path = os.path.join(reports_dir, f"markdown.md") - with open(markdown_path, "w") as f: - f.write(markdown_content) diff --git a/src/permission_scanner/utils/__pycache__/block_explorer.cpython-311.pyc b/src/permission_scanner/utils/__pycache__/block_explorer.cpython-311.pyc index 095c9960c4e6aab89d0d103d6d0acbd5a5b303e5..1a812219266300102691e7271d96e3d79925add0 100644 GIT binary patch delta 6488 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 W`fmTwdC;r<#7i8kHhfZx1N=XkyG)}1 diff --git a/src/permission_scanner/utils/block_explorer.py b/src/permission_scanner/utils/block_explorer.py index 7a9ea9e..9069229 100644 --- a/src/permission_scanner/utils/block_explorer.py +++ b/src/permission_scanner/utils/block_explorer.py @@ -113,8 +113,8 @@ def get_contract_metadata(self, address: str) -> Dict: "ContractName": all_data.get("ContractName"), "Proxy": all_data.get("Proxy") == "1", "Implementation": all_data.get("Implementation"), - "CompilerVersion": all_data.get("CompilerVersion"), - "Library": all_data.get("Library"), + # "CompilerVersion": all_data.get("CompilerVersion"), + # "Library": all_data.get("Library"), } return metadata diff --git a/tests/metadata.py b/tests/metadata.py deleted file mode 100644 index dc455de..0000000 --- a/tests/metadata.py +++ /dev/null @@ -1,32 +0,0 @@ -from slither import Slither -from permission_scanner import ContractScanner, BlockExplorer -import os -from dotenv import load_dotenv - -load_dotenv() - - -def main(): - # Setup environment variables - api_key = os.getenv("ETHERSCAN_API_KEY") - rpc_url = os.getenv("RPC_URL") - - if not api_key or not rpc_url: - raise ValueError("Missing required environment variables") - - # Initiate the BlockExplorer object - block_explorer = BlockExplorer(api_key, "mainnet") - address = "0xf165148978Fa3cE74d76043f833463c340CFB704" - - block_explorer.save_sourcecode(address, save_dir="sourcecode") - - -if __name__ == "__main__": - main() - - -""" -sourcecode.keys() -dict_keys(['SourceCode', 'ABI', 'ContractName', 'CompilerVersion', 'CompilerType', 'OptimizationUsed', 'Runs', 'ConstructorArguments', -'EVMVersion', 'Library', 'LicenseType', 'Proxy', 'Implementation', 'SwarmSource', 'SimilarMatch']) -""" From 37cb379de42a29f1cbe9076bf9a79a8a756b753a Mon Sep 17 00:00:00 2001 From: trangnv Date: Fri, 9 May 2025 20:24:07 +0700 Subject: [PATCH 6/8] Update scanner and block explorer implementation --- .gitignore | 2 +- example/run_scanner.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index d8fd434..7ad5ac4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ src/__pycache__ /contracts.json /permissions.json -/results/ +results/ # markdown generation outputs *.md diff --git a/example/run_scanner.py b/example/run_scanner.py index 2045858..0aa7982 100644 --- a/example/run_scanner.py +++ b/example/run_scanner.py @@ -69,11 +69,14 @@ def main(): report_dir = f"{export_dir}/{project_name}-reports" os.makedirs(report_dir, exist_ok=True) - json_path = os.path.join(report_dir, "permissions.json") + permissions_json_path = os.path.join(report_dir, "permissions.json") markdown_path = os.path.join(report_dir, "markdown.md") - with open(json_path, "w") as f: + # save permissions.json + with open(permissions_json_path, "w") as f: json.dump(all_scan_results, f, indent=4) + + # save markdown.md markdown_content = generate_full_markdown( project_name, all_contract_data_for_markdown, all_scan_results ) From 12d09e8d82e62b68f41c6070259305e4b2aa03c5 Mon Sep 17 00:00:00 2001 From: trangnv Date: Mon, 12 May 2025 18:45:20 +0700 Subject: [PATCH 7/8] edit gitignore --- .gitignore | 3 ++- example/run_scanner.py | 6 +++--- .../__pycache__/scanner.cpython-311.pyc | Bin 19118 -> 19667 bytes src/permission_scanner/scanner/scanner.py | 9 ++++++++- .../block_explorer.cpython-311.pyc | Bin 13643 -> 13643 bytes .../markdown_generator.cpython-311.pyc | Bin 2416 -> 2416 bytes 6 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 7ad5ac4..678fb1b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ results/ .venv logs .cursor -temp/ \ No newline at end of file +temp/ +.DS_Store \ No newline at end of file diff --git a/example/run_scanner.py b/example/run_scanner.py index 0aa7982..d6ddb83 100644 --- a/example/run_scanner.py +++ b/example/run_scanner.py @@ -35,19 +35,19 @@ def main(): load_dotenv() # Load contracts from json - config_json = load_config_from_file("example/contracts_full.json") + config_json = load_config_from_file("temp/contracts_kodiak_error.json") contracts_addresses = config_json["Contracts"] project_name = config_json["Project_Name"] chain_name = config_json["Chain_Name"] # Setup environment variables - block_explorer_api_key = os.getenv("ETHERSCAN_API_KEY") + block_explorer_api_key = os.getenv("BERASCAN_API_KEY") rpc_url = os.getenv("RPC_URL") if not block_explorer_api_key or not rpc_url: raise ValueError("Missing required environment variables") - export_dir = f"results" + export_dir = f"temp/kodiak_error" # Scan each contract all_scan_results = {} diff --git a/src/permission_scanner/scanner/__pycache__/scanner.cpython-311.pyc b/src/permission_scanner/scanner/__pycache__/scanner.cpython-311.pyc index 1c498db001e6bd26989321192bebab737577cc15..76ca1da7ceb00d70d1aae8b75b9fa4203788445d 100644 GIT binary patch delta 2199 zcma)6Z%A8L6u&ol`SaB~M!<7P zXG(|)8gEIuQesrpcx%#~@Dz<+&!e;>`I2|n` zf&p8~uxXVeW@EHolrbwV-3FmV(VJ>gD{LWuj{OWCA_$`?=W%ozrMX$t2j(ki7Grb; zq!_6tVyP?o{zL+7En#ANPdjvmTaXsexSdL8eWo>@2K>G@CP@@aLhLK1bqqy8_)MKR*sjfAbORKh{sTV zl(8v6U5AirC6a2pUsaKcop@FIIQG%gAzv1_YJG>w$v;9@e5I~r97k18b^NcIp+t=y z;hF07Jv*2GRXtwi6ScaNj8nC)`PpRf^9YJs4Mla*y}rQkOS%a=yRnA)e93;Lw`4_X zj1xd=Y51Hs%jTjuMa0jhsNBw=tz%l1OS_+IQLjy5s1&s_w-NMC=xvzqjF54`Bsa22 zo^%=W&xp_+Nfk&#;eR4KRnd?~Ba5=wRUtj5NzW?%rRd5)i3bnO^!5H=uvwQGd>$*C z1UTPX3(KtAt{To-F7dugZ^?M zK>xaM8&}bf!cDk`ejfhRxdCQrb|*}<(4L_)rU{NFhJx8Wm$d8lE@rodH{n_H?F4!X zEYMxSy>Bs#`~d=zN1UTGZ$+YePw*^8wrhg-q(6j?9id}e=*T;mQnO%WpOJ$iEGa6u z!^8T+3Q+;_sO|*4GxSa8Zhg~A_jYXro_ixZodX3HXx_$od5GhM?10SM@*<54-@vu> zx8ae_-KN%+>FtIwcz)n^PK66BP}tTe!nQ^cwl#|Lm-519VB`_5)k?F))r8y3TWwtU zgl($}1Er3OOZegD^2Kj4o}h!H!IvknWeDfd3qLZujb#sz_8NM3wAcC*!@tt)(TDsl mu&nKA2VIYZ0XlDJzg_w`l8Jn1!7Re_Kl=sU!4W1}^nU>TOr&-I delta 1618 zcma)6Uuaup6#q_cZvNaPH~01?X|kkii(m+&c7k2%sGD}3&UQWxCJl}ldaYZQbnxX0 zQoc(>gf?S>q7Sk?3}2?`iy)H0p7tP~?~Aud>i!+L=bm$Z z=ihg}bMLJy@YQ9|ug78q6MRcfJnxD*Cr6Wd?Y>XA}J%4#iI zkCkFl4%GB|ycDk+C8KVZOtb~vSRQ|appPZvmqm@ zNxEpD?(6Eda^5EZS7{k7QdW4SnBa+g$`EQ*LISq^ygZqN0RJtWX9D*V9P)+bvv|$)o<)};QCjMMXuxpOz|yT6gT2VTVyjMmh@b-|2S?8 zZ^L8}d$6RA-XQuRlz<(_5cn)fbr$X*usS+ zhgKGx#&`kfAm21I{Es6a!)rVAXaV5N&efNTFfdGqMd0UxiM6EL9HvKubWq3^AD+IP zHR%W{znF{XmMXP!1Iw4`JB{~h4!y!}Pk*9qBiazZn78?bV~2d7D*XDfymBX9;P1|S zVYOAo((Vyt_XS%GwxfLF_y>^U-yEM2Xii5orz4uvS5LOJoo8SD9aQ=ZCgdY0*5HSo zuTOjlFwalV=3kfxbsE~_i7y#W4a`wU@|&|0@IC))_DAa$8pMyMA$VH>fIIx>xgmac eF2MhuOTw=_cG7*FsvwFy`gN~^-G7XWY~WuUA0i(B diff --git a/src/permission_scanner/scanner/scanner.py b/src/permission_scanner/scanner/scanner.py index acee451..4dff08d 100644 --- a/src/permission_scanner/scanner/scanner.py +++ b/src/permission_scanner/scanner/scanner.py @@ -1,7 +1,7 @@ from typing import List, Dict, Any, Tuple import urllib.error import re - +import json from slither import Slither from slither.core.declarations.function import Function from slither.core.declarations.contract import Contract @@ -306,6 +306,11 @@ def scan(self) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]: """ final_scan_result = {} contract_metadata = self.block_explorer.get_contract_metadata(self.address) + with open( + f"{self.export_dir}/{self.project_name}-contracts/contract_metadata.json", + "w", + ) as f: + json.dump(contract_metadata, f) contract_name = contract_metadata["ContractName"] isProxy = contract_metadata["Proxy"] == 1 @@ -321,6 +326,7 @@ def scan(self) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]: slither = Slither( f"{self.chain_name}:{self.address}", export_dir=f"{self.export_dir}/{self.project_name}-contracts/{contract_name}", + allow_path=f"{self.export_dir}/{self.project_name}-contracts", ) # Get target contract from slither @@ -340,6 +346,7 @@ def scan(self) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]: impl_slither = Slither( f"{self.chain_name}:{impl_address}", export_dir=f"{self.export_dir}/{self.project_name}-contracts/{self.implementation_name}", + allow_path=f"{self.export_dir}/{self.project_name}-contracts", ) # Get implementation contract impl_contracts = impl_slither.contracts_derived diff --git a/src/permission_scanner/utils/__pycache__/block_explorer.cpython-311.pyc b/src/permission_scanner/utils/__pycache__/block_explorer.cpython-311.pyc index 1a812219266300102691e7271d96e3d79925add0..6ea19ce052b2988ae46898ba1521720b95df3f7b 100644 GIT binary patch delta 20 acmX?|bvlcCIWI340}z-q%5CI!Fa-cVB?QO- delta 20 acmX?|bvlcCIWI340}y!smEFkgU Date: Wed, 14 May 2025 11:05:29 +0700 Subject: [PATCH 8/8] gitignore --- .gitignore | 47 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 678fb1b..25cc347 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,47 @@ -/venv +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual Environment +venv/ +.venv/ +ENV/ +env/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +.cursor/ + +# Environment variables .env -src/__pycache__ -/crytic-export +# Project specific +/crytic-export /contracts.json /permissions.json results/ -# markdown generation outputs *.md - -.venv -logs -.cursor +logs/ temp/ .DS_Store \ No newline at end of file