From aa6733ab5f49bc1007f2bfe499542f8703c51787 Mon Sep 17 00:00:00 2001 From: alexvozhak Date: Mon, 29 Jul 2024 18:03:58 +0300 Subject: [PATCH] Added binary verification through Ganache --- config_samples/lido_dao_holesky_config.json | 29 ++++ config_samples/lido_dao_sepolia_config.json | 78 +++++++++- diffyscan/diffyscan.py | 59 +++++-- diffyscan/utils/binary_verifier.py | 162 +++++++++++++------- diffyscan/utils/constants.py | 13 ++ diffyscan/utils/encoder.py | 96 ++++++++++++ diffyscan/utils/ganache.py | 34 ++++ 7 files changed, 402 insertions(+), 69 deletions(-) create mode 100644 diffyscan/utils/encoder.py create mode 100644 diffyscan/utils/ganache.py diff --git a/config_samples/lido_dao_holesky_config.json b/config_samples/lido_dao_holesky_config.json index 1b94274..64c9dac 100644 --- a/config_samples/lido_dao_holesky_config.json +++ b/config_samples/lido_dao_holesky_config.json @@ -111,5 +111,34 @@ "commit": "6bd6b76d1156e20e45d1016f355d154141c7e5b9", "relative_root": "contracts" } + }, + "ConstructorArgs": { + "OracleReportSanityChecker" : [ + "0x28FAB2059C713A7F9D8c86Db49f9bb0e96Af1ef8", + "0x22896Bfc68814BFD855b1a167255eE497006e730", + [ + 1500, + 500, + 1000, + 250, + 2000, + 100, + 100, + 128, + 5000000 + ], + [ + [], + [], + [], + [], + [], + [], + [], + [], + [], + [] + ] + ] } } diff --git a/config_samples/lido_dao_sepolia_config.json b/config_samples/lido_dao_sepolia_config.json index f03da40..50a4f06 100644 --- a/config_samples/lido_dao_sepolia_config.json +++ b/config_samples/lido_dao_sepolia_config.json @@ -97,5 +97,81 @@ "commit": "6bd6b76d1156e20e45d1016f355d154141c7e5b9", "relative_root": "contracts" } - } + }, + "ConstructorArgs": { + "0x8f6254332f69557A72b0DA2D5F0Bc07d4CA991E7" : ["0xf30a674935479cc6f2254ba65d7534eab8bd6ba2","0x6885E36BFcb68CB383DfE90023a462C03BCB2AE5",""], + "0x9726CA9AEFF4BC8FB8C084BdAbdB71608248E3f8" : [ + "0x3e3FE7dBc6B4C189E7128855dD526361c49b40Af" + ], + "0x604dc1776eEbe7ddCf4cf5429226Ad20a5a294eE" : [ ["0xd497be005638efcf09f6bfc8dafbbb0bb72cd991", + "0x6885e36bfcb68cb383dfe90023a462c03bcb2ae5", + "0x94b1b8e2680882f8652882e7f196169de3d9a3b2", + "0x3483c140ef7f2716460198ff831a8e53f05f1606", + "0x3e3fe7dbc6b4c189e7128855dd526361c49b40af", + "0xbac2a471443f18ac5c31078b96c5797a78fcc680", + "0x3483c140ef7f2716460198ff831a8e53f05f1606", + "0x61bb0ef69262d5ef1cc2873cf61766751d99b699", + "0x4f36aaeb18ab56a4e380241bea6ebf215b9cb12c", + "0x32a0e5828b62aab932362a4816ae03b860b65e83", + "0x7637d44c9f2e9ca584a8b5d2ea493012a5cdaeb6", + "0x1583c7b3f4c3b008720e6bce5726336b0ab25fdd", + "0xde7318afa67ead6d6bbc8224dfce5ed6e4b86d76", + "0x7bc76076b0f3879b4a750450c0ccf02c6ca11220"] + ], + "0xB82381A3fBD3FaFA77B3a7bE693342618240067b" : ["0x3e3FE7dBc6B4C189E7128855dD526361c49b40Af"], + "0x46cF57508B0565decC0419B833C2dAFa50B132e0" : ["0x80b5DC88C98E528bF9cb4B7F0f076aC41da24651"], + "0x94B1B8e2680882f8652882e7F196169dE3d9a3B2" : ["0x3e3FE7dBc6B4C189E7128855dD526361c49b40Af", "0x32A0E5828B62AAb932362a4816ae03b860b65e83"], + "0x0220A1cF6C3a548BE75aEabCdA509CaB08CDe063" : [ "0xB82381A3fBD3FaFA77B3a7bE693342618240067b", "Lido: stETH Withdrawal NFT", "unstETH" + ], + "0xee386d787Db24AbEe4dcc591F35405E323b70Dad" : ["0x3e3FE7dBc6B4C189E7128855dD526361c49b40Af","0x32A0E5828B62AAb932362a4816ae03b860b65e83"], + "0x61Bb0Ef69262d5EF1cc2873cf61766751D99B699" : ["0x6885E36BFcb68CB383DfE90023a462C03BCB2AE5", "0x32A0E5828B62AAb932362a4816ae03b860b65e83", "0x3e3FE7dBc6B4C189E7128855dD526361c49b40Af", 0, 0], + "0x082d16150BF75BB8F2197eEC1d293DbA96c93638" : ["0x8f6254332f69557A72b0DA2D5F0Bc07d4CA991E7","0x3e3FE7dBc6B4C189E7128855dD526361c49b40Af", "0x3483c140EF7F2716460198Ff831a8e53F05F1606", 12, 1655733600], + "0x1c2807B207f140a1DE0b39E5546eDEf67Af2568c" : [1], + "0xd06dF83b8ad6D89C86a187fba4Eae918d497BdCB" : ["0x9D381f44d1fbdf8190fA0EcdC028e2Af24DdD3FE","0x0000000000000000000000000000000000000000",0, "TEST Lido DAO Token", 18, "TLDO", 1], + "0x758D8c3CE794b3Dfe3b3A3482B7eD33de2109D95" : [32, 12, 1655733600, 12, 10, "0x6885E36BFcb68CB383DfE90023a462C03BCB2AE5", "0xd497Be005638efCf09F6BFC8DAFBBB0BB72cD991"], + "0xC40801b88C835a58e54eEE6679D301ba31a4C72b" : [12, 1655733600, "0x8f6254332f69557A72b0DA2D5F0Bc07d4CA991E7"], + "0xbac2A471443F18aC5C31078b96C5797A78fCc680" : [ + "0x28FAB2059C713A7F9D8c86Db49f9bb0e96Af1ef8", + "0x22896Bfc68814BFD855b1a167255eE497006e730", + [ + 1500, + 500, + 1000, + 250, + 2000, + 100, + 100, + 128, + 5000000 + ], + [ + [], + [], + [], + [], + [], + [], + [], + [], + [], + [] + ] + ], + "0x7bC76076b0f3879b4A750450C0Ccf02c6Ca11220" : ["0x6885E36BFcb68CB383DfE90023a462C03BCB2AE5",""], + "0x6155bD199ECcc79Ff4e8B392f6cBD9c9874E8916" : ["0x1c2807B207f140a1DE0b39E5546eDEf67Af2568c"], + "0xC73cd4B2A7c1CBC5BF046eB4A7019365558ABF66" : ["0x6155bD199ECcc79Ff4e8B392f6cBD9c9874E8916", "0xcd567bdf93dd0f6acc3bc7f2155f83244d56a65abbfbefb763e015420102c67b", ""], + "0x52AD3004Bc993d63931142Dd4f3DD647414048a1" : ["0x6155bD199ECcc79Ff4e8B392f6cBD9c9874E8916","0x5c9918c99c4081ca9459c178381be71d9da40e49e151687da55099c49a4237f1", ""], + "0x098a952BD200005382aEb3229e38ae39A7616F56" : [32,12,1655733600, 75, 10, "0x7FAcEF1c7248ed171FDd9ea3B25B4550b38e6133", "0x7637d44c9f2e9cA584a8B5D2EA493012A5cdaEB6"], + "0x1583C7b3f4C3B008720E6BcE5726336b0aB25fdd" : ["0x0220A1cF6C3a548BE75aEabCdA509CaB08CDe063", "0x32A0E5828B62AAb932362a4816ae03b860b65e83","0x0"], + "0x32A0E5828B62AAb932362a4816ae03b860b65e83" : ["0x6155bD199ECcc79Ff4e8B392f6cBD9c9874E8916","0x701a4fd1f5174d12a0f1d9ad2c88d0ad11ab6aad0ac72b7d9ce621815f8016a9","0"], + "0x33d6E15047E8644F8DDf5CD05d202dfE587DA6E3" : ["0x6155bD199ECcc79Ff4e8B392f6cBD9c9874E8916","0x7071f283424072341f856ac9e947e7ec0eb68719f757a7e785979b6b8717579d","0"], + "0x3483c140EF7F2716460198Ff831a8e53F05F1606" : ["0x6155bD199ECcc79Ff4e8B392f6cBD9c9874E8916","0x8b47ba2a8454ec799cd91646e7ec47168e91fd139b23f017455f3e5898aaba93", "0"], + "0x39A0EbdEE54cB319f4F42141daaBDb6ba25D341A" : ["0x6155bD199ECcc79Ff4e8B392f6cBD9c9874E8916","0x0abcd104777321a82b010357f20887d61247493d89d2e987ff57bcecbde00e1e", "0"], + "0x3e3FE7dBc6B4C189E7128855dD526361c49b40Af" : ["0x6155bD199ECcc79Ff4e8B392f6cBD9c9874E8916","0x3ca7c3e38968823ccb4c78ea688df41356f182ae1d159e4ee608d30d68cef320", "0"], + "0x4F36aAEb18Ab56A4e380241bea6ebF215b9cb12c" : ["0x46cF57508B0565decC0419B833C2dAFa50B132e0", "0x32A0E5828B62AAb932362a4816ae03b860b65e83", "0"], + "0x7637d44c9f2e9cA584a8B5D2EA493012A5cdaEB6" : ["0xC40801b88C835a58e54eEE6679D301ba31a4C72b", "0x32A0E5828B62AAb932362a4816ae03b860b65e83", "0"], + "0x8A1AA86d35b2EE8C9369618E7D7b40000cCD3295" : ["0x6155bD199ECcc79Ff4e8B392f6cBD9c9874E8916", "0xe3262375f45a6e2026b7e7b18c2b807434f2508fe1a2a3dfb493c7df8f4aad6a", "0"], + "0xd497Be005638efCf09F6BFC8DAFBBB0BB72cD991" : ["0x082d16150BF75BB8F2197eEC1d293DbA96c93638", "0x32A0E5828B62AAb932362a4816ae03b860b65e83", "0"] + } } + diff --git a/diffyscan/diffyscan.py b/diffyscan/diffyscan.py index 104d177..9f06a74 100644 --- a/diffyscan/diffyscan.py +++ b/diffyscan/diffyscan.py @@ -6,14 +6,15 @@ import subprocess import tempfile import shutil - + from utils.common import load_config, load_env -from utils.constants import DIFFS_DIR, START_TIME, DEFAULT_CONFIG_PATH +from utils.constants import DIFFS_DIR, START_TIME, DEFAULT_CONFIG_PATH, LOCAL_RPC_URL, REMOTE_RPC_URL from utils.explorer import get_contract_from_explorer from utils.github import get_file_from_github, get_file_from_github_recursive, resolve_dep from utils.helpers import create_dirs from utils.logger import logger from utils.binary_verifier import * +from utils.ganache import ganache __version__ = "0.0.0" @@ -33,13 +34,38 @@ def prettify_solidity(solidity_contract_content: str): with open(github_file_name, "r") as fp: return fp.read() -def run_binary_diff(contract_address, code): - logger.info(f'Started binary checking') - bytecode_from_etherscan, immutables = get_bytecode_from_etherscan(code) +def run_binary_diff(remote_contract_address, contract_source_code, config): + logger.info(f'Started binary checking for {remote_contract_address}') + + contract_creation_code, immutables, is_valid_constructor = get_contract_creation_code_from_etherscan(contract_source_code, config, remote_contract_address) + + if not is_valid_constructor: + logger.error(f'Failed to find constructorArgs, binary diff skipped') + return + + deployer_account = get_account(LOCAL_RPC_URL) + + if (deployer_account is None): + logger.error(f'Failed to receive the account, binary diff skipped') + return + + local_contract_address = deploy_contract(LOCAL_RPC_URL, deployer_account, contract_creation_code) + + if (local_contract_address is None): + logger.error(f'Failed to deploy bytecode to {LOCAL_RPC_URL}, binary diff skipped') + return - bytecode_from_blockchain = get_bytecode_from_blockchain(contract_address) + local_deployed_bytecode = get_bytecode(local_contract_address, LOCAL_RPC_URL) + if (local_deployed_bytecode is None): + logger.error(f'Failed to receive bytecode from {LOCAL_RPC_URL}') + return - match(bytecode_from_blockchain, bytecode_from_etherscan, immutables) + remote_deployed_bytecode = get_bytecode(remote_contract_address, REMOTE_RPC_URL) + if remote_deployed_bytecode is None: + logger.error(f'Failed to receive bytecode from {REMOTE_RPC_URL}') + return + + to_match(local_deployed_bytecode, remote_deployed_bytecode, immutables, remote_contract_address) def run_source_diff(contract_address_from_config, contract_code, config, github_api_token, recursive_parsing=False, prettify=False): logger.divider() @@ -149,13 +175,22 @@ def process_config(path: str, recursive_parsing: bool, unify_formatting: bool, b explorer_token = None if "explorer_token_env_var" in config: explorer_token = load_env(config["explorer_token_env_var"], masked=True, required=False) - contracts = config["contracts"] - for contract_address, contract_name in contracts.items(): - contract_code = get_contract_from_explorer(explorer_token, config["explorer_hostname"], contract_address, contract_name) - run_source_diff(contract_address, contract_code, config, github_api_token, recursive_parsing, unify_formatting) + + try: if (binary_check): - run_binary_diff(contract_address, contract_code) + ganache.start() + + for contract_address, contract_name in contracts.items(): + contract_code = get_contract_from_explorer(explorer_token, config["explorer_hostname"], contract_address, contract_name) + run_source_diff(contract_address, contract_code, config, github_api_token, recursive_parsing, unify_formatting) + if (binary_check): + run_binary_diff(contract_address, contract_code, config) + except KeyboardInterrupt: + logger.info(f'Keyboard interrupt by user') + finally: + ganache.stop() + if (autoclean): builds_dir_path = os.getenv('SOLC_DIR', 'solc') diff --git a/diffyscan/utils/binary_verifier.py b/diffyscan/utils/binary_verifier.py index 0992606..b84ffab 100644 --- a/diffyscan/utils/binary_verifier.py +++ b/diffyscan/utils/binary_verifier.py @@ -4,15 +4,11 @@ import os import stat - from utils.common import fetch, pull, get_solc_native_platform_from_os from utils.helpers import create_dirs from utils.logger import logger - -RPC_URL = os.getenv('RPC_URL', '') -if not RPC_URL: - raise ValueError('RPC_URL variable is not set') - +from utils.encoder import encode_constructor_arguments + def get_compiler_info(platform, required_compiler_version): compilers_list_url = ( f'https://raw.githubusercontent.com/ethereum/solc-bin/gh-pages/{platform}/list.json' @@ -44,7 +40,7 @@ def set_compiler_executable(compiler_path): compiler_file_rights = os.stat(compiler_path) os.chmod(compiler_path, compiler_file_rights.st_mode | stat.S_IEXEC) -def compile_contract(compiler_path, input_settings): +def compile_contracts(compiler_path, input_settings): process = subprocess.run([compiler_path,'--standard-json'], input=input_settings.encode(), capture_output=True, check=True, timeout=30) return json.loads(process.stdout) @@ -55,10 +51,9 @@ def prepare_compiler(platform, build_info, compiler_path): check_compiler_checksum(compiler_binary, valid_checksum) set_compiler_executable(compiler_path) -def get_target_contract(compilation_result, target_contract_name): - contracts = compilation_result['contracts'].values(); +def get_target_compiled_contract(compiled_contracts, target_contract_name): contracts_to_check = [] - for contracts in compilation_result['contracts'].values(): + for contracts in compiled_contracts: for name, contract in contracts.items(): if name == target_contract_name: contracts_to_check.append(contract) @@ -66,13 +61,13 @@ def get_target_contract(compilation_result, target_contract_name): if len(contracts_to_check) != 1: raise ValueError('multiple contracts with the same name') - logger.info('Contracts were successfully compiled. The target contract is ready for matching') + logger.okay('Contracts were successfully compiled. The target contract is ready for matching') return contracts_to_check[0] -def get_bytecode_from_etherscan(code): +def get_contract_creation_code_from_etherscan(contract_code, config, remote_contract_address): platform = get_solc_native_platform_from_os() - build_name = code["compiler"][1:] + build_name = contract_code["compiler"][1:] build_info = get_compiler_info(platform, build_name) compilers_dir_path = os.getenv('SOLC_DIR', 'solc') compiler_path = os.path.join(compilers_dir_path, build_info['path']) @@ -82,41 +77,112 @@ def get_bytecode_from_etherscan(code): if not is_compiler_already_prepared: prepare_compiler(platform, build_info, compiler_path) - input_settings = json.dumps(code["solcInput"]) - contract_name = code['name'] + input_settings = json.dumps(contract_code["solcInput"]) + compiled_contracts = compile_contracts(compiler_path, input_settings)['contracts'].values() - compilation_result = compile_contract(compiler_path, input_settings) + target_contract_name = contract_code['name'] + target_compiled_contract = get_target_compiled_contract(compiled_contracts, target_contract_name) - target_contract = get_target_contract(compilation_result, contract_name) - - expected_bytecode = target_contract['evm']['deployedBytecode']['object'] - + contract_creation_code = f'0x{target_compiled_contract['evm']['bytecode']['object']}' immutables = {} - if ('immutableReferences' in target_contract['evm']['deployedBytecode']): - immutable_references = target_contract['evm']['deployedBytecode']['immutableReferences'] + if ('immutableReferences' in target_compiled_contract['evm']['deployedBytecode']): + immutable_references = target_compiled_contract['evm']['deployedBytecode']['immutableReferences'] for refs in immutable_references.values(): for ref in refs: immutables[ref['start']] = ref['length'] - - return expected_bytecode, immutables + constructor_abi = None + try: + constructor_abi = [entry["inputs"] for entry in target_compiled_contract['abi'] if entry["type"] == "constructor"][0] + except IndexError: + logger.info(f'Constructor in ABI not found') + return contract_creation_code, immutables, True + + constructor_calldata = None + + if (remote_contract_address in config["ConstructorArgs"]): + constructor_args = config["ConstructorArgs"][remote_contract_address] + if constructor_args: + constructor_calldata = encode_constructor_arguments(constructor_abi, constructor_args) + return contract_creation_code+constructor_calldata, immutables, True -def get_bytecode_from_blockchain(contract_address): - logger.info(f'Retrieving the bytecode by contract address "{contract_address}" from the blockchain...') + logger.warn(f'Constructor in ABI found, but config not found (contract {target_contract_name})') + return contract_creation_code, immutables, True + +def get_bytecode(contract_address, rpc_url): + logger.info(f'Receiving the bytecode from "{rpc_url}" ...') payload = json.dumps({'id': 1, 'jsonrpc': '2.0', 'method': 'eth_getCode', 'params': [contract_address, 'latest']}) - sources_url_response = pull(RPC_URL, payload) - sources_url_response_in_json = sources_url_response.json() - if 'result' in sources_url_response_in_json: - bytecode = sources_url_response_in_json['result'] - else: - raise ValueError(f'Failed to find section "result" in response') + sources_url_response_in_json = pull(rpc_url, payload).json() + if 'result' not in sources_url_response_in_json or sources_url_response_in_json['result'] == '0x': + return None + + logger.okay(f'Bytecode was successfully received') + return sources_url_response_in_json['result'] + +def get_chain_id(rpc_url): + payload = json.dumps({'id': 1, 'jsonrpc': '2.0', 'method': 'eth_chainId', 'params': []}) + + response = pull(rpc_url, payload).json() + if 'error' in response: + logger.error(f'Failed to received chainId: {response['error']['message']}') + return 1 + return int (response['result'],16) + +def get_account(rpc_url): + logger.info(f'Receiving the account from "{rpc_url}" ...') + + payload = json.dumps({'id': 42, 'jsonrpc': '2.0', 'method': 'eth_accounts', 'params': []}) - logger.okay(f'Bytecode was successfully fetched') + account_address_response = pull(rpc_url, payload).json() - return bytecode + if 'result' not in account_address_response: + return None + + logger.okay(f'The account was successfully received') + return account_address_response['result'][0] + +def deploy_contract(rpc_url, deployer, data): + logger.info(f'Trying to deploy transaction to "{rpc_url}" ...') + + payload_sendTransaction = json.dumps({ + "jsonrpc": "2.0", + "method": "eth_sendTransaction", + "params": [{ + "from": deployer, + "gas": 9000000, + "input": data + }], + "id": 1 + }) + response_sendTransaction = pull(rpc_url, payload_sendTransaction).json() + + if 'error' in response_sendTransaction: + logger.warn(f'Failed to deploy transaction: {response_sendTransaction['error']['message']}') + return None + + logger.okay(f'Transaction was successfully deployed') + tx_hash = response_sendTransaction['result'] + + payload_getTransactionReceipt = json.dumps({'id': 1, 'jsonrpc': '2.0', 'method': 'eth_getTransactionReceipt', 'params':[tx_hash]}) + response_getTransactionReceipt = pull(rpc_url, payload_getTransactionReceipt).json() + + if 'result' not in response_getTransactionReceipt or \ + 'contractAddress' not in response_getTransactionReceipt['result'] or \ + 'status' not in response_getTransactionReceipt['result'] : + logger.error(f'Failed to received transaction receipt') + return None + + if response_getTransactionReceipt['result']['status'] != '0x1': + logger.error(f'Failed to received transaction receipt. Transaction has been reverted (status 0x0). Input missmatch?') + return None + + contract_address = response_getTransactionReceipt['result']['contractAddress'] + + return contract_address + OPCODES = { 0x00: 'STOP', 0x01: 'ADD', 0x02: 'MUL', 0x03: 'SUB', 0x04: 'DIV', 0x05: 'SDIV', @@ -151,25 +217,9 @@ def get_bytecode_from_blockchain(contract_address): 0xF0: 'CREATE', 0xF1: 'CALL', 0xF2: 'CALLCODE', 0xF3: 'RETURN', 0xF4: 'DELEGATECALL', 0xF5: 'CREATE2', 0xFA: 'STATICCALL', 0xFD: 'REVERT', 0xFE: 'INVALID', 0xFF: 'SELFDESTRUCT', } - -def parse(bytecode): - instructions = [] - buffer = bytearray.fromhex(bytecode[2:] if bytecode.startswith('0x') else bytecode) - i = 0 - while i < len(buffer): - opcode = buffer[i] - length = 1 + (opcode >= 0x5f and opcode <= 0x7f) * (opcode - 0x5f) - instructions.append({ - 'start': i, - 'length': length, - 'op': {'name': OPCODES.get(opcode, 'INVALID'), 'code': opcode}, - 'bytecode': buffer[i:i + length].hex() - }) - i += length - return instructions - -def match(actualBytecode, expectedBytecode, immutables): - logger.info('Comparing actual code with the expected one:') + +def to_match(actualBytecode, expectedBytecode, immutables, remote_contract_address): + logger.info('Comparing actual code with the expected one...') actualInstructions = parse(actualBytecode) expectedInstructions = parse(expectedBytecode) @@ -181,13 +231,13 @@ def match(actualBytecode, expectedBytecode, immutables): expected = expectedInstructions[i] if i < len(expectedInstructions) else None if not actual and not expected: raise ValueError('Invalid instructions data') - elif actual.get('bytecode') != expected.get('bytecode'): + elif (actual is not None) and (actual.get('bytecode') != expected.get('bytecode')): differences.append(i) if not differences: - logger.okay(f'Bytecodes are fully matched') + logger.okay(f'Bytecodes are fully matched (contract {remote_contract_address})') return - logger.warn(f'Bytecodes have differences') + logger.warn(f'Bytecodes have differences contract {remote_contract_address})') nearLinesCount = 3 checkpoints = {0, *differences} diff --git a/diffyscan/utils/constants.py b/diffyscan/utils/constants.py index 757e5a2..10918b6 100644 --- a/diffyscan/utils/constants.py +++ b/diffyscan/utils/constants.py @@ -1,4 +1,5 @@ import time +import os DIGEST_DIR = "digest" START_TIME = time.time() @@ -6,3 +7,15 @@ DIFFS_DIR = f"{DIGEST_DIR}/{START_TIME_INT}/diffs" LOGS_PATH = f"{DIGEST_DIR}/{START_TIME_INT}/logs.txt" DEFAULT_CONFIG_PATH = "config.json" + +REMOTE_RPC_URL = os.getenv('REMOTE_RPC_URL', '') +if not REMOTE_RPC_URL: + raise ValueError('REMOTE_RPC_URL variable is not set') + +REMOTE_EXPLORER_NAME = os.getenv('REMOTE_EXPLORER_NAME', '') +if REMOTE_EXPLORER_NAME == '': + raise ValueError('REMOTE_EXPLORER_NAME variable is not set') + +LOCAL_RPC_URL = os.getenv('LOCAL_RPC_URL', '') +if not LOCAL_RPC_URL: + raise ValueError('LOCAL_RPC_URL variable is not set') diff --git a/diffyscan/utils/encoder.py b/diffyscan/utils/encoder.py new file mode 100644 index 0000000..1b3076b --- /dev/null +++ b/diffyscan/utils/encoder.py @@ -0,0 +1,96 @@ +from utils.logger import logger + +def encode_address(address): + number = int(address, 16) + formatted_address = f"{number:064x}" + return formatted_address + +def encode_uint256(number): + return format(number, "064x") + +def encode_bytes32(data): + return data.replace("0x", "") + +def encode_bytes(data): + bytes_str = data.lstrip("0x") + data_length = len(bytes_str) // 2 + encoded_length = 0 + if data_length > 0: + encoded_length = ((len(bytes_str) - 1) // 64 + 1) * 64 + bytes_str += "0" * (encoded_length - len(bytes_str)) + return format(data_length, "064x") + bytes_str + +def encode_tuple(types, args): + args_length = len(types) + encoded_offsets = "" + encoded_data = "" + for arg_index in range(args_length): + arg_type = types[arg_index] + arg_value = args[arg_index] + if arg_type == "address": + encoded_offsets += encode_address(arg_value) + elif arg_type == "uint256" or arg_type == "bool" or arg_type == 'uint8': + encoded_offsets += encode_uint256(arg_value) + elif arg_type == "bytes32": + encoded_offsets += encode_bytes32(arg_value) + elif arg_type == "address[]" and not arg_value: + encoded_data += '0' * 64 + offset = format((arg_index + args_length) * 32, "064x") + encoded_offsets += offset + else: + logger.warn(f"Unknown constructor argument type '{arg_type}', use --constructor-calldata instead") + return encoded_offsets+encoded_data + +def to_hex_with_alignment(value): + return format(value, "064x") + +def encode_constructor_arguments(constructor_abi, constructor_config_args): + arg_length = len(constructor_abi) + + logger.info(f"Constructor args types: {[arg['type'] for arg in constructor_abi]}") + + constructor_calldata = "" + compl_data = [] + if arg_length > 0: + for argument_index in range(arg_length): + arg_type = constructor_abi[argument_index]["type"] + arg_value = constructor_config_args[argument_index] + if arg_type == "address": + constructor_calldata += encode_address(arg_value) + elif arg_type == "uint256" or arg_type == "bool" or arg_type == 'uint8': + constructor_calldata += encode_uint256(arg_value) + elif arg_type == "bytes32": + constructor_calldata += encode_bytes32(arg_value) + elif arg_type == "bytes": + data = format((argument_index+1)*32, "064x") + constructor_calldata+=data + data2 = encode_bytes(arg_value) + compl_data.append(data2) + elif arg_type == "string": + offset_arg = to_hex_with_alignment((arg_length + len(compl_data))*32) + constructor_calldata += offset_arg + length_arg = to_hex_with_alignment(len(arg_value.encode('utf-8'))) + hex_text = arg_value.encode('utf-8').hex().ljust(64, '0') + compl_data.append(length_arg) + compl_data.append(hex_text) + elif arg_type == "tuple": + args_tuple_types = [component["type"] for component in constructor_abi[argument_index]["components"]] + if len(args_tuple_types) and args_tuple_types[0] == "address[]": + dynamic_type_length = format((len(constructor_calldata)//64 + 1) *32, "064x") + constructor_calldata += dynamic_type_length + logger.info(f'dynamic_type_length {dynamic_type_length}') + compl_data.append(encode_tuple(args_tuple_types, arg_value)) + else: + constructor_calldata += encode_tuple(args_tuple_types, arg_value) + elif arg_type.endswith("[]"): + data = format((argument_index+1)*32, "064x") + constructor_calldata+=data + data2 = encode_bytes(arg_value) + compl_data.append(data2) + else: + raise ValueError(f"Unknown constructor argument type: {arg_type}") + for data in compl_data: + constructor_calldata += data + + return constructor_calldata + diff --git a/diffyscan/utils/ganache.py b/diffyscan/utils/ganache.py new file mode 100644 index 0000000..0e8b317 --- /dev/null +++ b/diffyscan/utils/ganache.py @@ -0,0 +1,34 @@ +import os +import subprocess +import signal + +from urllib.parse import urlparse +from utils.logger import logger +from utils.constants import LOCAL_RPC_URL, REMOTE_RPC_URL +from utils.binary_verifier import get_chain_id +class Ganache: + sub_process = None + + def __init__(self): + pass + + def start(self): + + local_node_command = ( + f'ganache --host {urlparse(LOCAL_RPC_URL).hostname} ' \ + f'--port {urlparse(LOCAL_RPC_URL).port} ' \ + f'--chain.chainId {get_chain_id (REMOTE_RPC_URL)} ' \ + f'--fork.url {REMOTE_RPC_URL} ' \ + f'-l 92000000 --hardfork istanbul -d ' + ) + + self.sub_process = subprocess.Popen("exec "+ local_node_command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + logger.info(f'Ganache successfully started: "{local_node_command}", PID {self.sub_process.pid}') + + def stop(self): + if self.sub_process is not None and self.sub_process.poll() is None: + os.kill(self.sub_process.pid, signal.SIGTERM) + logger.info(f'Ganache stopped, PID {self.sub_process.pid}') + +ganache = Ganache() \ No newline at end of file