From 0a8cd5d57f5c4478274dc45a507965bcb986da5b Mon Sep 17 00:00:00 2001 From: luongnt95 Date: Mon, 28 Aug 2017 15:01:47 +0700 Subject: [PATCH 1/3] Refactor the codebase --- oyente/symExec.py | 314 ++++++++++++++++++++++++---------------------- 1 file changed, 164 insertions(+), 150 deletions(-) diff --git a/oyente/symExec.py b/oyente/symExec.py index 4576dbac..a62e4c5a 100755 --- a/oyente/symExec.py +++ b/oyente/symExec.py @@ -133,116 +133,6 @@ def compare_storage_and_gas_unit_test(global_state, analysis): test_status = unit_test.compare_with_symExec_result(global_state, analysis) exit(test_status) -def handler(signum, frame): - if global_params.UNIT_TEST == 2 or global_params.UNIT_TEST == 3: - exit(TIME_OUT) - detect_bugs() - raise Exception("timeout") - -def detect_bugs(): - global results - global source_map - global visited_pcs - global global_problematic_pcs - - percentage_of_opcodes_covered = float(len(visited_pcs)) / len(instructions.keys()) * 100 - log.info("\t EVM code coverage: \t %s%%", round(percentage_of_opcodes_covered, 1)) - results["perct_evm_covered"] = str(round(percentage_of_opcodes_covered, 1)) - - log.debug("Checking for Callstack attack...") - run_callstack_attack() - - if global_params.REPORT_MODE: - rfile.write(str(total_no_of_paths) + "\n") - detect_money_concurrency() - detect_time_dependency() - stop = time.time() - if global_params.REPORT_MODE: - rfile.write(str(stop-start)) - rfile.close() - if global_params.DATA_FLOW: - detect_data_concurrency() - detect_data_money_concurrency() - log.debug("Results for Reentrancy Bug: " + str(reentrancy_all_paths)) - reentrancy_bug_found = any([v for sublist in reentrancy_all_paths for v in sublist]) - if not isTesting(): - s = "" - if reentrancy_bug_found and source_map != None: - pcs = global_problematic_pcs["reentrancy_bug"] - pcs = [pc for pc in pcs if source_map.find_source_code(pc)] - pcs = source_map.reduce_same_position_pcs(pcs) - s = source_map.to_str(pcs, "Reentrancy bug") - results["reentrancy"] = s - s = "\t Reentrancy bug: \t True" + s if s else "\t Reentrancy bug: \t False" - log.info(s) - - if global_params.CHECK_ASSERTIONS: - if source_map == None: - raise("Assertion checks need a Source Map") - pcs = [pc for pc in global_problematic_pcs["assertion_failure"] if "assert" in source_map.find_source_code(pc)] - pcs = source_map.reduce_same_position_pcs(pcs) - - if not isTesting(): - s = source_map.to_str(pcs, "Assertion failure") - results["assertion_failure"] = s - s = "\t Assertion failure: \t True" + s if s else "\t Assertion failure: \t False" - log.info(s) - - if global_params.WEB: - results_for_web() - -def main(contract, contract_sol, _source_map = None): - global c_name - global c_name_sol - global source_map - - c_name = contract - c_name_sol = contract_sol - source_map = _source_map - - check_unit_test_file() - initGlobalVars() - set_cur_file(c_name[4:] if len(c_name) > 5 else c_name) - start = time.time() - signal.signal(signal.SIGALRM, handler) - if global_params.UNIT_TEST == 2 or global_params.UNIT_TEST == 3: - global_params.GLOBAL_TIMEOUT = global_params.GLOBAL_TIMEOUT_TEST - signal.alarm(global_params.GLOBAL_TIMEOUT) - atexit.register(closing_message) - - log.info("Running, please wait...") - - if not isTesting(): - log.info("\t============ Results ===========") - - try: - build_cfg_and_analyze() - log.debug("Done Symbolic execution") - except Exception as e: - if global_params.UNIT_TEST == 2 or global_params.UNIT_TEST == 3: - log.exception(e) - exit(EXCEPTION) - traceback.print_exc() - raise e - signal.alarm(0) - - detect_bugs() - - -def results_for_web(): - global results - results["cname"] = source_map.get_cname().split(":")[1] - print "======= results =======" - print json.dumps(results) - -def closing_message(): - log.info("\t====== Analysis Completed ======") - if global_params.STORE_RESULT: - result_file = c_name + '.json' - with open(result_file, 'w') as of: - of.write(json.dumps(results, indent=1)) - log.info("Wrote results to %s.", result_file) - def change_format(): with open(c_name) as disasm_file: file_contents = disasm_file.readlines() @@ -276,8 +166,9 @@ def change_format(): with open(c_name, 'w') as disasm_file: disasm_file.write("\n".join(file_contents)) - def build_cfg_and_analyze(): + global source_map + change_format() with open(c_name, 'r') as disasm_file: disasm_file.readline() # Remove first line @@ -423,14 +314,61 @@ def print_cfg(): log.debug(str(edges)) +def mapping_push_instruction(current_line_content, current_ins_address, idx, positions, length): + global source_map + + instr_value = current_line_content.split(" ")[1] + while (idx < length): + name = positions[idx]['name'] + if name.startswith("tag"): + idx += 1 + else: + value = positions[idx]['value'] + if name.startswith("PUSH"): + if name == "PUSH": + if int(value, 16) == int(instr_value, 16): + source_map.set_instr_positions(current_ins_address, idx) + idx += 1 + break; + else: + raise Exception("Source map error") + else: + source_map.set_instr_positions(current_ins_address, idx) + idx += 1 + break; + else: + raise Exception("Source map error") + return idx + +def mapping_non_push_instruction(current_line_content, current_ins_address, idx, positions, length): + global source_map + + instr_name = current_line_content.split(" ")[0] + while (idx < length): + name = positions[idx]['name'] + if name.startswith("tag"): + idx += 1 + else: + if name == instr_name or name == "INVALID" and instr_name == "ASSERTFAIL" or name == "KECCAK256" and instr_name == "SHA3" or name == "SELFDESTRUCT" and instr_name == "SUICIDE": + source_map.set_instr_positions(current_ins_address, idx) + idx += 1 + break; + else: + raise Exception("Source map error") + return idx + # 1. Parse the disassembled file # 2. Then identify each basic block (i.e. one-in, one-out) # 3. Store them in vertices def collect_vertices(tokens): + global source_map + if source_map: + idx = 0 + positions = source_map.get_positions() + length = len(positions) global end_ins_dict global instructions global jump_type - global source_map current_ins_address = 0 last_ins_address = 0 @@ -439,11 +377,6 @@ def collect_vertices(tokens): current_line_content = "" wait_for_push = False is_new_block = False - count = 0 - positions = [] - if source_map != None: - positions = source_map.get_positions() - length = len(positions) for tok_type, tok_string, (srow, scol), _, line_number in tokens: if wait_for_push is True: @@ -453,27 +386,7 @@ def collect_vertices(tokens): is_new_line = True current_line_content += push_val + ' ' instructions[current_ins_address] = current_line_content - instr_name, instr_value, _ = current_line_content.split(" ") - while (count < length): - name = positions[count]['name'] - if name.startswith("tag"): - count += 1 - else: - value = positions[count]['value'] - if name.startswith("PUSH"): - if name == "PUSH": - if int(value, 16) == int(instr_value, 16): - source_map.set_instr_positions(current_ins_address, count) - count += 1 - break; - else: - raise Exception("Source map error") - else: - source_map.set_instr_positions(current_ins_address, count) - count += 1 - break; - else: - raise Exception("Source map error") + idx = mapping_push_instruction(current_line_content, current_ins_address, idx, positions, length) if source_map else None log.debug(current_line_content) current_line_content = "" wait_for_push = False @@ -501,18 +414,7 @@ def collect_vertices(tokens): is_new_line = True log.debug(current_line_content) instructions[current_ins_address] = current_line_content - instr_name = current_line_content.split(" ")[0] - while (count < length): - name = positions[count]['name'] - if name.startswith("tag"): - count += 1 - else: - if name == instr_name or name == "INVALID" and instr_name == "ASSERTFAIL" or name == "KECCAK256" and instr_name == "SHA3" or name == "SELFDESTRUCT" and instr_name == "SUICIDE": - source_map.set_instr_positions(current_ins_address, count) - count += 1 - break; - else: - raise Exception("Source map error") + idx = mapping_non_push_instruction(current_line_content, current_ins_address, idx, positions, length) if source_map else None current_line_content = "" continue elif tok_type == NAME: @@ -2080,5 +1982,117 @@ def run_callstack_attack(): results["callstack"] = s s = "\t Callstack bug: \t True" + s if s else "\t Callstack bug: \t False" log.info(s) + +def detect_bugs(): + if isTesting(): + return + + global results + global source_map + global visited_pcs + global global_problematic_pcs + + evm_code_coverage = float(len(visited_pcs)) / len(instructions.keys()) * 100 + log.info("\t EVM code coverage: \t %s%%", round(evm_code_coverage, 1)) + results["perct_evm_covered"] = str(round(evm_code_coverage, 1)) + + log.debug("Checking for Callstack attack...") + run_callstack_attack() + + if global_params.REPORT_MODE: + rfile.write(str(total_no_of_paths) + "\n") + detect_money_concurrency() + detect_time_dependency() + stop = time.time() + if global_params.REPORT_MODE: + rfile.write(str(stop-start)) + rfile.close() + if global_params.DATA_FLOW: + detect_data_concurrency() + detect_data_money_concurrency() + log.debug("Results for Reentrancy Bug: " + str(reentrancy_all_paths)) + reentrancy_bug_found = any([v for sublist in reentrancy_all_paths for v in sublist]) + + s = "" + if reentrancy_bug_found and source_map != None: + pcs = global_problematic_pcs["reentrancy_bug"] + pcs = [pc for pc in pcs if source_map.find_source_code(pc)] + pcs = source_map.reduce_same_position_pcs(pcs) + s = source_map.to_str(pcs, "Reentrancy bug") + results["reentrancy"] = s + s = "\t Reentrancy bug: \t True" + s if s else "\t Reentrancy bug: \t False" + log.info(s) + + if global_params.CHECK_ASSERTIONS: + if source_map == None: + raise("Assertion checks need a Source Map") + pcs = [pc for pc in global_problematic_pcs["assertion_failure"] if "assert" in source_map.find_source_code(pc)] + pcs = source_map.reduce_same_position_pcs(pcs) + + s = source_map.to_str(pcs, "Assertion failure") + results["assertion_failure"] = s + s = "\t Assertion failure: \t True" + s if s else "\t Assertion failure: \t False" + log.info(s) + + if global_params.WEB: + results_for_web() + +def closing_message(): + log.info("\t====== Analysis Completed ======") + if global_params.STORE_RESULT: + result_file = c_name + '.json' + with open(result_file, 'w') as of: + of.write(json.dumps(results, indent=1)) + log.info("Wrote results to %s.", result_file) + +def handler(signum, frame): + if global_params.UNIT_TEST == 2 or global_params.UNIT_TEST == 3: + exit(TIME_OUT) + detect_bugs() + raise Exception("timeout") + +def results_for_web(): + global results + results["cname"] = source_map.get_cname().split(":")[1] + print "======= results =======" + print json.dumps(results) + +def main(contract, contract_sol, _source_map = None): + global c_name + global c_name_sol + global source_map + + c_name = contract + c_name_sol = contract_sol + source_map = _source_map + + check_unit_test_file() + initGlobalVars() + set_cur_file(c_name[4:] if len(c_name) > 5 else c_name) + start = time.time() + signal.signal(signal.SIGALRM, handler) + if global_params.UNIT_TEST == 2 or global_params.UNIT_TEST == 3: + global_params.GLOBAL_TIMEOUT = global_params.GLOBAL_TIMEOUT_TEST + signal.alarm(global_params.GLOBAL_TIMEOUT) + atexit.register(closing_message) + + log.info("Running, please wait...") + + if not isTesting(): + log.info("\t============ Results ===========") + + try: + build_cfg_and_analyze() + log.debug("Done Symbolic execution") + except Exception as e: + if global_params.UNIT_TEST == 2 or global_params.UNIT_TEST == 3: + log.exception(e) + exit(EXCEPTION) + traceback.print_exc() + raise e + signal.alarm(0) + + detect_bugs() + if __name__ == '__main__': main(sys.argv[1]) From eb11ffd5f4abe33201a406a6c84ae075cd045150 Mon Sep 17 00:00:00 2001 From: luongnt95 Date: Mon, 28 Aug 2017 15:52:36 +0700 Subject: [PATCH 2/3] Fix in bugs not found when running binary --- KingOfTheEtherThrone.sol.1 | 169 ++++++++++++++++++ oyente/symExec.py | 88 +++++---- .../javascripts/src/app/oyente-analyzer.js | 2 +- .../analyzer_result_notification.html.erb | 2 +- 4 files changed, 220 insertions(+), 41 deletions(-) create mode 100644 KingOfTheEtherThrone.sol.1 diff --git a/KingOfTheEtherThrone.sol.1 b/KingOfTheEtherThrone.sol.1 new file mode 100644 index 00000000..5b73afda --- /dev/null +++ b/KingOfTheEtherThrone.sol.1 @@ -0,0 +1,169 @@ +// A chain-game contract that maintains a 'throne' which agents may pay to rule. +// See www.kingoftheether.com & https://github.com/kieranelby/KingOfTheEtherThrone . +// (c) Kieran Elby 2016. All rights reserved. +// v0.4.0. +// Inspired by ethereumpyramid.com and the (now-gone?) "magnificent bitcoin gem". + +// This contract lives on the blockchain at 0xb336a86e2feb1e87a328fcb7dd4d04de3df254d0 +// and was compiled (using optimization) with: +// Solidity version: 0.2.1-fad2d4df/.-Emscripten/clang/int linked to libethereum + +// For future versions it would be nice to ... +// TODO - enforce time-limit on reign (can contracts do that without external action)? +// TODO - add a random reset? +// TODO - add bitcoin bridge so agents can pay in bitcoin? +// TODO - maybe allow different return payment address? + +contract KingOfTheEtherThrone { + + struct Monarch { + // Address to which their compensation will be sent. + address etherAddress; + // A name by which they wish to be known. + // NB: Unfortunately "string" seems to expose some bugs in web3. + string name; + // How much did they pay to become monarch? + uint claimPrice; + // When did their rule start (based on block.timestamp)? + uint coronationTimestamp; + } + + // The wizard is the hidden power behind the throne; they + // occupy the throne during gaps in succession and collect fees. + address wizardAddress; + + // Used to ensure only the wizard can do some things. + modifier onlywizard { if (msg.sender == wizardAddress) _; } + + // How much must the first monarch pay? + uint constant startingClaimPrice = 100 finney; + + // The next claimPrice is calculated from the previous claimFee + // by multiplying by claimFeeAdjustNum and dividing by claimFeeAdjustDen - + // for example, num=3 and den=2 would cause a 50% increase. + uint constant claimPriceAdjustNum = 3; + uint constant claimPriceAdjustDen = 2; + + // How much of each claimFee goes to the wizard (expressed as a fraction)? + // e.g. num=1 and den=100 would deduct 1% for the wizard, leaving 99% as + // the compensation fee for the usurped monarch. + uint constant wizardCommissionFractionNum = 1; + uint constant wizardCommissionFractionDen = 100; + + // How much must an agent pay now to become the monarch? + uint public currentClaimPrice; + + // The King (or Queen) of the Ether. + Monarch public currentMonarch; + + // Earliest-first list of previous throne holders. + Monarch[] public pastMonarchs; + + // Create a new throne, with the creator as wizard and first ruler. + // Sets up some hopefully sensible defaults. + function KingOfTheEtherThrone() { + wizardAddress = msg.sender; + currentClaimPrice = startingClaimPrice; + currentMonarch = Monarch( + wizardAddress, + "[Vacant]", + 0, + block.timestamp + ); + } + + function numberOfMonarchs() constant returns (uint n) { + return pastMonarchs.length; + } + + // Fired when the throne is claimed. + // In theory can be used to help build a front-end. + event ThroneClaimed( + address usurperEtherAddress, + string usurperName, + uint newClaimPrice + ); + + // Fallback function - simple transactions trigger this. + // Assume the message data is their desired name. + function() { + claimThrone(string(msg.data)); + } + + // Claim the throne for the given name by paying the currentClaimFee. + function claimThrone(string name) { + + uint valuePaid = msg.value; + + // If they paid too little, reject claim and refund their money. + if (valuePaid < currentClaimPrice) { + msg.sender.send(valuePaid); + return; + } + + // If they paid too much, continue with claim but refund the excess. + if (valuePaid > currentClaimPrice) { + uint excessPaid = valuePaid - currentClaimPrice; + msg.sender.send(excessPaid); + valuePaid = valuePaid - excessPaid; + } + + // The claim price payment goes to the current monarch as compensation + // (with a commission held back for the wizard). We let the wizard's + // payments accumulate to avoid wasting gas sending small fees. + + uint wizardCommission = (valuePaid * wizardCommissionFractionNum) / wizardCommissionFractionDen; + + uint compensation = valuePaid - wizardCommission; + + if (currentMonarch.etherAddress != wizardAddress) { + currentMonarch.etherAddress.send(compensation); + } else { + // When the throne is vacant, the fee accumulates for the wizard. + } + + // Usurp the current monarch, replacing them with the new one. + pastMonarchs.push(currentMonarch); + currentMonarch = Monarch( + msg.sender, + name, + valuePaid, + block.timestamp + ); + + // Increase the claim fee for next time. + // Stop number of trailing decimals getting silly - we round it a bit. + uint rawNewClaimPrice = currentClaimPrice * claimPriceAdjustNum / claimPriceAdjustDen; + if (rawNewClaimPrice < 10 finney) { + currentClaimPrice = rawNewClaimPrice; + } else if (rawNewClaimPrice < 100 finney) { + currentClaimPrice = 100 szabo * (rawNewClaimPrice / 100 szabo); + } else if (rawNewClaimPrice < 1 ether) { + currentClaimPrice = 1 finney * (rawNewClaimPrice / 1 finney); + } else if (rawNewClaimPrice < 10 ether) { + currentClaimPrice = 10 finney * (rawNewClaimPrice / 10 finney); + } else if (rawNewClaimPrice < 100 ether) { + currentClaimPrice = 100 finney * (rawNewClaimPrice / 100 finney); + } else if (rawNewClaimPrice < 1000 ether) { + currentClaimPrice = 1 ether * (rawNewClaimPrice / 1 ether); + } else if (rawNewClaimPrice < 10000 ether) { + currentClaimPrice = 10 ether * (rawNewClaimPrice / 10 ether); + } else { + currentClaimPrice = rawNewClaimPrice; + } + + // Hail the new monarch! + ThroneClaimed(currentMonarch.etherAddress, currentMonarch.name, currentClaimPrice); + } + + // Used only by the wizard to collect his commission. + function sweepCommission(uint amount) onlywizard { + wizardAddress.send(amount); + } + + // Used only by the wizard to collect his commission. + function transferOwnership(address newOwner) onlywizard { + wizardAddress = newOwner; + } + +} diff --git a/oyente/symExec.py b/oyente/symExec.py index a62e4c5a..50bf7dc8 100755 --- a/oyente/symExec.py +++ b/oyente/symExec.py @@ -199,15 +199,15 @@ def detect_time_dependency(): is_dependant = True continue - if not isTesting(): - s = "" - if source_map != None: - pcs = [pc for pc in pcs if source_map.find_source_code(pc)] - pcs = source_map.reduce_same_position_pcs(pcs) - s = source_map.to_str(pcs, "Time dependency bug") + if source_map: + pcs = [pc for pc in pcs if source_map.find_source_code(pc)] + pcs = source_map.reduce_same_position_pcs(pcs) + s = source_map.to_str(pcs, "Time dependency bug") results["time_dependency"] = s s = "\t Time dependency bug: \t True" + s if s else "\t Time dependency bug: \t False" log.info(s) + else: + log.info("\t Timedependency bug: \t %s", bool(pcs)) if global_params.REPORT_MODE: file_name = c_name.split("/")[len(c_name.split("/"))-1].split(".")[0] @@ -250,15 +250,16 @@ def detect_money_concurrency(): # if PRINT_MODE: print "All false positive cases: ", false_positive log.debug("Concurrency in paths: ") - if not isTesting(): - s = "" - if source_map != None: - pcs = [pc for pc in pcs if source_map.find_source_code(pc)] - pcs = source_map.reduce_same_position_pcs(pcs) - s = source_map.to_str(pcs, "Money concurrency bug") + if source_map: + pcs = [pc for pc in pcs if source_map.find_source_code(pc)] + pcs = source_map.reduce_same_position_pcs(pcs) + s = source_map.to_str(pcs, "Money concurrency bug") results["concurrency"] = s s = "\t Money concurrency bug: True" + s if s else "\t Money concurrency bug: False" log.info(s) + else: + log.info("\t Money concurrency bug: %s", bool(pcs)) + if global_params.REPORT_MODE: rfile.write("number of path: " + str(n) + "\n") # number of FP detected @@ -1971,17 +1972,38 @@ def run_callstack_attack(): instr_pattern = r"([\d]+) ([A-Z]+)([\d]+)?(?: => 0x)?(\S+)?" instr = re.findall(instr_pattern, disasm_data) pcs = check_callstack_attack(instr) - result = True if pcs else False - if not isTesting(): - s = "" - if source_map != None: - pcs = [pc for pc in pcs if source_map.find_source_code(pc)] - pcs = source_map.reduce_same_position_pcs(pcs) - s = source_map.to_str(pcs, "Callstack bug") + if source_map: + pcs = [pc for pc in pcs if source_map.find_source_code(pc)] + pcs = source_map.reduce_same_position_pcs(pcs) + s = source_map.to_str(pcs, "Callstack bug") results["callstack"] = s s = "\t Callstack bug: \t True" + s if s else "\t Callstack bug: \t False" log.info(s) + else: + log.info("\t Callstack bug: \t %s", bool(pcs)) + +def detect_reentrancy(): + reentrancy_bug_found = any([v for sublist in reentrancy_all_paths for v in sublist]) + if source_map: + pcs = global_problematic_pcs["reentrancy_bug"] + pcs = [pc for pc in pcs if source_map.find_source_code(pc)] + pcs = source_map.reduce_same_position_pcs(pcs) + s = source_map.to_str(pcs, "Reentrancy bug") + results["reentrancy"] = s + s = "\t Reentrancy bug: \t True" + s if s else "\t Reentrancy bug: \t False" + log.info(s) + else: + log.info("\t Reentrancy bug: \t %s", reentrancy_bug_found) + +def detect_assertion_failure(): + pcs = [pc for pc in global_problematic_pcs["assertion_failure"] if "assert" in source_map.find_source_code(pc)] + pcs = source_map.reduce_same_position_pcs(pcs) + + s = source_map.to_str(pcs, "Assertion failure") + results["assertion_failure"] = s + s = "\t Assertion failure: \t True" + s if s else "\t Assertion failure: \t False" + log.info(s) def detect_bugs(): if isTesting(): @@ -1994,15 +2016,17 @@ def detect_bugs(): evm_code_coverage = float(len(visited_pcs)) / len(instructions.keys()) * 100 log.info("\t EVM code coverage: \t %s%%", round(evm_code_coverage, 1)) - results["perct_evm_covered"] = str(round(evm_code_coverage, 1)) + results["evm_code_coverage"] = str(round(evm_code_coverage, 1)) log.debug("Checking for Callstack attack...") run_callstack_attack() if global_params.REPORT_MODE: rfile.write(str(total_no_of_paths) + "\n") + detect_money_concurrency() detect_time_dependency() + stop = time.time() if global_params.REPORT_MODE: rfile.write(str(stop-start)) @@ -2010,29 +2034,15 @@ def detect_bugs(): if global_params.DATA_FLOW: detect_data_concurrency() detect_data_money_concurrency() - log.debug("Results for Reentrancy Bug: " + str(reentrancy_all_paths)) - reentrancy_bug_found = any([v for sublist in reentrancy_all_paths for v in sublist]) - s = "" - if reentrancy_bug_found and source_map != None: - pcs = global_problematic_pcs["reentrancy_bug"] - pcs = [pc for pc in pcs if source_map.find_source_code(pc)] - pcs = source_map.reduce_same_position_pcs(pcs) - s = source_map.to_str(pcs, "Reentrancy bug") - results["reentrancy"] = s - s = "\t Reentrancy bug: \t True" + s if s else "\t Reentrancy bug: \t False" - log.info(s) + log.debug("Results for Reentrancy Bug: " + str(reentrancy_all_paths)) + detect_reentrancy() if global_params.CHECK_ASSERTIONS: - if source_map == None: + if source_map: + detect_assertion_failure() + else: raise("Assertion checks need a Source Map") - pcs = [pc for pc in global_problematic_pcs["assertion_failure"] if "assert" in source_map.find_source_code(pc)] - pcs = source_map.reduce_same_position_pcs(pcs) - - s = source_map.to_str(pcs, "Assertion failure") - results["assertion_failure"] = s - s = "\t Assertion failure: \t True" + s if s else "\t Assertion failure: \t False" - log.info(s) if global_params.WEB: results_for_web() diff --git a/web/app/assets/javascripts/src/app/oyente-analyzer.js b/web/app/assets/javascripts/src/app/oyente-analyzer.js index 211b09b6..c7589009 100644 --- a/web/app/assets/javascripts/src/app/oyente-analyzer.js +++ b/web/app/assets/javascripts/src/app/oyente-analyzer.js @@ -76,7 +76,7 @@ function Analyzer () { ${contracts.map(function (contract) { return yo`
======= contract ${contract.cname} =======
-
EVM code coverage: ${contract.perct_evm_covered}%
+
EVM code coverage: ${contract.evm_code_coverage}%
Callstack bug: ${bug_exists(contract.callstack)}
Money concurrency bug: ${bug_exists(contract.concurrency)}
Time dependency bug: ${bug_exists(contract.time_dependency)}
diff --git a/web/app/views/user_mailer/analyzer_result_notification.html.erb b/web/app/views/user_mailer/analyzer_result_notification.html.erb index 649878cc..6873612b 100644 --- a/web/app/views/user_mailer/analyzer_result_notification.html.erb +++ b/web/app/views/user_mailer/analyzer_result_notification.html.erb @@ -8,7 +8,7 @@ <% @results[:contracts].each do |contract| %>
======= contract <%= contract[:cname] %> =======
-
EVM code coverage: <%= contract[:perct_evm_covered] %>%
+
EVM code coverage: <%= contract[:evm_code_coverage] %>%
Callstack bug: <%= bug_exists?(contract[:callstack]) %>
Money concurrency bug: <%= bug_exists?(contract[:concurrency]) %>
Time dependency bug: <%= bug_exists?(contract[:time_dependency]) %>
From 9ad58b7de07ecc72f07a99b3734279651e8e4f16 Mon Sep 17 00:00:00 2001 From: luongnt95 Date: Mon, 28 Aug 2017 17:18:21 +0700 Subject: [PATCH 3/3] Fix ZeroDivisionError when there are no instructions --- KingOfTheEtherThrone.sol.1 | 169 ---------- oyente/symExec.py | 316 +++++++++--------- .../javascripts/src/app/oyente-analyzer.js | 54 +-- .../analyzer_result_notification.html.erb | 10 +- 4 files changed, 203 insertions(+), 346 deletions(-) delete mode 100644 KingOfTheEtherThrone.sol.1 diff --git a/KingOfTheEtherThrone.sol.1 b/KingOfTheEtherThrone.sol.1 deleted file mode 100644 index 5b73afda..00000000 --- a/KingOfTheEtherThrone.sol.1 +++ /dev/null @@ -1,169 +0,0 @@ -// A chain-game contract that maintains a 'throne' which agents may pay to rule. -// See www.kingoftheether.com & https://github.com/kieranelby/KingOfTheEtherThrone . -// (c) Kieran Elby 2016. All rights reserved. -// v0.4.0. -// Inspired by ethereumpyramid.com and the (now-gone?) "magnificent bitcoin gem". - -// This contract lives on the blockchain at 0xb336a86e2feb1e87a328fcb7dd4d04de3df254d0 -// and was compiled (using optimization) with: -// Solidity version: 0.2.1-fad2d4df/.-Emscripten/clang/int linked to libethereum - -// For future versions it would be nice to ... -// TODO - enforce time-limit on reign (can contracts do that without external action)? -// TODO - add a random reset? -// TODO - add bitcoin bridge so agents can pay in bitcoin? -// TODO - maybe allow different return payment address? - -contract KingOfTheEtherThrone { - - struct Monarch { - // Address to which their compensation will be sent. - address etherAddress; - // A name by which they wish to be known. - // NB: Unfortunately "string" seems to expose some bugs in web3. - string name; - // How much did they pay to become monarch? - uint claimPrice; - // When did their rule start (based on block.timestamp)? - uint coronationTimestamp; - } - - // The wizard is the hidden power behind the throne; they - // occupy the throne during gaps in succession and collect fees. - address wizardAddress; - - // Used to ensure only the wizard can do some things. - modifier onlywizard { if (msg.sender == wizardAddress) _; } - - // How much must the first monarch pay? - uint constant startingClaimPrice = 100 finney; - - // The next claimPrice is calculated from the previous claimFee - // by multiplying by claimFeeAdjustNum and dividing by claimFeeAdjustDen - - // for example, num=3 and den=2 would cause a 50% increase. - uint constant claimPriceAdjustNum = 3; - uint constant claimPriceAdjustDen = 2; - - // How much of each claimFee goes to the wizard (expressed as a fraction)? - // e.g. num=1 and den=100 would deduct 1% for the wizard, leaving 99% as - // the compensation fee for the usurped monarch. - uint constant wizardCommissionFractionNum = 1; - uint constant wizardCommissionFractionDen = 100; - - // How much must an agent pay now to become the monarch? - uint public currentClaimPrice; - - // The King (or Queen) of the Ether. - Monarch public currentMonarch; - - // Earliest-first list of previous throne holders. - Monarch[] public pastMonarchs; - - // Create a new throne, with the creator as wizard and first ruler. - // Sets up some hopefully sensible defaults. - function KingOfTheEtherThrone() { - wizardAddress = msg.sender; - currentClaimPrice = startingClaimPrice; - currentMonarch = Monarch( - wizardAddress, - "[Vacant]", - 0, - block.timestamp - ); - } - - function numberOfMonarchs() constant returns (uint n) { - return pastMonarchs.length; - } - - // Fired when the throne is claimed. - // In theory can be used to help build a front-end. - event ThroneClaimed( - address usurperEtherAddress, - string usurperName, - uint newClaimPrice - ); - - // Fallback function - simple transactions trigger this. - // Assume the message data is their desired name. - function() { - claimThrone(string(msg.data)); - } - - // Claim the throne for the given name by paying the currentClaimFee. - function claimThrone(string name) { - - uint valuePaid = msg.value; - - // If they paid too little, reject claim and refund their money. - if (valuePaid < currentClaimPrice) { - msg.sender.send(valuePaid); - return; - } - - // If they paid too much, continue with claim but refund the excess. - if (valuePaid > currentClaimPrice) { - uint excessPaid = valuePaid - currentClaimPrice; - msg.sender.send(excessPaid); - valuePaid = valuePaid - excessPaid; - } - - // The claim price payment goes to the current monarch as compensation - // (with a commission held back for the wizard). We let the wizard's - // payments accumulate to avoid wasting gas sending small fees. - - uint wizardCommission = (valuePaid * wizardCommissionFractionNum) / wizardCommissionFractionDen; - - uint compensation = valuePaid - wizardCommission; - - if (currentMonarch.etherAddress != wizardAddress) { - currentMonarch.etherAddress.send(compensation); - } else { - // When the throne is vacant, the fee accumulates for the wizard. - } - - // Usurp the current monarch, replacing them with the new one. - pastMonarchs.push(currentMonarch); - currentMonarch = Monarch( - msg.sender, - name, - valuePaid, - block.timestamp - ); - - // Increase the claim fee for next time. - // Stop number of trailing decimals getting silly - we round it a bit. - uint rawNewClaimPrice = currentClaimPrice * claimPriceAdjustNum / claimPriceAdjustDen; - if (rawNewClaimPrice < 10 finney) { - currentClaimPrice = rawNewClaimPrice; - } else if (rawNewClaimPrice < 100 finney) { - currentClaimPrice = 100 szabo * (rawNewClaimPrice / 100 szabo); - } else if (rawNewClaimPrice < 1 ether) { - currentClaimPrice = 1 finney * (rawNewClaimPrice / 1 finney); - } else if (rawNewClaimPrice < 10 ether) { - currentClaimPrice = 10 finney * (rawNewClaimPrice / 10 finney); - } else if (rawNewClaimPrice < 100 ether) { - currentClaimPrice = 100 finney * (rawNewClaimPrice / 100 finney); - } else if (rawNewClaimPrice < 1000 ether) { - currentClaimPrice = 1 ether * (rawNewClaimPrice / 1 ether); - } else if (rawNewClaimPrice < 10000 ether) { - currentClaimPrice = 10 ether * (rawNewClaimPrice / 10 ether); - } else { - currentClaimPrice = rawNewClaimPrice; - } - - // Hail the new monarch! - ThroneClaimed(currentMonarch.etherAddress, currentMonarch.name, currentClaimPrice); - } - - // Used only by the wizard to collect his commission. - function sweepCommission(uint amount) onlywizard { - wizardAddress.send(amount); - } - - // Used only by the wizard to collect his commission. - function transferOwnership(address newOwner) onlywizard { - wizardAddress = newOwner; - } - -} diff --git a/oyente/symExec.py b/oyente/symExec.py index 50bf7dc8..2da35e77 100755 --- a/oyente/symExec.py +++ b/oyente/symExec.py @@ -40,7 +40,10 @@ def initGlobalVars(): visited_pcs = set() global results - results = {} + results = { + "evm_code_coverage": "", "callstack": "", "concurrency": "", + "time_dependency": "", "reentrancy": "", "assertion_failure": "" + } # capturing the last statement of each basic block global end_ins_dict @@ -179,136 +182,6 @@ def build_cfg_and_analyze(): full_sym_exec() # jump targets are constructed on the fly -# Detect if a money flow depends on the timestamp -def detect_time_dependency(): - global results - global source_map - - TIMESTAMP_VAR = "IH_s" - is_dependant = False - pcs = [] - if global_params.PRINT_PATHS: - log.info("ALL PATH CONDITIONS") - for i, cond in enumerate(path_conditions): - if global_params.PRINT_PATHS: - log.info("PATH " + str(i + 1) + ": " + str(cond)) - for j, expr in enumerate(cond): - if is_expr(expr): - if TIMESTAMP_VAR in str(expr) and j in global_problematic_pcs["time_dependency_bug"][i]: - pcs.append(global_problematic_pcs["time_dependency_bug"][i][j]) - is_dependant = True - continue - - if source_map: - pcs = [pc for pc in pcs if source_map.find_source_code(pc)] - pcs = source_map.reduce_same_position_pcs(pcs) - s = source_map.to_str(pcs, "Time dependency bug") - results["time_dependency"] = s - s = "\t Time dependency bug: \t True" + s if s else "\t Time dependency bug: \t False" - log.info(s) - else: - log.info("\t Timedependency bug: \t %s", bool(pcs)) - - if global_params.REPORT_MODE: - file_name = c_name.split("/")[len(c_name.split("/"))-1].split(".")[0] - report_file = file_name + '.report' - with open(report_file, 'w') as rfile: - if is_dependant: - rfile.write("yes\n") - else: - rfile.write("no\n") - - -# detect if two paths send money to different people -def detect_money_concurrency(): - global results - global source_map - - n = len(money_flow_all_paths) - for i in range(n): - log.debug("Path " + str(i) + ": " + str(money_flow_all_paths[i])) - log.debug(all_gs[i]) - i = 0 - false_positive = [] - concurrency_paths = [] - pcs = [] - for flow in money_flow_all_paths: - i += 1 - if len(flow) == 1: - continue # pass all flows which do not do anything with money - for j in range(i, n): - jflow = money_flow_all_paths[j] - if len(jflow) == 1: - continue - if is_diff(flow, jflow): - pcs = global_problematic_pcs["money_concurrency_bug"][j] - concurrency_paths.append([i-1, j]) - if global_params.CHECK_CONCURRENCY_FP and \ - is_false_positive(i-1, j, all_gs, path_conditions) and \ - is_false_positive(j, i-1, all_gs, path_conditions): - false_positive.append([i-1, j]) - - # if PRINT_MODE: print "All false positive cases: ", false_positive - log.debug("Concurrency in paths: ") - if source_map: - pcs = [pc for pc in pcs if source_map.find_source_code(pc)] - pcs = source_map.reduce_same_position_pcs(pcs) - s = source_map.to_str(pcs, "Money concurrency bug") - results["concurrency"] = s - s = "\t Money concurrency bug: True" + s if s else "\t Money concurrency bug: False" - log.info(s) - else: - log.info("\t Money concurrency bug: %s", bool(pcs)) - - if global_params.REPORT_MODE: - rfile.write("number of path: " + str(n) + "\n") - # number of FP detected - rfile.write(str(len(false_positive)) + "\n") - rfile.write(str(false_positive) + "\n") - # number of total races - rfile.write(str(len(concurrency_paths)) + "\n") - # all the races - rfile.write(str(concurrency_paths) + "\n") - - -# Detect if there is data concurrency in two different flows. -# e.g. if a flow modifies a value stored in the storage address and -# the other one reads that value in its execution -def detect_data_concurrency(): - sload_flows = data_flow_all_paths[0] - sstore_flows = data_flow_all_paths[1] - concurrency_addr = [] - for sflow in sstore_flows: - for addr in sflow: - for lflow in sload_flows: - if addr in lflow: - if not addr in concurrency_addr: - concurrency_addr.append(addr) - break - log.debug("data concurrency in storage " + str(concurrency_addr)) - -# Detect if any change in a storage address will result in a different -# flow of money. Currently I implement this detection by -# considering if a path condition contains -# a variable which is a storage address. -def detect_data_money_concurrency(): - n = len(money_flow_all_paths) - sstore_flows = data_flow_all_paths[1] - concurrency_addr = [] - for i in range(n): - cond = path_conditions[i] - list_vars = [] - for expr in cond: - list_vars += get_vars(expr) - set_vars = set(i.decl().name() for i in list_vars) - for sflow in sstore_flows: - for addr in sflow: - var_name = gen.gen_owner_store_var(addr) - if var_name in set_vars: - concurrency_addr.append(var_name) - log.debug("Concurrency in data that affects money flow: " + str(set(concurrency_addr))) - - def print_cfg(): for block in vertices.values(): block.display() @@ -1936,6 +1809,137 @@ def sym_exec_ins(start, instr, stack, mem, memory, global_state, sha3_list, path except: log.debug("Error: Debugging states") +# Detect if a money flow depends on the timestamp +def detect_time_dependency(): + global results + global source_map + + TIMESTAMP_VAR = "IH_s" + is_dependant = False + pcs = [] + if global_params.PRINT_PATHS: + log.info("ALL PATH CONDITIONS") + for i, cond in enumerate(path_conditions): + if global_params.PRINT_PATHS: + log.info("PATH " + str(i + 1) + ": " + str(cond)) + for j, expr in enumerate(cond): + if is_expr(expr): + if TIMESTAMP_VAR in str(expr) and j in global_problematic_pcs["time_dependency_bug"][i]: + pcs.append(global_problematic_pcs["time_dependency_bug"][i][j]) + is_dependant = True + continue + + if source_map: + pcs = [pc for pc in pcs if source_map.find_source_code(pc)] + pcs = source_map.reduce_same_position_pcs(pcs) + s = source_map.to_str(pcs, "Time dependency bug") + results["time_dependency"] = s + s = "\t Time dependency bug: \t True" + s if s else "\t Time dependency bug: \t False" + log.info(s) + else: + log.info("\t Timedependency bug: \t %s", bool(pcs)) + + if global_params.REPORT_MODE: + file_name = c_name.split("/")[len(c_name.split("/"))-1].split(".")[0] + report_file = file_name + '.report' + with open(report_file, 'w') as rfile: + if is_dependant: + rfile.write("yes\n") + else: + rfile.write("no\n") + + +# detect if two paths send money to different people +def detect_money_concurrency(): + global results + global source_map + + n = len(money_flow_all_paths) + for i in range(n): + log.debug("Path " + str(i) + ": " + str(money_flow_all_paths[i])) + log.debug(all_gs[i]) + i = 0 + false_positive = [] + concurrency_paths = [] + pcs = [] + for flow in money_flow_all_paths: + i += 1 + if len(flow) == 1: + continue # pass all flows which do not do anything with money + for j in range(i, n): + jflow = money_flow_all_paths[j] + if len(jflow) == 1: + continue + if is_diff(flow, jflow): + pcs = global_problematic_pcs["money_concurrency_bug"][j] + concurrency_paths.append([i-1, j]) + if global_params.CHECK_CONCURRENCY_FP and \ + is_false_positive(i-1, j, all_gs, path_conditions) and \ + is_false_positive(j, i-1, all_gs, path_conditions): + false_positive.append([i-1, j]) + + # if PRINT_MODE: print "All false positive cases: ", false_positive + log.debug("Concurrency in paths: ") + if source_map: + pcs = [pc for pc in pcs if source_map.find_source_code(pc)] + pcs = source_map.reduce_same_position_pcs(pcs) + s = source_map.to_str(pcs, "Money concurrency bug") + results["concurrency"] = s + s = "\t Money concurrency bug: True" + s if s else "\t Money concurrency bug: False" + log.info(s) + else: + log.info("\t Money concurrency bug: %s", bool(pcs)) + + if global_params.REPORT_MODE: + rfile.write("number of path: " + str(n) + "\n") + # number of FP detected + rfile.write(str(len(false_positive)) + "\n") + rfile.write(str(false_positive) + "\n") + # number of total races + rfile.write(str(len(concurrency_paths)) + "\n") + # all the races + rfile.write(str(concurrency_paths) + "\n") + + +# Detect if there is data concurrency in two different flows. +# e.g. if a flow modifies a value stored in the storage address and +# the other one reads that value in its execution +def detect_data_concurrency(): + sload_flows = data_flow_all_paths[0] + sstore_flows = data_flow_all_paths[1] + concurrency_addr = [] + for sflow in sstore_flows: + for addr in sflow: + for lflow in sload_flows: + if addr in lflow: + if not addr in concurrency_addr: + concurrency_addr.append(addr) + break + log.debug("data concurrency in storage " + str(concurrency_addr)) + +# Detect if any change in a storage address will result in a different +# flow of money. Currently I implement this detection by +# considering if a path condition contains +# a variable which is a storage address. +def detect_data_money_concurrency(): + n = len(money_flow_all_paths) + sstore_flows = data_flow_all_paths[1] + concurrency_addr = [] + for i in range(n): + cond = path_conditions[i] + list_vars = [] + for expr in cond: + list_vars += get_vars(expr) + set_vars = set(i.decl().name() for i in list_vars) + for sflow in sstore_flows: + for addr in sflow: + var_name = gen.gen_owner_store_var(addr) + if var_name in set_vars: + concurrency_addr.append(var_name) + log.debug("Concurrency in data that affects money flow: " + str(set(concurrency_addr))) + + + def check_callstack_attack(disasm): problematic_instructions = ['CALL', 'CALLCODE'] pcs = [] @@ -2014,35 +2018,39 @@ def detect_bugs(): global visited_pcs global global_problematic_pcs - evm_code_coverage = float(len(visited_pcs)) / len(instructions.keys()) * 100 - log.info("\t EVM code coverage: \t %s%%", round(evm_code_coverage, 1)) - results["evm_code_coverage"] = str(round(evm_code_coverage, 1)) + if instructions: + evm_code_coverage = float(len(visited_pcs)) / len(instructions.keys()) * 100 + log.info("\t EVM code coverage: \t %s%%", round(evm_code_coverage, 1)) + results["evm_code_coverage"] = str(round(evm_code_coverage, 1)) - log.debug("Checking for Callstack attack...") - run_callstack_attack() + log.debug("Checking for Callstack attack...") + run_callstack_attack() - if global_params.REPORT_MODE: - rfile.write(str(total_no_of_paths) + "\n") + if global_params.REPORT_MODE: + rfile.write(str(total_no_of_paths) + "\n") - detect_money_concurrency() - detect_time_dependency() + detect_money_concurrency() + detect_time_dependency() - stop = time.time() - if global_params.REPORT_MODE: - rfile.write(str(stop-start)) - rfile.close() - if global_params.DATA_FLOW: - detect_data_concurrency() - detect_data_money_concurrency() + stop = time.time() + if global_params.REPORT_MODE: + rfile.write(str(stop-start)) + rfile.close() + if global_params.DATA_FLOW: + detect_data_concurrency() + detect_data_money_concurrency() - log.debug("Results for Reentrancy Bug: " + str(reentrancy_all_paths)) - detect_reentrancy() + log.debug("Results for Reentrancy Bug: " + str(reentrancy_all_paths)) + detect_reentrancy() - if global_params.CHECK_ASSERTIONS: - if source_map: - detect_assertion_failure() - else: - raise("Assertion checks need a Source Map") + if global_params.CHECK_ASSERTIONS: + if source_map: + detect_assertion_failure() + else: + raise("Assertion checks need a Source Map") + else: + log.info("\t EVM code coverage: \t 0/0") + results["evm_code_coverage"] = "0/0" if global_params.WEB: results_for_web() diff --git a/web/app/assets/javascripts/src/app/oyente-analyzer.js b/web/app/assets/javascripts/src/app/oyente-analyzer.js index c7589009..00c1b600 100644 --- a/web/app/assets/javascripts/src/app/oyente-analyzer.js +++ b/web/app/assets/javascripts/src/app/oyente-analyzer.js @@ -74,26 +74,40 @@ function Analyzer () {
${filename}

