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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 147 additions & 63 deletions solc_json_parser/ast_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,87 @@

# Installable version supported on linux-amd64
INSTALLABLE_VERSION = [
"0.4.10", "0.4.11", "0.4.12", "0.4.13", "0.4.14", "0.4.15", "0.4.16", "0.4.17", "0.4.18", "0.4.19", "0.4.20", "0.4.21", "0.4.22", "0.4.23", "0.4.24", "0.4.25", "0.4.26",
"0.5.0", "0.5.1", "0.5.2", "0.5.3", "0.5.4", "0.5.5", "0.5.6", "0.5.7", "0.5.8", "0.5.9", "0.5.10", "0.5.11", "0.5.12", "0.5.13", "0.5.14", "0.5.15", "0.5.16", "0.5.17",
"0.6.0", "0.6.1", "0.6.2", "0.6.3", "0.6.4", "0.6.5", "0.6.6", "0.6.7", "0.6.8", "0.6.9", "0.6.10", "0.6.11", "0.6.12",
"0.7.0", "0.7.1", "0.7.2", "0.7.3", "0.7.4", "0.7.5", "0.7.6",
"0.8.0", "0.8.1", "0.8.2", "0.8.3", "0.8.4", "0.8.5", "0.8.6", "0.8.7", "0.8.8", "0.8.9", "0.8.10", "0.8.11", "0.8.12", "0.8.13", "0.8.14", "0.8.15", "0.8.16", "0.8.17", "0.8.18", "0.8.19"
"0.4.10",
"0.4.11",
"0.4.12",
"0.4.13",
"0.4.14",
"0.4.15",
"0.4.16",
"0.4.17",
"0.4.18",
"0.4.19",
"0.4.20",
"0.4.21",
"0.4.22",
"0.4.23",
"0.4.24",
"0.4.25",
"0.4.26",
"0.5.0",
"0.5.1",
"0.5.2",
"0.5.3",
"0.5.4",
"0.5.5",
"0.5.6",
"0.5.7",
"0.5.8",
"0.5.9",
"0.5.10",
"0.5.11",
"0.5.12",
"0.5.13",
"0.5.14",
"0.5.15",
"0.5.16",
"0.5.17",
"0.6.0",
"0.6.1",
"0.6.2",
"0.6.3",
"0.6.4",
"0.6.5",
"0.6.6",
"0.6.7",
"0.6.8",
"0.6.9",
"0.6.10",
"0.6.11",
"0.6.12",
"0.7.0",
"0.7.1",
"0.7.2",
"0.7.3",
"0.7.4",
"0.7.5",
"0.7.6",
"0.8.0",
"0.8.1",
"0.8.2",
"0.8.3",
"0.8.4",
"0.8.5",
"0.8.6",
"0.8.7",
"0.8.8",
"0.8.9",
"0.8.10",
"0.8.11",
"0.8.12",
"0.8.13",
"0.8.14",
"0.8.15",
"0.8.16",
"0.8.17",
"0.8.18",
"0.8.19",
]


INSTALLABLE_VERSION = sorted([Version(v) for v in INSTALLABLE_VERSION])

INTERFACE_OR_LIB_KIND = set(['interface', 'library'])
INTERFACE_OR_LIB_KIND = set(["interface", "library"])

DEPLOY_START_OPCODES = [
# For solidity 0.4.23 and above
Expand All @@ -48,21 +118,23 @@
],
]


def keccak256(s: str) -> str:
k = keccak.new(digest_bits=256)
k.update(s.encode())
return k.hexdigest()


def get_by_index(lst: Union[List, Tuple], idx: int):
'''Get by index from a list, returns None if the index is out of range '''
"""Get by index from a list, returns None if the index is out of range"""
if len(lst) > idx:
return lst[idx]
return None


def get_in(d, key: Any, *nkeys) -> Any:
'''Get in nested datastructure by keys. Only dictionary, tuple and
list are supported'''
"""Get in nested datastructure by keys. Only dictionary, tuple and
list are supported"""
try:
nd = d.get(key)
except Exception:
Expand All @@ -74,6 +146,7 @@ def get_in(d, key: Any, *nkeys) -> Any:
return get_in(nd, *nkeys)
return nd


def assoc_in(d, keys, value):
"""Associates a value with a sequence of keys in a nested dictionary"""
key = keys[0]
Expand All @@ -87,36 +160,40 @@ def assoc_in(d, keys, value):


