def prettify(exp, rem_bool=False, parentheses=True, top_level=False, add_color=False): col = partial(colorize, add_color=add_color) pret = partial(prettify, add_color=add_color, parentheses=False) if rem_bool: exp = simplify_bool(exp) if opcode(exp) == 'bool': return prettify(exp, rem_bool=rem_bool, parentheses=parentheses, top_level=top_level, add_color=add_color) if type(exp) == int and exp % (24 * 3600) == 0 and exp > 24 * 3600: exp = ('mul', exp//3600, 24, 3600) if type(exp) == int and exp % 3600 == 0 and exp > 3600: exp = ('mul', exp//3600, 3600) # also tried return col('seconds(', COLOR_GRAY) + '1 hour' + col(')', COLOR_GRAY) # but seemed less intuitive, e.g. 0xf64B584972FE6055a770477670208d737Fff282f calcMaxWithdraw # and 3600 every programmer should know, by heart, means 1 hour :) # # also, not tackling single minutes because too often they are not time related if type(exp) in (int, float): return pretty_num(exp, add_color) if opcode(exp) in precompiled.values(): return f'{exp[0]}({pret(exp[1])})' if exp ~ ('arr', int:num, ('mask_shl', _, _, _, str:s)) \ and len(s) == num+2: return s
def handle_jumps(self, trace, line, condition): i, op = line[0], line[1] stack = self.stack if '--explain' in sys.argv and op in ('jump', 'jumpi', 'selfdestruct', 'stop', 'return', 'invalid', 'assert_fail', 'revert'): trace.append(C.asm(f' {stack}')) trace.append('') trace.append(f'[{line[0]}] {C.asm(op)}') if op == 'jump': target = stack.pop() n = Node(self, start=target, safe=False, stack=tuple(self.stack.stack), condition=condition) trace.append(('jump', n)) return trace if op == 'jumpi': target = stack.pop() if_condition = simplify_bool(stack.pop()) tuple_stack = tuple(self.stack.stack) n_true = Node(self, start=target, safe=False, stack=tuple_stack, condition=if_condition) n_false = Node(self, start=self.loader.next_line(i), safe=True, stack=tuple_stack, condition=is_zero(if_condition)) if self.just_fdests: if if_condition ~ ('eq', int:fx_hash, :is_cd) and str(('cd', 0)) in str(is_cd): n_true.trace=[('funccall', fx_hash, target)] if if_condition ~ ('eq', :is_cd, int:fx_hash) and str(('cd', 0)) in str(is_cd): n_true.trace=[('funccall', fx_hash, target)]
def cleanup_ors(path): assert type(path) == list ret = [] idx = 0 while idx < len(path): if type(path[idx]) == list: path = path[:idx] + path[idx] + path[idx + 1 :] line = path[idx] if opcode(line) != "or": ret.append(line) elif len(line) == 2: # one-sided or # clean up the inside, skip the next line in the main path condition = line[1][0] line = ("or", cleanup_ors(line[1])) ret.append(line) idx += 1 elif len(line) == 3: # two-sided or if len(line[1]) == 1: assert comp_bool(simplify_bool(line[1][0]), is_zero(line[2][0])) line = ("or", cleanup_ors(line[2])) ret.append(line) else: assert comp_bool(is_zero(line[1][0]), simplify_bool(line[2][0])) line = ("or", cleanup_ors(line[1]), cleanup_ors(line[2][1:])) ret.append(line) else: # three-sided ors? madness! assert False idx += 1 return ret
def handle_jumps(self, trace, line, condition): i, op = line[0], line[1] stack = self.stack if "--explain" in sys.argv and op in ( "jump", "jumpi", "selfdestruct", "stop", "return", "invalid", "assert_fail", "revert", ): trace.append(C.asm(f" {stack}")) trace.append("") trace.append(f"[{line[0]}] {C.asm(op)}") if op in ( "jump", "jumpi", "selfdestruct", "stop", "return", "invalid", "assert_fail", "revert", ): logger.debug("[%s] %s", i, op) if op == "jump": target = stack.pop() n = Node( self, start=target, safe=False, stack=tuple(self.stack.stack), condition=condition, ) trace.append(("jump", n)) return trace elif op == "jumpi": target = stack.pop() if_condition = simplify_bool(stack.pop()) tuple_stack = tuple(self.stack.stack) n_true = Node( self, start=target, safe=False, stack=tuple_stack, condition=if_condition, ) n_false = Node( self, start=self.loader.next_line(i), safe=True, stack=tuple_stack, condition=is_zero(if_condition), ) if self.just_fdests: if ((m := match(if_condition, ("eq", ":fx_hash", ":is_cd"))) and str(("cd", 0)) in str(m.is_cd) and isinstance(m.fx_hash, int)): n_true.trace = [("funccall", m.fx_hash, target, tuple_stack)] if ((m := match(if_condition, ("eq", ":is_cd", ":fx_hash"))) and str(("cd", 0)) in str(m.is_cd) and isinstance(m.fx_hash, int)): n_true.trace = [("funccall", m.fx_hash, target, tuple_stack)]
def analyse(self): assert len(self.trace) > 0 def find_returns(exp): if opcode(exp) == 'return': return [exp] else: return [] exp_text = [] self.returns = find_f_list(self.trace, find_returns) exp_text.append(('possible return values', prettify(self.returns))) first = self.trace[0] if opcode(first) == 'if' and simplify_bool(first[1]) == 'callvalue' \ and (first[2][0] == ('revert', 0) or opcode(first[2][0]) == 'invalid'): self.trace = self.trace[0][3] self.payable = False elif opcode(first) == 'if' and simplify_bool(first[1]) == ('iszero', 'callvalue') \ and (first[3][0] == ('revert', 0) or opcode(first[3][0]) == 'invalid'): self.trace = self.trace[0][2] self.payable = False else: self.payable = True exp_text.append(('payable', self.payable)) self.read_only = True for op in ['store', 'selfdestruct', 'call', 'delegatecall', 'codecall', 'create']: if f"'{op}'" in str(self.trace): self.read_only = False exp_text.append(('read_only', self.read_only)) ''' const func detection ''' self.const = self.read_only for exp in ['storage', 'calldata', 'calldataload', 'store', 'cd']: if exp in str(self.trace) or len(self.returns)!=1: self.const = False if self.const: self.const = self.returns[0] if len(self.const) == 3 and opcode(self.const[2]) == 'data': self.const = self.const[2] if len(self.const) == 3 and opcode(self.const[2]) == 'mask_shl': self.const = self.const[2] if len(self.const) == 3 and type(self.const[2]) == int: self.const = self.const[2] else: self.const = None if self.const: exp_text.append(('const', self.const)) ''' getter detection ''' self.getter = None self.simplify_string_getter_from_storage() if self.const is None and \ self.read_only and \ len(self.returns) == 1: ret = self.returns[0][1] if ret ~ ('bool', ('storage', _, _, :loc)):
def analyse(self): assert len(self.trace) > 0 def find_returns(exp): if opcode(exp) == "return": return [exp] else: return [] exp_text = [] self.returns = find_f_list(self.trace, find_returns) exp_text.append(("possible return values", prettify(self.returns))) first = self.trace[0] if (opcode(first) == "if" and simplify_bool(first[1]) == "callvalue" and (first[2][0] == ("revert", 0) or opcode(first[2][0]) == "invalid")): self.trace = self.trace[0][3] self.payable = False elif (opcode(first) == "if" and simplify_bool(first[1]) == ("iszero", "callvalue") and (first[3][0] == ("revert", 0) or opcode(first[3][0]) == "invalid")): self.trace = self.trace[0][2] self.payable = False else: self.payable = True exp_text.append(("payable", self.payable)) self.read_only = True for op in [ "store", "selfdestruct", "call", "delegatecall", "codecall", "create", ]: if f"'{op}'" in str(self.trace): self.read_only = False exp_text.append(("read_only", self.read_only)) """ const func detection """ self.const = self.read_only for exp in ["storage", "calldata", "calldataload", "store", "cd"]: if exp in str(self.trace) or len(self.returns) != 1: self.const = False if self.const: self.const = self.returns[0] if len(self.const) == 3 and opcode(self.const[2]) == "data": self.const = self.const[2] if len(self.const) == 3 and opcode(self.const[2]) == "mask_shl": self.const = self.const[2] if len(self.const) == 3 and type(self.const[2]) == int: self.const = self.const[2] else: self.const = None if self.const: exp_text.append(("const", self.const)) """ getter detection """ self.getter = None self.simplify_string_getter_from_storage() if self.const is None and self.read_only and len(self.returns) == 1: ret = self.returns[0][1] if match(ret, ("bool", ("storage", Any, Any, ":loc"))): self.getter = ( ret # we have to be careful when using this for naming purposes, ) # because sometimes the storage can refer to array length elif opcode(ret) == "mask_shl" and opcode(ret[4]) == "storage": self.getter = ret[4] elif opcode(ret) == "storage": self.getter = ret elif opcode(ret) == "data": terms = ret[1:] # for structs, we check if all the parts of the struct are storage from the same # location. if so, we return the location number t0 = terms[ 0] # 0xFAFfea71A6da719D6CAfCF7F52eA04Eb643F6De2 - documents if m := match(t0, ("storage", 256, 0, ":loc")): loc = m.loc for e in terms[1:]: if not match(e, ("storage", 256, 0, ("add", Any, loc))): break else: self.getter = t0 # kitties getKitten - with more cases this and the above could be uniformed if self.getter is None: prev_loc = -1 for e in terms: def l2(x): if m := match(x, ("sha3", ("data", Any, ":l"))): if type(m.l) == int and m.l < 1000: return m.l if (opcode(x) == "sha3" and type(x[1]) == int and x[1] < 1000): return x[1] return None loc = find_f(e, l2) if not loc or (prev_loc != -1 and prev_loc != loc): break prev_loc = loc else: self.getter = ("struct", ("loc", loc))