${contracts.map(function (contract) { - return yo`
-
======= contract ${contract.cname} =======
-
EVM code coverage: ${contract.evm_code_coverage}%
-
Callstack bug: ${bug_exists(contract.callstack)}
-
Money concurrency bug: ${bug_exists(contract.concurrency)}
-
Time dependency bug: ${bug_exists(contract.time_dependency)}
-
Reentrancy bug: ${bug_exists(contract.reentrancy)}
-
Assertion failure: ${bug_exists(contract.assertion_failure)}
- ${ - (contract.callstack || contract.concurrency || contract.time_dependency - || contract.reentrancy || contract.assertion_failure) ? $.parseHTML("
") : "" - } -
${$.parseHTML(contract.callstack)}
-
${$.parseHTML(contract.concurrency)}
-
${$.parseHTML(contract.time_dependency)}
-
${$.parseHTML(contract.reentrancy)}
-
${$.parseHTML(contract.assertion_failure)}
-
======= Analysis Completed =======
-
-
` + if (contract.evm_code_coverage === "0/0") { + return yo`
+
======= contract ${contract.cname} =======
+
EVM code coverage: ${contract.evm_code_coverage}
+
Callstack bug: ${bug_exists(contract.callstack)}
+
Money concurrency bug: ${bug_exists(contract.concurrency)}
+
Time dependency bug: ${bug_exists(contract.time_dependency)}
+
Reentrancy bug: ${bug_exists(contract.reentrancy)}
+
Assertion failure: ${bug_exists(contract.assertion_failure)}
+
======= Analysis Completed =======
+
+
` + } else { + return yo`
+
======= contract ${contract.cname} =======
+
EVM code coverage: ${contract.evm_code_coverage}%
+
Callstack bug: ${bug_exists(contract.callstack)}
+
Money concurrency bug: ${bug_exists(contract.concurrency)}
+
Time dependency bug: ${bug_exists(contract.time_dependency)}
+
Reentrancy bug: ${bug_exists(contract.reentrancy)}
+
Assertion failure: ${bug_exists(contract.assertion_failure)}
+ ${ + (contract.callstack || contract.concurrency || contract.time_dependency + || contract.reentrancy || contract.assertion_failure) ? $.parseHTML("
") : "" + } +
${$.parseHTML(contract.callstack)}
+
${$.parseHTML(contract.concurrency)}
+
${$.parseHTML(contract.time_dependency)}
+
${$.parseHTML(contract.reentrancy)}
+
${$.parseHTML(contract.assertion_failure)}
+
======= Analysis Completed =======
+
+
` + } })}
` } diff --git a/web/app/views/user_mailer/analyzer_result_notification.html.erb b/web/app/views/user_mailer/analyzer_result_notification.html.erb index 6873612b..591297ff 100644 --- a/web/app/views/user_mailer/analyzer_result_notification.html.erb +++ b/web/app/views/user_mailer/analyzer_result_notification.html.erb @@ -8,14 +8,18 @@ <% @results[:contracts].each do |contract| %>
======= contract <%= contract[:cname] %> =======
-
EVM code coverage: <%= contract[:evm_code_coverage] %>%
+ <% if contract[:evm_code_coverage] == "0/0" %> +
EVM code coverage: <%= contract[:evm_code_coverage] %>
+ <% else %> +
EVM code coverage: <%= contract[:evm_code_coverage] %>%
+ <% end %>
Callstack bug: <%= bug_exists?(contract[:callstack]) %>
Money concurrency bug: <%= bug_exists?(contract[:concurrency]) %>
Time dependency bug: <%= bug_exists?(contract[:time_dependency]) %>
Reentrancy bug: <%= bug_exists?(contract[:reentrancy]) %>
Assertion failure: <%= bug_exists?(contract[:assertion_failure]) %>
- <% if contract[:callstack] || contract[:concurrency] \ - || contract[:time_dependency] || contract[:reentrancy] || contract[:assertion_failure] %> + <% if contract[:callstack].present? || contract[:concurrency].present? \ + || contract[:time_dependency].present? || contract[:reentrancy].present? || contract[:assertion_failure].present? %>
<% end %>
<%= contract[:callstack].html_safe %>