def get_all_installable_versions():
'''
"""
Returns a cached list of solc versions available for install,
version list is sorted in ascending order
'''
"""
return INSTALLABLE_VERSION


def version_str_from_line(line) -> Optional[str]:
'''
"""
Extract solc version string from input line
'''
if line.strip().startswith('pragma') and 'solidity' in line:
ver = line.strip().split(maxsplit=2)[-1].split(';', maxsplit=1)[0]
if 'solidity' in ver:
ver = ver.split('solidity', maxsplit=1)[-1]
ver = re.sub(r'([\^>=<~]+)\s+', r'\1', ver)
return re.sub(r'(\.0+)', '.0', ver)
"""
if line.strip().startswith("pragma") and "solidity" in line:
ver = line.strip().split(maxsplit=2)[-1].split(";", maxsplit=1)[0]
if "solidity" in ver:
ver = ver.split("solidity", maxsplit=1)[-1]
ver = re.sub(r"([\^>=<~]+)\s+", r"\1", ver)
return re.sub(r"(\.0+)", ".0", ver)
return None


def version_str_from_source(source_or_source_file: str) -> Optional[str]:
inputs = source_or_source_file.split('\n') if '\n' in source_or_source_file else open(source_or_source_file, 'r')
inputs = source_or_source_file.split("\n") if "\n" in source_or_source_file else open(source_or_source_file, "r")

# Get version part from `pragma solidity ***;` lines
versions = [version_str_from_line(line) for line in inputs if line.strip().startswith('pragma') and 'solidity' in line]
versions = [
version_str_from_line(line) for line in inputs if line.strip().startswith("pragma") and "solidity" in line
]

if not versions:
logging.warning('No pragma directive found in source code')
logging.warning("No pragma directive found in source code")
return None

return ' '.join(set(versions))
return " ".join(set(versions))


def get_solc_candidates(source_or_source_file: str) -> List[str]:
merged_version = version_str_from_source(source_or_source_file)
Expand All @@ -127,29 +204,30 @@ def get_solc_candidates(source_or_source_file: str) -> List[str]:
spec = semantic_version.NpmSpec(merged_version)
return [str(v) for v in spec.filter(get_all_installable_versions())]


def detect_solc_version(source_or_source_file: str) -> Optional[str]:
'''
"""
Detect solc version from a flatten source. Input can be a single file or source code string
'''
"""
versions = get_solc_candidates(source_or_source_file)
return versions[-1] if versions else None


def symbols_to_ids_from_ast_v8(ast: dict) -> Dict[str, int]:
syms = [c['ast']['exportedSymbols'] for c in ast.values()]
syms = [c["ast"]["exportedSymbols"] for c in ast.values()]
return {k: v[0] for m in syms for k, v in m.items()}


def symbols_to_ids_from_ast_v7(ast: Dict[Any, Any]) -> Dict[str, int]:
syms = [c['ast']['attributes']['exportedSymbols'] for c in ast.values()]
syms = [c["ast"]["attributes"]["exportedSymbols"] for c in ast.values()]
return {k: v[0] for m in syms for k, v in m.items()}


def find_next_version_in_candidates(current_version: str, solc_candidates: List[str]) -> Tuple[str, List[str]]:
"""Try to get the next version"""
ver = Version(current_version)
try_next_version = Version(major=ver.major, minor= ver.minor - 1, patch=0)
print(f'try_next_version: {try_next_version} solc_candidates: {solc_candidates}')
try_next_version = Version(major=ver.major, minor=ver.minor - 1, patch=0)
print(f"try_next_version: {try_next_version} solc_candidates: {solc_candidates}")
version = None
# print(f'try_next_version: {try_next_version} solc_candidates: {solc_candidates}')
if str(try_next_version) in solc_candidates:
Expand All @@ -160,12 +238,13 @@ def find_next_version_in_candidates(current_version: str, solc_candidates: List[
version = str(solc_candidates[-1])
solc_candidates = solc_candidates[:-1]
if not version:
raise ValueError(f'No next solc version available for {current_version}')
raise ValueError(f"No next solc version available for {current_version}")
return version, solc_candidates


def skip_deploys(opcodes, deploy_sig_idx=0):
if deploy_sig_idx >= len(DEPLOY_START_OPCODES):
raise SolidityAstError(f'Code deploy sequence not found in opcodes: {opcodes}')
raise SolidityAstError(f"Code deploy sequence not found in opcodes: {opcodes}")
offset = 1
match_idx = 0
deploy_start_sequence = DEPLOY_START_OPCODES[deploy_sig_idx]
Expand All @@ -180,32 +259,32 @@ def skip_deploys(opcodes, deploy_sig_idx=0):
offset += 1

if offset < len(opcodes):
return opcodes[offset - len(deploy_start_sequence) + 1:]
return skip_deploys(opcodes, deploy_sig_idx+1)
return opcodes[offset - len(deploy_start_sequence) + 1 :]
return skip_deploys(opcodes, deploy_sig_idx + 1)


def parse_src_mapping(srcmap: str):
def _reduce_fn(accumulator, current_value):
last, *tlist = accumulator
return [
{
's': int(current_value['s'] or last['s']),
'l': int(current_value['l'] or last['l']),
'f': int(current_value['f'] or last['f']),
"s": int(current_value["s"] or last["s"]),
"l": int(current_value["l"] or last["l"]),
"f": int(current_value["f"] or last["f"]),
},
last,
*tlist
*tlist,
]

parsed = srcmap.split(";")
parsed = [l.split(':') for l in parsed]
parsed = [l.split(":") for l in parsed]
t = []
for l in parsed:
if len(l) >= 3:
t.append(l[:3])
else:
t.append(l + [None] * (3 - len(l)))
parsed = [{'s': s if s != "" else None, 'l': l, 'f': f} for s, l, f in t]
parsed = [{"s": s if s != "" else None, "l": l, "f": f} for s, l, f in t]
parsed = reduce(_reduce_fn, parsed, [{}])
parsed = list(reversed(parsed[:-1]))
return parsed
Expand All @@ -215,41 +294,41 @@ def process_literal_node(literals_nodes, only_value):
def _process_other_literal_node(literal_node, literals, only_value):
try:
if only_value:
literals['other'].add(literal_node.str_value)
literals["other"].add(literal_node.str_value)
else:
literals['other'].add(literal_node)
literals["other"].add(literal_node)
except AttributeError:
pass

literals = dict(number=set(), string=set(), address=set(), other=set())
for literal in literals_nodes:
try:
if literal.sub_type is None and literal.token_type == 'number':
if literal.sub_type is None and literal.token_type == "number":
if only_value and literal.str_value.isdecimal():
literals['number'].add(int(literal.str_value))
literals["number"].add(int(literal.str_value))
else:
literals['number'].add(literal)
literals["number"].add(literal)
elif literal.sub_type.startswith("address"):
if only_value:
literals['address'].add(literal.str_value)
literals["address"].add(literal.str_value)
else:
literals['address'].add(literal)
literals["address"].add(literal)
elif literal.sub_type.startswith("int"):
if only_value:
if literal.str_value.startswith('0x'):
literals['number'].add(int(literal.str_value, 16))
if literal.str_value.startswith("0x"):
literals["number"].add(int(literal.str_value, 16))
elif literal.sub_type.split()[1].isdecimal():
literals['number'].add(int(literal.sub_type.split()[1]))
literals["number"].add(int(literal.sub_type.split()[1]))
else:
literals['number'].add(int(literal.str_value))
literals["number"].add(int(literal.str_value))
else:
literals['number'].add(literal)
literals["number"].add(literal)
# check if string in token_type, ignore case
elif literal.sub_type.startswith("literal_string"):
if only_value:
literals['string'].add(literal.str_value)
literals["string"].add(literal.str_value)
else:
literals['string'].add(literal)
literals["string"].add(literal)
elif literal.sub_type.startswith("bool"):
continue
else:
Expand All @@ -260,29 +339,34 @@ def _process_other_literal_node(literal_node, literals, only_value):
return literals


def record_jumps(opcode: str, code: list[Dict[str, Any]], idx: int, pc: int, seen_targets: set[int], pc2opcode: Dict[int, str]) -> set[int]:
def record_jumps(
opcode: str, code: list[Dict[str, Any]], idx: int, pc: int, seen_targets: set[int], pc2opcode: Dict[int, str]
) -> set[int]:
pc2opcode[pc] = opcode
if opcode == 'JUMPI':
seen_targets.add(int(code[idx-1].get('value')))
if opcode == "JUMPI":
seen_targets.add(int(code[idx - 1].get("value")))
seen_targets.add(int(pc + 1))

return seen_targets


def solc_bin(ver: str):
'''
"""
Get solc bin full path by version. By default it checks the solcx installion path.
You can also override this function to use solc from https://github.com/ethereum/solc-bin/tree/gh-pages/linux-amd64
'''
return os.path.expanduser(f'~/.solcx/solc-v{ver}')
"""
return os.path.expanduser(f"~/.solcx/solc-v{ver}")


version_pattern = r"v(\d+\.\d+\.\d+)"

version_pattern = r'v(\d+\.\d+\.\d+)'

def simplify_version(s):
'''
"""
Convert a version with sha to a simple version
Example: v0.8.13+commit.abaa5c0e -> 0.8.13
'''
match = re.search(version_pattern, s or '')
"""
match = re.search(version_pattern, s or "")
if match:
extracted_version = match.group(1)
return extracted_version
Expand Down
Loading