diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8aa622fff..ae3044ab2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - type: ["ethereum_truffle", "ethereum_bench", "examples", "ethereum", "ethereum_vm", "native", "wasm", "wasm_sym", "other"] + type: ["ethereum_bench", "examples", "ethereum", "ethereum_metacoin", "ethereum_truffle", "ethereum_vm", "native", "wasm", "wasm_sym", "other"] steps: - uses: actions/checkout@v1 - name: Set up Python 3.6 @@ -155,26 +155,29 @@ jobs: cd $DIR } - install_truffle(){ - npm install -g truffle + install_node_packages(){ + npm install -g ganache-cli truffle } - run_truffle_tests(){ + run_metacoin_tests(){ COVERAGE_RCFILE=$GITHUB_WORKSPACE/.coveragerc - mkdir truffle_tests - cd truffle_tests - truffle unbox metacoin - coverage run -m manticore . --contract MetaCoin --workspace output --exclude-all --evm.oog ignore --evm.txfail optimistic + ADDITIONAL_ARGS=. + EXPECTED_TX=27 + if [ "$1" = "deploy" ]; then + ADDRESS="$(truffle deploy | grep '> contract address:.*' | tail -n 1 | grep -o '0x.*$')" + ADDITIONAL_ARGS="--rpc 127.0.0.1:7545 --txtarget $ADDRESS" + EXPECTED_TX=26 + fi + coverage run -p -m manticore --contract MetaCoin --workspace output --exclude-all --evm.oog ignore --evm.txfail optimistic --smt.timeout 600 $ADDITIONAL_ARGS # Truffle smoke test. We test if manticore is able to generate states # from a truffle project. - if [ "$(ls output/*tx -l | wc -l)" != "26" ]; then - echo "Truffle test failed" `ls output/*tx -l | wc -l` "!= 26" + ACTUAL_TX="$(ls output/*tx -l | wc -l)" + rm -rf output + if [ "$ACTUAL_TX" != "$EXPECTED_TX" ]; then + echo "MetaCoin test failed: $ACTUAL_TX != $EXPECTED_TX" return 1 fi - echo "Truffle test succeded" - coverage xml - cd .. - cp truffle_tests/coverage.xml . + echo "MetaCoin test succeded" return 0 } @@ -183,6 +186,9 @@ jobs: COVERAGE_RCFILE=$GITHUB_WORKSPACE/.coveragerc echo "Running only the tests from 'tests/$DIR' directory" pytest --durations=100 --cov=manticore --cov-config=$GITHUB_WORKSPACE/.coveragerc -n auto "tests/$DIR" + if [ $? -ne 0 ]; then + return 1 + fi coverage xml } @@ -207,15 +213,30 @@ jobs: # Test type case $TEST_TYPE in + ethereum_metacoin) + echo "Running metacoin tests" + install_node_packages + ganache-cli -p 7545 -i 5777 & + mkdir metacoin_tests \ + && cd metacoin_tests \ + && truffle unbox metacoin \ + && run_metacoin_tests \ + && run_metacoin_tests deploy \ + && coverage combine \ + && coverage xml \ + && cd .. \ + && cp metacoin_tests/coverage.xml . + ;; + ethereum_truffle) + echo "Running truffle tests" + install_node_packages + ganache-cli -p 7545 -i 5777 & + run_tests_from_dir $TEST_TYPE + ;; ethereum_vm) make_vmtests run_tests_from_dir $TEST_TYPE ;; - ethereum_truffle) - echo "Running truffle test" - install_truffle - run_truffle_tests - ;; wasm) make_wasm_tests run_tests_from_dir $TEST_TYPE diff --git a/manticore/__main__.py b/manticore/__main__.py index 510082925..51a691752 100644 --- a/manticore/__main__.py +++ b/manticore/__main__.py @@ -41,7 +41,12 @@ def main() -> None: resources.check_disk_usage() resources.check_memory_usage() - if args.argv[0].endswith(".sol") or is_supported(args.argv[0]): + if ( + args.url is not None + or args.txtarget is not None + or args.argv[0].endswith(".sol") + or is_supported(args.argv[0]) + ): ethereum_main(args, logger) elif args.argv[0].endswith(".wasm") or args.argv[0].endswith(".wat"): wasm_main(args, logger) @@ -165,6 +170,8 @@ def positive(value): "--txnoether", action="store_true", help="Do not attempt to send ether to contract" ) + eth_flags.add_argument("--txtarget", type=str, help="Address of a deployed contract to target") + eth_flags.add_argument( "--txaccount", type=str, @@ -183,6 +190,13 @@ def positive(value): "--contract", type=str, help="Contract name to analyze in case of multiple contracts" ) + eth_flags.add_argument( + "--rpc", + type=str, + dest="url", + help="URL of an Ethereum node to connect to. Must be of the form 'IP:PORT'", + ) + eth_detectors = parser.add_argument_group("Ethereum detectors") eth_detectors.add_argument( @@ -242,6 +256,9 @@ def positive(value): parsed = parser.parse_args(sys.argv[1:]) config.process_config_values(parser, parsed) + if parsed.txtarget is not None and not parsed.argv: + parsed.argv = [None] + if not parsed.argv: print(parser.format_usage() + "error: the following arguments are required: argv") sys.exit(1) diff --git a/manticore/core/smtlib/expression.py b/manticore/core/smtlib/expression.py index d0634ab53..201424a28 100644 --- a/manticore/core/smtlib/expression.py +++ b/manticore/core/smtlib/expression.py @@ -4,7 +4,7 @@ import re import copy -from typing import Union, Optional, Dict, List +from typing import Union, Optional, Dict, List, Tuple class ExpressionException(SmtlibError): @@ -670,7 +670,7 @@ def __init__( self, index_bits: int, index_max: Optional[int], value_bits: int, *operands, **kwargs ): assert index_bits in (32, 64, 256) - assert value_bits in (8, 16, 32, 64, 256) + assert value_bits in (1, 8, 16, 32, 64, 256) assert index_max is None or index_max >= 0 and index_max < 2 ** index_bits self._index_bits = index_bits self._index_max = index_max @@ -1205,6 +1205,14 @@ def get(self, index, default=None): value = self._array.select(index) return BitVecITE(self._array.value_bits, is_known, value, default) + def get_items(self) -> List[Tuple[Union[int, BitVec], Union[int, BitVec]]]: + items = [] + array = self.array + while not isinstance(array, ArrayVariable): + items.append((array.index, array.value)) + array = array.array + return items + class ArraySelect(BitVec): __slots__ = ["_operands"] diff --git a/manticore/ethereum/cli.py b/manticore/ethereum/cli.py index 388fe7b30..2aa0713de 100644 --- a/manticore/ethereum/cli.py +++ b/manticore/ethereum/cli.py @@ -15,6 +15,7 @@ DetectManipulableBalance, ) from ..core.plugin import Profiler +from ..exceptions import EthereumError from .manticore import ManticoreEVM from .plugins import ( FilterFunctions, @@ -23,6 +24,7 @@ KeepOnlyIfStorageChanges, SkipRevertBasicBlocks, ) +from ..platforms.evm_world_state import RemoteWorldState from ..utils.nointerrupt import WithKeyboardInterruptAs from ..utils import config @@ -79,6 +81,10 @@ def choose_detectors(args): f"{e} is not a detector name, must be one of {arguments}. See also `--list-detectors`." ) + # sam.moelius: Do not enable uninitialized storage detector when using RPC. It generates too much noise. + if args.url is not None: + exclude.append(DetectUninitializedStorage.ARGUMENT) + for arg, detector_cls in detectors.items(): if arg not in exclude: detectors_to_run.append(detector_cls) @@ -87,7 +93,11 @@ def choose_detectors(args): def ethereum_main(args, logger): - m = ManticoreEVM(workspace_url=args.workspace) + world_state = None + if args.url is not None: + world_state = RemoteWorldState(args.url) + + m = ManticoreEVM(workspace_url=args.workspace, world_state=world_state) if args.quick_mode: args.avoid_constant = True @@ -130,16 +140,28 @@ def ethereum_main(args, logger): logger.info("Beginning analysis") with m.kill_timeout(): - m.multi_tx_analysis( - args.argv[0], - contract_name=args.contract, - tx_limit=args.txlimit, - tx_use_coverage=not args.txnocoverage, - tx_send_ether=not args.txnoether, - tx_account=args.txaccount, - tx_preconstrain=args.txpreconstrain, - compile_args=vars(args), # FIXME - ) + try: + contract_account = None + if args.txtarget is not None: + contract_account = int(args.txtarget, base=0) + if world_state is not None and not world_state.get_code(contract_account): + raise EthereumError( + "Could not get code for target account: " + args.txtarget + ) + + m.multi_tx_analysis( + args.argv[0], + contract_name=args.contract, + tx_limit=args.txlimit, + tx_use_coverage=not args.txnocoverage, + tx_send_ether=not args.txnoether, + contract_account=contract_account, + tx_account=args.txaccount, + tx_preconstrain=args.txpreconstrain, + compile_args=vars(args), # FIXME + ) + except EthereumError as e: + logger.error("%r", e.args[0]) if not args.no_testcases: m.finalize(only_alive_states=args.only_alive_testcases) diff --git a/manticore/ethereum/manticore.py b/manticore/ethereum/manticore.py index 32a0d78a5..4815746e0 100644 --- a/manticore/ethereum/manticore.py +++ b/manticore/ethereum/manticore.py @@ -34,6 +34,7 @@ from .state import State from ..exceptions import EthereumError, DependencyError, NoAliveStates from ..platforms import evm +from ..platforms.evm_world_state import WorldState from ..utils import config, log from ..utils.deprecated import deprecated from ..utils.helpers import PickleSerializer, printable_bytes @@ -390,7 +391,7 @@ def contract_accounts(self): def get_account(self, name): return self._accounts[name] - def __init__(self, plugins=None, **kwargs): + def __init__(self, world_state: Optional[WorldState] = None, plugins=None, **kwargs): """ A Manticore EVM manager :param plugins: the plugins to register in this manticore manager @@ -398,7 +399,7 @@ def __init__(self, plugins=None, **kwargs): # Make the constraint store constraints = ConstraintSet() # make the ethereum world state - world = evm.EVMWorld(constraints) + world = evm.EVMWorld(constraints, world_state=world_state) initial_state = State(constraints, world) super().__init__(initial_state, **kwargs) if plugins is not None: @@ -1035,6 +1036,7 @@ def multi_tx_analysis( tx_limit=None, tx_use_coverage=True, tx_send_ether=True, + contract_account: Optional[int] = None, tx_account="attacker", tx_preconstrain=False, args=None, @@ -1042,23 +1044,27 @@ def multi_tx_analysis( ): owner_account = self.create_account(balance=10 ** 10, name="owner") attacker_account = self.create_account(balance=10 ** 10, name="attacker") - # Pretty print - logger.info("Starting symbolic create contract") - if tx_send_ether: - create_value = self.make_symbolic_value() - else: - create_value = 0 - - contract_account = self.solidity_create_contract( - solidity_filename, - contract_name=contract_name, - owner=owner_account, - args=args, - compile_args=compile_args, - balance=create_value, - gas=230000, - ) + if contract_account is None: + # Pretty print + logger.info("Starting symbolic create contract") + + if tx_send_ether: + create_value = self.make_symbolic_value() + else: + create_value = 0 + + contract_account = self.solidity_create_contract( + solidity_filename, + contract_name=contract_name, + owner=owner_account, + args=args, + compile_args=compile_args, + balance=create_value, + gas=230000, + ) + elif solidity_filename is not None: + raise EthereumError("A target contract and a Solidty filename cannot both be specified") if tx_account == "attacker": tx_account = [attacker_account] diff --git a/manticore/platforms/evm.py b/manticore/platforms/evm.py index 0b85987f4..1fd757790 100644 --- a/manticore/platforms/evm.py +++ b/manticore/platforms/evm.py @@ -7,6 +7,7 @@ import inspect from functools import wraps from typing import List, Set, Tuple, Union +from ..platforms.evm_world_state import * from ..platforms.platform import * from ..core.smtlib import ( SelectedSolver, @@ -15,9 +16,8 @@ ArrayProxy, Operators, Constant, - ArrayVariable, - ArrayStore, BitVecConstant, + ConstraintSet, translate_to_smtlib, to_constant, simplify, @@ -87,7 +87,7 @@ def globalfakesha3(data): description=( "Default behavior for transaction failing because not enough funds." "optimistic: Assume there is always enough funds to pay for value and gas. Wont fork" - "pessimistic: Assume the balance is never enough for paying fo a transaction. Wont fork" + "pessimistic: Assume the balance is never enough for paying for a transaction. Wont fork" "both: Will fork for both options if possible." ), ) @@ -116,9 +116,6 @@ def globalfakesha3(data): "PendingTransaction", ["type", "address", "price", "data", "caller", "value", "gas", "failed"] ) EVMLog = namedtuple("EVMLog", ["address", "memlog", "topics"]) -BlockHeader = namedtuple( - "BlockHeader", ["blocknumber", "timestamp", "difficulty", "gaslimit", "coinbase"] -) def ceil32(x): @@ -1529,6 +1526,8 @@ def EXP(self, base, exponent): :param exponent: exponent value, concretized with sampled values :return: BitVec* EXP result """ + if isinstance(exponent, Constant): + exponent = exponent.value if exponent == 0: return 1 @@ -1955,10 +1954,13 @@ def SSTORE_gas(self, offset, value): self.fail_if(Operators.ULT(self.gas, SSSTORESENTRYGAS)) # Get the storage from the snapshot took before this call + original_value = 0 try: - original_value = self.world._callstack[-1][-2].get(offset, 0) + storage = self.world._callstack[-1][-2] + if storage is not None: + original_value = storage.get(offset, 0) except IndexError: - original_value = 0 + pass current_value = self.world.get_storage_data(storage_address, offset) @@ -2387,20 +2389,23 @@ class EVMWorld(Platform): "symbolic_function", } - def __init__(self, constraints, fork=DEFAULT_FORK, **kwargs): + def __init__( + self, constraints, fork=DEFAULT_FORK, world_state: Optional[WorldState] = None, **kwargs, + ): super().__init__(path="NOPATH", **kwargs) - self._world_state = {} + self._world_state = OverlayWorldState( + world_state if world_state is not None else DefaultWorldState() + ) self._constraints = constraints self._callstack: List[ - Tuple[Transaction, List[EVMLog], Set[int], Union[bytearray, ArrayProxy], EVM] + Tuple[Transaction, List[EVMLog], Set[int], Optional[Storage], EVM] ] = [] self._deleted_accounts: Set[int] = set() self._logs: List[EVMLog] = list() self._pending_transaction = None self._transactions: List[Transaction] = list() self._fork = fork - self._block_header = None - self.start_block() + self.start_block(**kwargs) def __getstate__(self): state = super().__getstate__() @@ -2412,7 +2417,6 @@ def __getstate__(self): state["_deleted_accounts"] = self._deleted_accounts state["_transactions"] = self._transactions state["_fork"] = self._fork - state["_block_header"] = self._block_header return state @@ -2426,7 +2430,6 @@ def __setstate__(self, state): self._callstack = state["_callstack"] self._transactions = state["_transactions"] self._fork = state["_fork"] - self._block_header = state["_block_header"] for _, _, _, _, vm in self._callstack: self.forward_events_from(vm) @@ -2476,7 +2479,7 @@ def PC(self): def __getitem__(self, index): assert isinstance(index, int) - return self._world_state[index] + return self.accounts[index] def __contains__(self, key): assert not issymbolic(key), "Symbolic address not supported" @@ -2606,7 +2609,7 @@ def _open_transaction(self, sort, address, price, bytecode_or_data, caller, valu f"Error: contract created from address {hex(caller)} with nonce {self.get_nonce(caller)} was expected to be at address {hex(expected_address)}, but create_contract was called with address={hex(address)}" ) - if address not in self.accounts: + if not self._world_state.is_remote() and address not in self.accounts: logger.info("Address does not exists creating it.") # Creating an unaccessible account self.create_account(address=address, nonce=int(sort != "CREATE")) @@ -2623,7 +2626,7 @@ def _open_transaction(self, sort, address, price, bytecode_or_data, caller, valu self.sub_from_balance(caller, aux_price * aux_gas) self.send_funds(tx.caller, tx.address, tx.value) - if tx.address not in self.accounts: + if not self._world_state.is_remote() and tx.address not in self.accounts: self.create_account(tx.address) # If not a human tx, reset returndata @@ -2671,8 +2674,7 @@ def _close_transaction(self, result, data=None, rollback=False): if tx.is_human: for deleted_account in self._deleted_accounts: - if deleted_account in self._world_state: - del self._world_state[deleted_account] + self._world_state.delete_account(self.constraints, deleted_account) unused_fee = unused_gas * tx.price used_fee = used_gas * tx.price self.add_to_balance(tx.caller, unused_fee) @@ -2772,7 +2774,7 @@ def current_human_transaction(self): @property def accounts(self): - return list(self._world_state.keys()) + return list(self._world_state.accounts()) @property def normal_accounts(self): @@ -2795,10 +2797,11 @@ def deleted_accounts(self): return self._deleted_accounts def delete_account(self, address): - if address in self._world_state: - self._deleted_accounts.add(address) + self._deleted_accounts.add(address) - def get_storage_data(self, storage_address, offset): + def get_storage_data( + self, storage_address: int, offset: Union[int, BitVec] + ) -> Union[int, BitVec]: """ Read a value from a storage slot on the specified account @@ -2808,10 +2811,12 @@ def get_storage_data(self, storage_address, offset): :return: the value :rtype: int or BitVec """ - value = self._world_state[storage_address]["storage"].get(offset, 0) + value = self._world_state.get_storage_data(self.constraints, storage_address, offset) return simplify(value) - def set_storage_data(self, storage_address, offset, value): + def set_storage_data( + self, storage_address: int, offset: Union[int, BitVec], value: Union[int, BitVec] + ): """ Writes a value to a storage slot in specified account @@ -2821,9 +2826,11 @@ def set_storage_data(self, storage_address, offset, value): :param value: the value to write :type value: int or BitVec """ - self._world_state[storage_address]["storage"][offset] = value + self._world_state.set_storage_data(self.constraints, storage_address, offset, value) - def get_storage_items(self, address): + def get_storage_items( + self, address: int + ) -> List[Tuple[Union[int, BitVec], Union[int, BitVec]]]: """ Gets all items in an account storage @@ -2831,93 +2838,89 @@ def get_storage_items(self, address): :return: all items in account storage. items are tuple of (index, value). value can be symbolic :rtype: list[(storage_index, storage_value)] """ - storage = self._world_state[address]["storage"] - items = [] - array = storage.array - while not isinstance(array, ArrayVariable): - items.append((array.index, array.value)) - array = array.array - return items - - def has_storage(self, address): + return self.get_storage(address).get_items() + + def has_storage(self, address: int) -> bool: """ True if something has been written to the storage. Note that if a slot has been erased from the storage this function may lose any meaning. """ - storage = self._world_state[address]["storage"] - array = storage.array - while not isinstance(array, ArrayVariable): - if isinstance(array, ArrayStore): - return True - array = array.array - return False - - def get_storage(self, address): + return self._world_state.has_storage(address) + + def get_storage(self, address: int) -> Storage: """ Gets the storage of an account :param address: account address :return: account storage - :rtype: bytearray or ArrayProxy + :rtype: Storage """ - return self._world_state[address]["storage"] + return self._get_storage(self.constraints, address) - def _set_storage(self, address, storage): + def _get_storage(self, constraints: ConstraintSet, address: int) -> Storage: + """Private auxiliary function to retrieve the storage""" + storage = self._world_state.get_storage(address) + if storage is None: + storage = Storage(constraints, address) + self._world_state.set_storage(address, storage) + return storage + + def _set_storage(self, address: int, storage: Union[Dict[int, int], Optional[Storage]]): """Private auxiliary function to replace the storage""" - self._world_state[address]["storage"] = storage + if isinstance(storage, dict): + storage = Storage(self.constraints, address, storage) + self._world_state.set_storage(address, storage) - def get_nonce(self, address): - if issymbolic(address): + def get_nonce(self, address: Union[int, BitVec]) -> Union[int, BitVec]: + if isinstance(address, BitVec): raise ValueError(f"Cannot retrieve the nonce of symbolic address {address}") else: - ret = self._world_state[address]["nonce"] - return ret + return self._world_state.get_nonce(address) + + def set_nonce(self, address: Union[int, BitVec], value: Union[int, BitVec]): + if isinstance(address, BitVec): + raise ValueError(f"Cannot set the nonce of symbolic address {address}") + else: + self._world_state.set_nonce(address, value) - def increase_nonce(self, address): + def increase_nonce(self, address: Union[int, BitVec]) -> Union[int, BitVec]: new_nonce = self.get_nonce(address) + 1 - self._world_state[address]["nonce"] = new_nonce + self.set_nonce(address, new_nonce) return new_nonce - def set_balance(self, address, value): + def set_balance(self, address: int, value: Union[int, BitVec]): if isinstance(value, BitVec): value = Operators.ZEXTEND(value, 512) - self._world_state[int(address)]["balance"] = value + self._world_state.set_balance(address, value) - def get_balance(self, address): - if address not in self._world_state: - return 0 - return Operators.EXTRACT(self._world_state[address]["balance"], 0, 256) + def get_balance(self, address: int) -> Union[int, BitVec]: + return Operators.EXTRACT(self._world_state.get_balance(address), 0, 256) - def add_to_balance(self, address, value): + def add_to_balance(self, address: int, value: Union[int, BitVec]): if isinstance(value, BitVec): value = Operators.ZEXTEND(value, 512) - self._world_state[address]["balance"] += value + new_balance = self._world_state.get_balance(address) + value + self._world_state.set_balance(address, new_balance) - def sub_from_balance(self, address, value): + def sub_from_balance(self, address: int, value: Union[int, BitVec]): if isinstance(value, BitVec): value = Operators.ZEXTEND(value, 512) - self._world_state[address]["balance"] -= value + new_balance = self._world_state.get_balance(address) - value + self._world_state.set_balance(address, new_balance) - def send_funds(self, sender, recipient, value): - if isinstance(value, BitVec): - value = Operators.ZEXTEND(value, 512) - self._world_state[sender]["balance"] -= value - self._world_state[recipient]["balance"] += value + def send_funds(self, sender: int, recipient: int, value: Union[int, BitVec]): + self.sub_from_balance(sender, value) + self.add_to_balance(recipient, value) - def get_code(self, address): - if address not in self._world_state: - return bytes() - return self._world_state[address]["code"] + def get_code(self, address: int) -> Union[bytes, Array]: + return self._world_state.get_code(address) - def set_code(self, address, data): - assert data is not None and isinstance(data, (bytes, Array)) - if self._world_state[address]["code"]: - raise EVMException("Code already set") - self._world_state[address]["code"] = data + def set_code(self, address: int, data: Union[bytes, Array]): + self._world_state.set_code(address, data) - def has_code(self, address): - return len(self._world_state[address]["code"]) > 0 + def has_code(self, address: int) -> bool: + return len(self.get_code(address)) > 0 def log(self, address, topics, data): self._logs.append(EVMLog(address, data, topics)) @@ -2944,11 +2947,15 @@ def start_block( gaslimit=0x7FFFFFFF, coinbase=0, ): - if coinbase not in self.accounts and coinbase != 0: + if not self._world_state.is_remote() and coinbase not in self.accounts and coinbase != 0: logger.info("Coinbase account does not exists") self.create_account(coinbase) - self._block_header = BlockHeader(blocknumber, timestamp, difficulty, gaslimit, coinbase) + self._world_state.set_blocknumber(blocknumber) + self._world_state.set_timestamp(timestamp) + self._world_state.set_difficulty(difficulty) + self._world_state.set_gaslimit(gaslimit) + self._world_state.set_coinbase(coinbase) def end_block(self, block_reward=None): coinbase = self.block_coinbase() @@ -2958,22 +2965,21 @@ def end_block(self, block_reward=None): if block_reward is None: block_reward = 2000000000000000000 # 2 eth self.add_to_balance(self.block_coinbase(), block_reward) - # self._block_header = None def block_coinbase(self): - return self._block_header.coinbase + return self._world_state.get_coinbase() def block_timestamp(self): - return self._block_header.timestamp + return self._world_state.get_timestamp() def block_number(self): - return self._block_header.blocknumber + return self._world_state.get_blocknumber() def block_difficulty(self): - return self._block_header.difficulty + return self._world_state.get_difficulty() def block_gaslimit(self): - return self._block_header.gaslimit + return self._world_state.get_gaslimit() def block_hash(self, block_number=None, force_recent=True): """ @@ -3057,7 +3063,14 @@ def execute(self): except EndTx as ex: self._close_transaction(ex.result, ex.data, rollback=ex.is_rollback()) - def create_account(self, address=None, balance=0, code=None, storage=None, nonce=None): + def create_account( + self, + address=None, + balance=0, + code=None, + storage: Optional[Dict[int, int]] = None, + nonce=None, + ): """ Low level account creation. No transaction is done. @@ -3090,31 +3103,17 @@ def create_account(self, address=None, balance=0, code=None, storage=None, nonce # selfdestructed address, it can not be reused raise EthereumError("The account already exists") - if storage is None: - # Uninitialized values in a storage are 0 by spec - storage = self.constraints.new_array( - index_bits=256, - value_bits=256, - name=f"STORAGE_{address:x}", - avoid_collisions=True, - default=0, - ) - else: - if isinstance(storage, ArrayProxy): - if storage.index_bits != 256 or storage.value_bits != 256: - raise TypeError("An ArrayProxy 256bits -> 256bits is needed") - else: - if any((k < 0 or k >= 1 << 256 for k, v in storage.items())): - raise TypeError( - "Need a dict like object that maps 256 bits keys to 256 bits values" - ) - # Hopefully here we have a mapping from 256b to 256b + if nonce is not None: + self.set_nonce(address, nonce) + + if balance is not None: + self.set_balance(address, balance) + + if storage is not None: + self._set_storage(address, storage) - self._world_state[address] = {} - self._world_state[address]["nonce"] = nonce - self._world_state[address]["balance"] = balance - self._world_state[address]["storage"] = storage - self._world_state[address]["code"] = code + if code is not None: + self.set_code(address, code) # adds hash of new address data = binascii.unhexlify("{:064x}{:064x}".format(address, 0)) @@ -3347,13 +3346,13 @@ def _process_pending_transaction(self): def dump(self, stream, state, mevm, message): from ..ethereum.manticore import calculate_coverage, flagged - blockchain = state.platform + blockchain: EVMWorld = state.platform last_tx = blockchain.last_transaction stream.write("Message: %s\n" % message) stream.write("Last exception: %s\n" % state.context.get("last_exception", "None")) - if last_tx: + if last_tx and "evm.trace" in state.context: at_runtime = last_tx.sort != "CREATE" address, offset, at_init = state.context.get("evm.trace", ((None, None, None),))[-1] assert last_tx.result is not None or at_runtime != at_init @@ -3385,17 +3384,9 @@ def dump(self, stream, state, mevm, message): balance = state.solve_one(balance, constrain=True) stream.write("Balance: %d %s\n" % (balance, flagged(is_balance_symbolic))) - storage = blockchain.get_storage(account_address) - concrete_indexes = set() - for sindex in storage.written: - concrete_indexes.add(state.solve_one(sindex, constrain=True)) - - for index in concrete_indexes: - stream.write( - f"storage[{index:x}] = {state.solve_one(storage[index], constrain=True):x}" - ) - storage = blockchain.get_storage(account_address) - stream.write("Storage: %s\n" % translate_to_smtlib(storage, use_bindings=False)) + storage = blockchain._get_storage(state.constraints, account_address) + storage.dump(stream, state) + stream.write("Storage: %s\n" % translate_to_smtlib(storage._data, use_bindings=False)) if consts.sha3 is consts.sha3.concretize: all_used_indexes = [] @@ -3403,18 +3394,19 @@ def dump(self, stream, state, mevm, message): # make a free symbolic idex that could address any storage slot index = temp_cs.new_bitvec(256) # get the storage for account_address - storage = blockchain.get_storage(account_address) + storage = blockchain._get_storage(temp_cs, account_address) # we are interested only in used slots - # temp_cs.add(storage.get(index) != 0) - temp_cs.add(storage.is_known(index)) + temp_cs.add(storage._data.is_known(index) != 0) # Query the solver to get all storage indexes with used slots all_used_indexes = SelectedSolver.instance().get_all_values(temp_cs, index) if all_used_indexes: stream.write("Storage:\n") for i in all_used_indexes: - value = storage.get(i) - is_storage_symbolic = issymbolic(value) + value = simplify(storage._data.get(i)) + is_storage_symbolic = issymbolic(value) and not isinstance( + value, BitVecConstant + ) stream.write( "storage[%x] = %x %s\n" % ( @@ -3433,7 +3425,7 @@ def dump(self, stream, state, mevm, message): runtime_trace = set( ( pc - for contract, pc, at_init in state.context["evm.trace"] + for contract, pc, at_init in state.context.get("evm.trace", []) if address == contract and not at_init ) ) diff --git a/manticore/platforms/evm_world_state.py b/manticore/platforms/evm_world_state.py new file mode 100644 index 000000000..0021a1b4f --- /dev/null +++ b/manticore/platforms/evm_world_state.py @@ -0,0 +1,465 @@ +import logging +import copy +from abc import abstractmethod +from eth_typing import ChecksumAddress, URI +from io import TextIOBase +from typing import Dict, List, Optional, Set, Tuple, Union +from urllib.parse import ParseResult, urlparse +from web3 import Web3 +from ..core.smtlib import ( + Array, + BitVec, + BitVecConstant, + ConstraintSet, +) +from ..ethereum.state import State +from ..exceptions import EthereumError + +logger = logging.getLogger(__name__) + + +class Storage: + def __init__( + self, constraints: ConstraintSet, address: int, items: Optional[Dict[int, int]] = None + ): + """ + :param constraints: the ConstraintSet with which this Storage object is associated + :param address: the address that owns this storage + :param items: optional items to populate the storage with + """ + self._data = constraints.new_array( + index_bits=256, + value_bits=256, + name=f"STORAGE_{address:x}", + avoid_collisions=True, + # sam.moelius: The use of default here creates unnecessary if-then-elses. See + # ArrayProxy.get in expression.py. + # default=0, + ) + if items is not None: + for key, value in items.items(): + self.set(key, value) + + def __copy__(self): + other = Storage.__new__(Storage) + other._data = copy.copy(self._data) + return other + + def __getitem__(self, offset: Union[int, BitVec]) -> Union[int, BitVec]: + return self.get(offset, 0) + + def get(self, offset: Union[int, BitVec], default: Union[int, BitVec]) -> Union[int, BitVec]: + return self._data.get(offset, default) + + def set(self, offset: Union[int, BitVec], value: Union[int, BitVec]): + self._data[offset] = value + + def get_items(self) -> List[Tuple[Union[int, BitVec], Union[int, BitVec]]]: + return self._data.get_items() + + def dump(self, stream: TextIOBase, state: State): + concrete_indexes = set() + for sindex in self._data.written: + concrete_indexes.add(state.solve_one(sindex, constrain=True)) + + for index in concrete_indexes: + stream.write( + f"storage[{index:x}] = {state.solve_one(self._data[index], constrain=True):x}\n" + ) + + +#################################################################################################### + + +class WorldState: + @abstractmethod + def is_remote(self) -> bool: + pass + + @abstractmethod + def accounts(self) -> Set[int]: + pass + + @abstractmethod + def get_nonce(self, address: int) -> Union[int, BitVec]: + pass + + @abstractmethod + def get_balance(self, address: int) -> Union[int, BitVec]: + pass + + @abstractmethod + def has_storage(self, address: int) -> bool: + pass + + @abstractmethod + def get_storage(self, address: int) -> Optional[Storage]: + pass + + # sam.moelius: The addition of the constraints parameter is future-proofing. We might want + # to create a new Storage object in OverlayWorldState when self._storage.get(address) is None. + @abstractmethod + def get_storage_data( + self, constraints: ConstraintSet, address: int, offset: Union[int, BitVec] + ) -> Union[int, BitVec]: + pass + + @abstractmethod + def get_code(self, address: int) -> Union[bytes, Array]: + pass + + @abstractmethod + def get_blocknumber(self) -> Union[int, BitVec]: + pass + + @abstractmethod + def get_timestamp(self) -> Union[int, BitVec]: + pass + + @abstractmethod + def get_difficulty(self) -> Union[int, BitVec]: + pass + + @abstractmethod + def get_gaslimit(self) -> Union[int, BitVec]: + pass + + @abstractmethod + def get_coinbase(self) -> Union[int, BitVec]: + pass + + +#################################################################################################### + + +class DefaultWorldState(WorldState): + def is_remote(self) -> bool: + return False + + def accounts(self) -> Set[int]: + return set() + + def get_nonce(self, address: int) -> int: + return 0 + + def get_balance(self, address: int) -> int: + return 0 + + def has_storage(self, address: int) -> bool: + return False + + def get_storage(self, address: int) -> Optional[Storage]: + raise NotImplementedError + + def get_storage_data( + self, constraints: ConstraintSet, address: int, offset: Union[int, BitVec] + ) -> int: + return 0 + + def get_code(self, address: int) -> bytes: + return bytes() + + def get_blocknumber(self) -> int: + # assume initial byzantium block + return 4370000 + + def get_timestamp(self) -> int: + # 1524785992; // Thu Apr 26 23:39:52 UTC 2018 + return 1524785992 + + def get_difficulty(self) -> int: + return 0 + + def get_gaslimit(self) -> int: + return 0 + + def get_coinbase(self) -> int: + return 0 + + +#################################################################################################### + + +class Endpoint: + def __init__(self, blocknumber: int, warned: bool): + self.blocknumber = blocknumber + self.warned = warned + + +_endpoints: Dict[str, Endpoint] = {} + + +def _web3_address(address: int) -> ChecksumAddress: + return Web3.toChecksumAddress("0x%0.40x" % address) + + +# sam.moelius: Notes: +# +# 1. https://github.com/ethereum/wiki/wiki/JSON-RPC lists the kinds of information that an Ethereum +# node can provide over JSON RPC. +# +# 2. The accounts and get_storage methods do not make sense when using JSON RPC. IMHO, we should +# program to the least common denominator. In that sense, we should see whether the accounts and +# get_storage methods could be eliminated. + + +class RemoteWorldState(WorldState): + def __init__(self, url: str): + actual = urlparse(url) + expected = ParseResult(scheme="", netloc="", path=url, params="", query="", fragment="") + if actual != expected: + raise EthereumError("URL must be of the form 'IP:PORT': " + url) + self._url = url + + def _web3(self) -> Web3: + # sam.moelius: Force WebsocketProvider.__init__ to call _get_threaded_loop. The existing + # "threaded loop" could be leftover from a fork, in which case it will not work. + Web3.WebsocketProvider._loop = None + web3 = Web3(Web3.WebsocketProvider(URI("ws://" + self._url))) + blocknumber = None + try: + blocknumber = web3.eth.blockNumber + except ConnectionRefusedError as e: + raise EthereumError("Could not connect to %s: %s" % (self._url, e.args[1])) + endpoint = _endpoints.get(self._url) + if endpoint is None: + endpoint = Endpoint(blocknumber, False) + _endpoints[self._url] = endpoint + logger.info("Connected to %s (blocknumber = %d)", self._url, blocknumber) + if endpoint.blocknumber != blocknumber: + if not endpoint.warned: + logger.warning( + "%s blocknumber has changed: %d != %d---someone is using the endpoint besides us", + self._url, + endpoint.blocknumber, + blocknumber, + ) + endpoint.warned = True + return web3 + + def is_remote(self) -> bool: + return True + + def accounts(self) -> Set[int]: + raise NotImplementedError + + def get_nonce(self, address: int) -> int: + return self._web3().eth.getTransactionCount(_web3_address(address)) + + def get_balance(self, address: int) -> int: + return self._web3().eth.getBalance(_web3_address(address)) + + def has_storage(self, address: int) -> bool: + raise NotImplementedError + + def get_storage(self, address) -> Storage: + raise NotImplementedError + + def get_storage_data( + self, constraints: ConstraintSet, address: int, offset: Union[int, BitVec] + ) -> int: + if not isinstance(offset, int): + raise NotImplementedError + return int.from_bytes(self._web3().eth.getStorageAt(_web3_address(address), offset), "big") + + def get_code(self, address: int) -> bytes: + return self._web3().eth.getCode(_web3_address(address)) + + def get_blocknumber(self) -> int: + return self._web3().eth.blockNumber + + def get_timestamp(self) -> int: + return self._web3().eth.getBlock("latest")["timestamp"] + + def get_difficulty(self) -> int: + return self._web3().eth.getBlock("latest")["difficulty"] + + def get_gaslimit(self) -> int: + return self._web3().eth.getBlock("latest")["gasLimit"] + + def get_coinbase(self) -> int: + return int(self._web3().eth.coinbase, 16) + + +#################################################################################################### + + +class OverlayWorldState(WorldState): + """ + If we decide to cache results returned from a RemoteWorldState, then they should NOT be cached + within an overlay. The reason is that this could affect the results of subsequent operations. + Consider a call to get_storage_data followed by a call to has_storage. If nothing was written + to storage within the overlay, then the call to has_storage will throw an exception. But if the + result of the call to get_storage_data was cached in the overlay, then no exception would be + thrown. + """ + + def __init__(self, underlay: WorldState): + self._underlay: WorldState = underlay + self._deleted_accounts: Set[int] = set() + self._nonce: Dict[int, Union[int, BitVec]] = {} + self._balance: Dict[int, Union[int, BitVec]] = {} + self._storage: Dict[int, Storage] = {} + self._code: Dict[int, Union[bytes, Array]] = {} + self._blocknumber: Optional[Union[int, BitVec]] = None + self._timestamp: Optional[Union[int, BitVec]] = None + self._difficulty: Optional[Union[int, BitVec]] = None + self._gaslimit: Optional[Union[int, BitVec]] = None + self._coinbase: Optional[Union[int, BitVec]] = None + + def is_remote(self) -> bool: + return self._underlay.is_remote() + + def accounts(self) -> Set[int]: + accounts: Set[int] = set() + try: + accounts = self._underlay.accounts() + except NotImplementedError: + pass + return ( + accounts + | self._nonce.keys() + | self._balance.keys() + | self._storage.keys() + | self._code.keys() + ) + + def get_nonce(self, address: int) -> Union[int, BitVec]: + if address in self._nonce: + return self._nonce[address] + else: + return self._underlay.get_nonce(address) + + def get_balance(self, address: int) -> Union[int, BitVec]: + if address in self._balance: + return self._balance[address] + else: + return self._underlay.get_balance(address) + + def has_storage(self, address: int) -> bool: + dirty = False + try: + dirty = self._underlay.has_storage(address) + except NotImplementedError: + pass + storage = self._storage.get(address) + if storage is not None: + dirty = dirty or len(storage._data.written) > 0 + return dirty + + def get_storage(self, address: int) -> Optional[Storage]: + storage = None + try: + storage = self._underlay.get_storage(address) + except NotImplementedError: + pass + # sam.moelius: Rightfully, the overlay's storage should be merged into the underlay's + # storage. However, this is not currently implemented. + if storage is not None: + # sam.moelius: At present, the get_storage methods of both DefaultWorldState and + # RemoteWorldState raise NotImplementedError. So this exception should be unreachable. + raise NotImplementedError("Merging of storage is not implemented") + storage = self._storage.get(address) + return storage + + def get_storage_data( + self, constraints: ConstraintSet, address: int, offset: Union[int, BitVec] + ) -> Union[int, BitVec]: + value: Union[int, BitVec] = 0 + # sam.moelius: If the account was ever deleted, then ignore the underlay's storage. + if address not in self._deleted_accounts: + try: + value = self._underlay.get_storage_data(constraints, address, offset) + except NotImplementedError: + pass + storage = self._storage.get(address) + if storage is not None: + value = storage.get(offset, value) + return value + + def get_code(self, address: int) -> Union[bytes, Array]: + if address in self._code: + return self._code[address] + else: + return self._underlay.get_code(address) + + def get_blocknumber(self) -> Union[int, BitVec]: + if self._blocknumber is not None: + return self._blocknumber + else: + return self._underlay.get_blocknumber() + + def get_timestamp(self) -> Union[int, BitVec]: + if self._timestamp is not None: + return self._timestamp + else: + return self._underlay.get_timestamp() + + def get_difficulty(self) -> Union[int, BitVec]: + if self._difficulty is not None: + return self._difficulty + else: + return self._underlay.get_difficulty() + + def get_gaslimit(self) -> Union[int, BitVec]: + if self._gaslimit is not None: + return self._gaslimit + else: + return self._underlay.get_gaslimit() + + def get_coinbase(self) -> Union[int, BitVec]: + if self._coinbase is not None: + return self._coinbase + else: + return self._underlay.get_coinbase() + + def delete_account(self, constraints: ConstraintSet, address: int): + default_world_state = DefaultWorldState() + self._nonce[address] = default_world_state.get_nonce(address) + self._balance[address] = default_world_state.get_balance(address) + self._storage[address] = Storage(constraints, address) + self._code[address] = default_world_state.get_code(address) + self._deleted_accounts.add(address) + + def set_nonce(self, address: int, value: Union[int, BitVec]): + self._nonce[address] = value + + def set_balance(self, address: int, value: Union[int, BitVec]): + self._balance[address] = value + + def set_storage(self, address: int, storage: Optional[Storage]): + if storage is None: + self._storage.pop(address, None) + else: + self._storage[address] = storage + + def set_storage_data( + self, + constraints: ConstraintSet, + address: int, + offset: Union[int, BitVec], + value: Union[int, BitVec], + ): + storage = self._storage.get(address) + if storage is None: + storage = Storage(constraints, address) + self._storage[address] = storage + storage.set(offset, value) + + def set_code(self, address: int, code: Union[bytes, Array]): + self._code[address] = code + + def set_blocknumber(self, value: Union[int, BitVec]): + self._blocknumber = value + + def set_timestamp(self, value: Union[int, BitVec]): + self._timestamp = value + + def set_difficulty(self, value: Union[int, BitVec]): + self._difficulty = value + + def set_gaslimit(self, value: Union[int, BitVec]): + self._gaslimit = value + + def set_coinbase(self, value: Union[int, BitVec]): + self._coinbase = value diff --git a/mypy.ini b/mypy.ini index 82669511c..ddb4cf9ce 100644 --- a/mypy.ini +++ b/mypy.ini @@ -42,3 +42,6 @@ ignore_missing_imports = True [mypy-psutil.*] ignore_missing_imports = True + +[mypy-web3.*] +ignore_missing_imports = True diff --git a/setup.py b/setup.py index d40169394..f22d9f490 100644 --- a/setup.py +++ b/setup.py @@ -59,6 +59,7 @@ def rtd_dependent_deps(): "rlp", "crytic-compile>=0.1.1", "wasm", + "web3", "dataclasses; python_version < '3.7'", "pyevmasm>=0.2.3", "psutil", diff --git a/tests/auto_generators/make_VMTests.py b/tests/auto_generators/make_VMTests.py index bd32b30a4..1aa35daf4 100644 --- a/tests/auto_generators/make_VMTests.py +++ b/tests/auto_generators/make_VMTests.py @@ -8,10 +8,10 @@ git clone https://github.com/ethereum/tests --depth=1 ## Get help -python make_VMTest.py --help +python make_VMTests.py --help ## Generate concrete tests: -for i in tests/BlockchainTests/ValidBlocks/VMTests/*/*json; do python make_VMTest.py -i $i --fork Istanbul -o ethereum_vm/VMTests_concrete; done +for i in tests/BlockchainTests/ValidBlocks/VMTests/*/*.json; do python make_VMTests.py -i $i --fork Istanbul -o ethereum_vm/VMTests_concrete; done """ import argparse @@ -66,6 +66,7 @@ def gen_header(testcases): import unittest from binascii import unhexlify from manticore import ManticoreEVM, Plugin +from manticore.core.smtlib import to_constant from manticore.utils import config ''' @@ -214,7 +215,7 @@ def did_close_transaction_callback(self, state, tx): body += f""" for state in m.all_states: world = state.platform - self.assertEqual(used_gas_plugin.used_gas, {blockheader['gasUsed']}) + self.assertEqual(to_constant(used_gas_plugin.used_gas), {blockheader['gasUsed']}) world.end_block()""" diff --git a/tests/ethereum/test_general.py b/tests/ethereum/test_general.py index 998aadd8f..1b0de2e0b 100644 --- a/tests/ethereum/test_general.py +++ b/tests/ethereum/test_general.py @@ -1,6 +1,7 @@ import binascii import unittest from contextlib import contextmanager +from io import StringIO from pathlib import Path import os @@ -31,6 +32,7 @@ from manticore.ethereum.plugins import FilterFunctions from manticore.ethereum.solidity import SolidityMetadata from manticore.platforms import evm +from manticore.platforms.evm_world_state import OverlayWorldState, WorldState from manticore.platforms.evm import EVMWorld, ConcretizeArgument, concretized_args, Return, Stop from manticore.utils.deprecated import ManticoreDeprecationWarning @@ -1348,6 +1350,65 @@ def will_evm_execute_instruction_callback(self, state, i, *args, **kwargs): self.assertEqual(aplug.context.get("xcount", 0), 112) +class EthWorldStateTests(unittest.TestCase): + class CustomWorldState(WorldState): + def accounts(self): + raise NotImplementedError + + def test_custom_world_state(self): + world_state = EthWorldStateTests.CustomWorldState() + m = ManticoreEVM(world_state=world_state) + self.assertIsInstance(m.world._world_state, OverlayWorldState) + # sam.moelius: You cannot use assertEqual because the world may be have been deserialized. + self.assertIsInstance(m.world._world_state._underlay, EthWorldStateTests.CustomWorldState) + + def test_init_blocknumber(self): + constraints = ConstraintSet() + self.assertEqual(evm.EVMWorld(constraints).block_number(), 4370000) + self.assertEqual(evm.EVMWorld(constraints, blocknumber=4370001).block_number(), 4370001) + + def test_init_timestamp(self): + constraints = ConstraintSet() + self.assertEqual(evm.EVMWorld(constraints).block_timestamp(), 1524785992) + self.assertEqual( + evm.EVMWorld(constraints, timestamp=1524785993).block_timestamp(), 1524785993 + ) + + def test_init_difficulty(self): + constraints = ConstraintSet() + self.assertEqual(evm.EVMWorld(constraints).block_difficulty(), 0x200) + self.assertEqual(evm.EVMWorld(constraints, difficulty=0x201).block_difficulty(), 0x201) + + def test_init_gaslimit(self): + constraints = ConstraintSet() + self.assertEqual(evm.EVMWorld(constraints).block_gaslimit(), 0x7FFFFFFF) + self.assertEqual( + evm.EVMWorld(constraints, gaslimit=0x80000000).block_gaslimit(), 0x80000000 + ) + + def test_init_coinbase(self): + constraints = ConstraintSet() + self.assertEqual(evm.EVMWorld(constraints).block_coinbase(), 0) + self.assertEqual( + evm.EVMWorld( + constraints, coinbase=0x111111111111111111111111111111111111111 + ).block_coinbase(), + 0x111111111111111111111111111111111111111, + ) + + def test_dump(self): + m = ManticoreEVM() + address = int(m.create_account()) + self.assertEqual(m.count_ready_states(), 1) + for state in m.ready_states: + xx = state.constraints.new_bitvec(256, name="x") + state.constraints.add(xx == 0x20) + m.world.set_storage_data(address, xx, 1) + output = StringIO() + m.world.dump(output, state, m, "") + self.assertIn("storage[20] = 1", output.getvalue()) + + class EthHelpersTest(unittest.TestCase): def setUp(self): self.bv = BitVec(256) @@ -1714,6 +1775,9 @@ def test_gas_check(self): world.create_account( address=0x111111111111111111111111111111111111111, code=EVMAsm.assemble(asm_acc) ) + self.assertIn(0x111111111111111111111111111111111111111, world) + self.assertTrue(world.has_code(0x111111111111111111111111111111111111111)) + self.assertEqual(world.get_nonce(0x111111111111111111111111111111111111111), 1) world.create_account( address=0x222222222222222222222222222222222222222, balance=10000000000000 ) @@ -1728,6 +1792,8 @@ def test_gas_check(self): except TerminateState as e: result = str(e) self.assertEqual(result, "SELFDESTRUCT") + self.assertFalse(world.has_code(0x111111111111111111111111111111111111111)) + self.assertEqual(world.get_nonce(0x111111111111111111111111111111111111111), 0) def test_selfdestruct(self): with disposable_mevm() as m: diff --git a/tests/ethereum/test_storage.py b/tests/ethereum/test_storage.py new file mode 100644 index 000000000..6df203896 --- /dev/null +++ b/tests/ethereum/test_storage.py @@ -0,0 +1,302 @@ +import unittest + +from manticore.core.smtlib import ( + ConstraintSet, + Version, + get_depth, + Operators, + translate_to_smtlib, + simplify, + arithmetic_simplify, + constant_folder, +) +from manticore.core.smtlib.solver import Z3Solver +from manticore.core.smtlib.expression import * +from manticore.platforms.evm_world_state import ( + DefaultWorldState, + DefaultWorldState, + OverlayWorldState, +) +from manticore.utils.helpers import pickle_dumps + +ADDRESS = 0x111111111111111111111111111111111111111 + +# sam.moelius: These tests were shamelessly copied from test_smtlibv2.py. + + +class StorageTest(unittest.TestCase): + _multiprocess_can_split_ = True + + def setUp(self): + self.solver = Z3Solver.instance() + + def assertItemsEqual(self, a, b): + # Required for Python3 compatibility + self.assertEqual(sorted(a), sorted(b)) + + def tearDown(self): + del self.solver + + def testBasicStorage(self): + cs = ConstraintSet() + # make storage + world_state = OverlayWorldState(DefaultWorldState()) + # make free 256bit bitvectors + key = cs.new_bitvec(256) + value = cs.new_bitvec(256) + other_key = cs.new_bitvec(256) + other_value = cs.new_bitvec(256) + # store two symbolic values at symbolic offsets + world_state.set_storage_data(cs, ADDRESS, key, value) + world_state.set_storage_data(cs, ADDRESS, other_key, other_value) + + # assert that the storage is 'A' at key position + cs.add(world_state.get_storage_data(cs, ADDRESS, key) == ord("A")) + + # let's restrict key to be greater than 1000 + cs.add(key.ugt(1000)) + with cs as temp_cs: + # 1001 position of storage can be 'A' + temp_cs.add(world_state.get_storage_data(cs, ADDRESS, 1001) == ord("A")) + self.assertTrue(self.solver.check(temp_cs)) + + with cs as temp_cs: + # 1001 position of storage can also be 'B' + temp_cs.add(world_state.get_storage_data(cs, ADDRESS, 1001) == ord("B")) + self.assertTrue(self.solver.check(temp_cs)) + + with cs as temp_cs: + # but if it is 'B' ... + temp_cs.add(world_state.get_storage_data(cs, ADDRESS, 1001) == ord("B")) + # then key can not be 1001 + temp_cs.add(key == 1001) + self.assertFalse(self.solver.check(temp_cs)) + + with cs as temp_cs: + # If 1001 position is 'B' ... + temp_cs.add(world_state.get_storage_data(cs, ADDRESS, 1001) == ord("B")) + # then key can be 1000 for ex.. + temp_cs.add(key == 1002) + self.assertTrue(self.solver.check(temp_cs)) + + def testBasicStorage256(self): + cs = ConstraintSet() + # make storage + world_state = OverlayWorldState(DefaultWorldState()) + # make free 256bit bitvectors + key = cs.new_bitvec(256) + value = cs.new_bitvec(256) + other_key = cs.new_bitvec(256) + other_value = cs.new_bitvec(256) + # store two symbolic values at symbolic offsets + world_state.set_storage_data(cs, ADDRESS, key, value) + world_state.set_storage_data(cs, ADDRESS, other_key, other_value) + + # assert that the storage is 111...111 at key position + cs.add( + world_state.get_storage_data(cs, ADDRESS, key) + == 11111111111111111111111111111111111111111111 + ) + # let's restrict key to be greater than 1000 + cs.add(key.ugt(1000)) + + with cs as temp_cs: + # 1001 position of storage can be 111...111 + temp_cs.add( + world_state.get_storage_data(cs, ADDRESS, 1001) + == 11111111111111111111111111111111111111111111 + ) + self.assertTrue(self.solver.check(temp_cs)) + + with cs as temp_cs: + # 1001 position of storage can also be 222...222 + temp_cs.add( + world_state.get_storage_data(cs, ADDRESS, 1001) + == 22222222222222222222222222222222222222222222 + ) + self.assertTrue(self.solver.check(temp_cs)) + + with cs as temp_cs: + # but if it is 222...222 ... + temp_cs.add( + world_state.get_storage_data(cs, ADDRESS, 1001) + == 22222222222222222222222222222222222222222222 + ) + # then key can not be 1001 + temp_cs.add(key == 1001) + self.assertFalse(self.solver.check(temp_cs)) + + with cs as temp_cs: + # If 1001 position is 222...222 ... + temp_cs.add( + world_state.get_storage_data(cs, ADDRESS, 1001) + == 22222222222222222222222222222222222222222222 + ) + # then key can be 1002 for ex.. + temp_cs.add(key == 1002) + self.assertTrue(self.solver.check(temp_cs)) + + def testBasicStorageStore(self): + cs = ConstraintSet() + # make storage + world_state = OverlayWorldState(DefaultWorldState()) + # make free 256bit bitvectors + key = cs.new_bitvec(256) + value = cs.new_bitvec(256) + other_key = cs.new_bitvec(256) + other_value = cs.new_bitvec(256) + # store two symbolic values at symbolic offsets + world_state.set_storage_data(cs, ADDRESS, key, value) + world_state.set_storage_data(cs, ADDRESS, other_key, other_value) + + # assert that the storage is 'A' at key position + world_state.set_storage_data(cs, ADDRESS, key, ord("A")) + # let's restrict key to be greater than 1000 + cs.add(key.ugt(1000)) + + # 1001 position of storage can be 'A' + self.assertTrue( + self.solver.can_be_true(cs, world_state.get_storage_data(cs, ADDRESS, 1001) == ord("A")) + ) + + # 1001 position of storage can be 'B' + self.assertTrue( + self.solver.can_be_true(cs, world_state.get_storage_data(cs, ADDRESS, 1001) == ord("B")) + ) + + with cs as temp_cs: + # but if it is 'B' ... + temp_cs.add(world_state.get_storage_data(cs, ADDRESS, 1001) == ord("B")) + # then key can not be 1001 + temp_cs.add(key == 1001) + self.assertFalse(self.solver.check(temp_cs)) + + with cs as temp_cs: + # If 1001 position is 'B' ... + temp_cs.add(world_state.get_storage_data(cs, ADDRESS, 1001) == ord("B")) + # then key can be 1002 for ex.. + temp_cs.add(key != 1002) + self.assertTrue(self.solver.check(temp_cs)) + + def testClosedWorldAssumption(self): + cs = ConstraintSet() + # make storage + world_state = OverlayWorldState(DefaultWorldState()) + # make free 256bit bitvectors + key = cs.new_bitvec(256) + value = cs.new_bitvec(256) + other_key = cs.new_bitvec(256) + other_value = cs.new_bitvec(256) + # store two symbolic values at symbolic offsets + world_state.set_storage_data(cs, ADDRESS, key, value) + world_state.set_storage_data(cs, ADDRESS, other_key, other_value) + + with cs as temp_cs: + # sam.moelius: The value at 1001 can be 'A' and the value at 1002 can be 'B'. + temp_cs.add(world_state.get_storage_data(cs, ADDRESS, 1001) == ord("A")) + temp_cs.add(world_state.get_storage_data(cs, ADDRESS, 1002) == ord("B")) + self.assertTrue(self.solver.check(temp_cs)) + + with cs as temp_cs: + # sam.moelius: If the value at 1001 is 'A' and the value at 1002 is 'B', then 'C' + # cannot appear anywhere in storage. + temp_cs.add(world_state.get_storage_data(cs, ADDRESS, 1001) == ord("A")) + temp_cs.add(world_state.get_storage_data(cs, ADDRESS, 1002) == ord("B")) + temp_cs.add(world_state.get_storage_data(cs, ADDRESS, key) == ord("C")) + self.assertFalse(self.solver.check(temp_cs)) + + def testBasicStorageSymbIdx(self): + cs = ConstraintSet() + world_state = OverlayWorldState(DefaultWorldState()) + key = cs.new_bitvec(256, name="key") + index = cs.new_bitvec(256, name="index") + + world_state.set_storage_data(cs, ADDRESS, key, 1) # Write 1 to a single location + + cs.add( + world_state.get_storage_data(cs, ADDRESS, index) != 0 + ) # Constrain index so it selects that location + + cs.add(index != key) + # key and index are the same there is only one slot in 1 + self.assertFalse(self.solver.check(cs)) + + def testBasicStorageSymbIdx2(self): + cs = ConstraintSet() + world_state = OverlayWorldState(DefaultWorldState()) + key = cs.new_bitvec(256, name="key") + index = cs.new_bitvec(256, name="index") + + world_state.set_storage_data(cs, ADDRESS, key, 1) # Write 1 to a single location + cs.add( + world_state.get_storage_data(cs, ADDRESS, index) != 0 + ) # Constrain index so it selects that location + a_index = self.solver.get_value(cs, index) # get a concrete solution for index + cs.add( + world_state.get_storage_data(cs, ADDRESS, a_index) != 0 + ) # now storage must have something at that location + cs.add(a_index != index) # remove it from the solutions + + # It should not be another solution for index + self.assertFalse(self.solver.check(cs)) + + def testBasicStorageSymbIdx3(self): + cs = ConstraintSet() + world_state = OverlayWorldState(DefaultWorldState()) + key = cs.new_bitvec(256, name="key") + index = cs.new_bitvec(256, name="index") + + world_state.set_storage_data(cs, ADDRESS, 0, 1) # Write 1 to first location + world_state.set_storage_data( + cs, ADDRESS, key, 2 + ) # Write 2 to a symbolic (potentially any (potentially 0))location + + solutions = self.solver.get_all_values( + cs, world_state.get_storage_data(cs, ADDRESS, 0) + ) # get a concrete solution for index + self.assertItemsEqual(solutions, (1, 2)) + + solutions = self.solver.get_all_values( + cs, world_state.get_storage_data(cs, ADDRESS, 0) + ) # get a concrete solution for index 0 + self.assertItemsEqual(solutions, (1, 2)) + + solutions = self.solver.get_all_values( + cs, world_state.get_storage_data(cs, ADDRESS, 1) + ) # get a concrete solution for index 1 + self.assertItemsEqual(solutions, (0, 2)) + + # sam.moelius: Nothing that could be 12345 has been written to storage. + self.assertFalse( + self.solver.can_be_true(cs, world_state.get_storage_data(cs, ADDRESS, 1) == 12345) + ) + + # sam.moelius: Write a symbolic value at symbolic offset key. + value = cs.new_bitvec(256, name="value") + world_state.set_storage_data(cs, ADDRESS, key, value) + + # sam.moelius: Could 12345 appear at offset 1? Yes, because value could be 12345 and key could be 1. + self.assertTrue( + self.solver.can_be_true(cs, world_state.get_storage_data(cs, ADDRESS, 1) == 12345) + ) + + def testBasicPickle(self): + import pickle + + cs = ConstraintSet() + + # make storage + world_state = OverlayWorldState(DefaultWorldState()) + # make free 256bit bitvector + key = cs.new_bitvec(256) + + # assert that the storage is 'A' at key position + world_state.set_storage_data(cs, ADDRESS, key, ord("A")) + # let's restrict key to be greater than 1000 + cs.add(key.ugt(1000)) + cs = pickle.loads(pickle_dumps(cs)) + self.assertTrue(self.solver.check(cs)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/ethereum_truffle/__init__.py b/tests/ethereum_truffle/__init__.py new file mode 100644 index 000000000..3755e783b --- /dev/null +++ b/tests/ethereum_truffle/__init__.py @@ -0,0 +1 @@ +# DO NOT DELETE diff --git a/tests/ethereum_truffle/test_truffle.py b/tests/ethereum_truffle/test_truffle.py new file mode 100644 index 000000000..62e3bf5ce --- /dev/null +++ b/tests/ethereum_truffle/test_truffle.py @@ -0,0 +1,266 @@ +import os +import re +import shutil +import subprocess +import sys +import tempfile +import unittest + +from manticore.ethereum import ABI + +DIRPATH = os.path.dirname(__file__) + +# TLDR: when we launch `python -m manticore` and one uses PyCharm remote interpreter +# the `python` might not refer to proper interpreter. The `/proc/self/exe` is a workaround +# so one doesn't have to set up virtualenv in a remote interpreter. +PYTHON_BIN = sys.executable + +# sam.moelius: All of these tests assume that an Ethereum node is listening on 127.0.0.1:7545. +URL = "127.0.0.1:7545" + + +class EthTruffleTests(unittest.TestCase): + def setUp(self): + # Create a temporary directory + self.test_dir = tempfile.mkdtemp() + + def tearDown(self): + # Remove the directory after the test + shutil.rmtree(self.test_dir) + + def test_bad_ip(self): + workspace = os.path.join(self.test_dir, "workspace") + + cmd = [ + PYTHON_BIN, + "-m", + "manticore", + "--workspace", + workspace, + "--no-color", + "--rpc", + "127.0.0.2:7545", + "--txtarget", + "0x111111111111111111111111111111111111111", + ] + mcore = subprocess.Popen(cmd, stdout=subprocess.PIPE) + + # sam.moelius: Manticore should have failed to connect. + self.assertRegex( + mcore.stdout.read().decode(), + r"\bm\.main:ERROR: \"Could not connect to 127.0.0.2:7545: Connect call failed \('127.0.0.2', 7545\)\"", + ) + + # sam.moelius: Wait for manticore to finish. + self.assertEqual(mcore.wait(), 0) + + def test_bad_port(self): + workspace = os.path.join(self.test_dir, "workspace") + + cmd = [ + PYTHON_BIN, + "-m", + "manticore", + "--workspace", + workspace, + "--no-color", + "--rpc", + "127.0.0.1:7546", + "--txtarget", + "0x111111111111111111111111111111111111111", + ] + mcore = subprocess.Popen(cmd, stdout=subprocess.PIPE) + + # sam.moelius: Manticore should have failed to connect. + self.assertRegex( + mcore.stdout.read().decode(), + r"\bm\.main:ERROR: \"Could not connect to 127.0.0.1:7546: Connect call failed \('127.0.0.1', 7546\)\"", + ) + + # sam.moelius: Wait for manticore to finish. + self.assertEqual(mcore.wait(), 0) + + def test_basic(self): + dir = os.path.abspath(os.path.join(DIRPATH, "truffle", "basic")) + workspace = os.path.join(self.test_dir, "workspace") + + os.chdir(dir) + + cmd = ["truffle", "deploy"] + output = subprocess.check_output(cmd).decode() + matches = [x for x in re.finditer(r"> contract address:\s*(0x\S*)", output)] + self.assertEqual(len(matches), 2) + address = matches[1].group(1) + + cmd = [ + PYTHON_BIN, + "-m", + "manticore", + "--workspace", + workspace, + "--no-color", + "--rpc", + URL, + "--txtarget", + address, + "--txlimit", + "1", + ] + subprocess.check_call(cmd) + + # sam.moelius: Manticore should have found a call to guess_x with the value of x, and a call + # to guess_y with the value of y. + cmd = [ + "grep", + "-r", + "--include=*.tx", + "^Data: 0x" + + ABI.function_selector("guess_x(uint256)").hex() + + "%0.64x" % int.from_bytes("constructor".encode(), byteorder="big"), + workspace, + ] + subprocess.check_call(cmd) + + cmd = [ + "grep", + "-r", + "--include=*.tx", + "^Data: 0x" + + ABI.function_selector("guess_y(uint256)").hex() + + "%0.64x" % int.from_bytes("set_y".encode(), byteorder="big"), + workspace, + ] + subprocess.check_call(cmd) + + # sam.moelius: test_predeployed is similar to test_basic. The difference is that + # test_predeployed involves two contracts: one deployed by truffle, and one deployed + # (internally) by Manticore. + def test_predeployed(self): + dir = os.path.abspath(os.path.join(DIRPATH, "truffle", "predeployed")) + workspace = os.path.join(self.test_dir, "workspace") + + os.chdir(dir) + + cmd = ["truffle", "deploy"] + output = subprocess.check_output(cmd).decode() + matches = [x for x in re.finditer(r"> contract address:\s*(0x\S*)", output)] + self.assertEqual(len(matches), 2) + address = matches[1].group(1) + + cmd = [ + PYTHON_BIN, + "-m", + "manticore", + "--workspace", + workspace, + "--no-color", + "--rpc", + URL, + "--txlimit", + "1", + "--contract", + "Guesser", + """ + interface Predeployed { + function x() external returns (uint256); + function y() external returns (uint256); + } + contract Guesser { + function guess_x(uint256 _x) external { + require(Predeployed(%s).x() == _x, "x != _x"); + } + function guess_y(uint256 _y) external { + require(Predeployed(%s).y() == _y, "y != _y"); + } + } + """ + % (address, address), + ] + subprocess.check_call(cmd) + + # sam.moelius: Manticore should have found a call to guess_x with the value of x, and a call + # to guess_y with the value of y. + cmd = [ + "grep", + "-r", + "--include=*.tx", + "^Data: 0x" + + ABI.function_selector("guess_x(uint256)").hex() + + "%0.64x" % int.from_bytes("constructor".encode(), byteorder="big"), + workspace, + ] + subprocess.check_call(cmd) + + cmd = [ + "grep", + "-r", + "--include=*.tx", + "^Data: 0x" + + ABI.function_selector("guess_y(uint256)").hex() + + "%0.64x" % int.from_bytes("set_y".encode(), byteorder="big"), + workspace, + ] + subprocess.check_call(cmd) + + # sam.moelius: One purpose of test_maze is to give Manticore a lot of work, so that we can alter + # the blockchain while it is working. + def test_maze(self): + dir = os.path.abspath(os.path.join(DIRPATH, "truffle", "maze")) + workspace = os.path.join(self.test_dir, "workspace") + + os.chdir(dir) + + cmd = ["truffle", "deploy"] + output = subprocess.check_output(cmd).decode() + matches = [x for x in re.finditer(r"> contract address:\s*(0x\S*)", output)] + self.assertEqual(len(matches), 2) + address = matches[1].group(1) + + cmd = [ + PYTHON_BIN, + "-m", + "manticore", + "--workspace", + workspace, + "--no-color", + "--rpc", + URL, + "--txtarget", + address, + "--txnocoverage", + "--exclude-all", + ] + mcore = subprocess.Popen(cmd, stdout=subprocess.PIPE) + + # sam.moelius: Burn some ether just to alter the blockchain. It appears that truffle + # console's stdin must be kept open, or else it will not do what you ask of it. + cmd = ["truffle", "console"] + truffle_console = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + truffle_console.stdin.write( + b"web3.eth.getCoinbase().then(account => web3.eth.sendTransaction({" + + b' to: "0x0000000000000000000000000000000000000000",' + + b" from: account," + + b' value: web3.utils.toWei("10", "ether")' + + b"}))\n" + ) + truffle_console.stdin.flush() + for line in truffle_console.stdout: + if re.search(r"\btransactionHash\b", line.decode()): + break + truffle_console.stdin.close() + + # sam.moelius: Wait for truffle console to finish. + self.assertEqual(mcore.wait(), 0) + + # sam.moelius: Wait for manticore to finish. + self.assertEqual(mcore.wait(), 0) + + # sam.moelius: Manticore should have complained that the blockchain was altered. + self.assertRegex( + mcore.stdout.read().decode(), + r"\bblocknumber has changed\b.*\bsomeone is using the endpoint besides us\b", + ) + + # sam.moelius: Manticore should have found a path through the maze. + cmd = ["grep", "-r", "--include=*.logs", "You won!", workspace] + subprocess.check_call(cmd) diff --git a/tests/ethereum_truffle/truffle/basic/contracts/Basic.sol b/tests/ethereum_truffle/truffle/basic/contracts/Basic.sol new file mode 100644 index 000000000..9fd87a66d --- /dev/null +++ b/tests/ethereum_truffle/truffle/basic/contracts/Basic.sol @@ -0,0 +1,16 @@ +contract Basic { + uint256 x; + uint256 y; + constructor(uint256 _x) public { + x = _x; + } + function set_y(uint256 _y) external { + y = _y; + } + function guess_x(uint256 _x) external view { + require(x == _x, "x != _x"); + } + function guess_y(uint256 _y) external view { + require(y == _y, "y != _y"); + } +} diff --git a/tests/ethereum_truffle/truffle/basic/contracts/Migrations.sol b/tests/ethereum_truffle/truffle/basic/contracts/Migrations.sol new file mode 100644 index 000000000..51dcdc13c --- /dev/null +++ b/tests/ethereum_truffle/truffle/basic/contracts/Migrations.sol @@ -0,0 +1,23 @@ +pragma solidity >=0.4.21 <0.7.0; + +contract Migrations { + address public owner; + uint public last_completed_migration; + + constructor() public { + owner = msg.sender; + } + + modifier restricted() { + if (msg.sender == owner) _; + } + + function setCompleted(uint completed) public restricted { + last_completed_migration = completed; + } + + function upgrade(address new_address) public restricted { + Migrations upgraded = Migrations(new_address); + upgraded.setCompleted(last_completed_migration); + } +} diff --git a/tests/ethereum_truffle/truffle/basic/migrations/1_initial_migration.js b/tests/ethereum_truffle/truffle/basic/migrations/1_initial_migration.js new file mode 100644 index 000000000..ee2135d29 --- /dev/null +++ b/tests/ethereum_truffle/truffle/basic/migrations/1_initial_migration.js @@ -0,0 +1,5 @@ +const Migrations = artifacts.require("Migrations"); + +module.exports = function(deployer) { + deployer.deploy(Migrations); +}; diff --git a/tests/ethereum_truffle/truffle/basic/migrations/2_deploy_contracts.js b/tests/ethereum_truffle/truffle/basic/migrations/2_deploy_contracts.js new file mode 100644 index 000000000..36194a24b --- /dev/null +++ b/tests/ethereum_truffle/truffle/basic/migrations/2_deploy_contracts.js @@ -0,0 +1,7 @@ +const Basic = artifacts.require("Basic"); + +module.exports = function (deployer) { + deployer.deploy(Basic, "0x" + Buffer.from("constructor").toString("hex")).then(basic => { + basic.set_y("0x" + Buffer.from("set_y").toString("hex")); + }); +}; diff --git a/tests/ethereum_truffle/truffle/basic/truffle-config.js b/tests/ethereum_truffle/truffle/basic/truffle-config.js new file mode 100644 index 000000000..38656904c --- /dev/null +++ b/tests/ethereum_truffle/truffle/basic/truffle-config.js @@ -0,0 +1,99 @@ +/** + * Use this file to configure your truffle project. It's seeded with some + * common settings for different networks and features like migrations, + * compilation and testing. Uncomment the ones you need or modify + * them to suit your project as necessary. + * + * More information about configuration can be found at: + * + * truffleframework.com/docs/advanced/configuration + * + * To deploy via Infura you'll need a wallet provider (like truffle-hdwallet-provider) + * to sign your transactions before they're sent to a remote public node. Infura accounts + * are available for free at: infura.io/register. + * + * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate + * public/private key pairs. If you're publishing your code to GitHub make sure you load this + * phrase from a file you've .gitignored so it doesn't accidentally become public. + * + */ + +// const HDWalletProvider = require('truffle-hdwallet-provider'); +// const infuraKey = "fj4jll3k....."; +// +// const fs = require('fs'); +// const mnemonic = fs.readFileSync(".secret").toString().trim(); + +module.exports = { + /** + * Networks define how you connect to your ethereum client and let you set the + * defaults web3 uses to send transactions. If you don't specify one truffle + * will spin up a development blockchain for you on port 9545 when you + * run `develop` or `test`. You can ask a truffle command to use a specific + * network from the command line, e.g + * + * $ truffle test --network + */ + + networks: { + // Useful for testing. The `development` name is special - truffle uses it by default + // if it's defined here and no other network is specified at the command line. + // You should run a client (like ganache-cli, geth or parity) in a separate terminal + // tab if you use this network and you must also set the `host`, `port` and `network_id` + // options below to some value. + // + // development: { + // host: "127.0.0.1", // Localhost (default: none) + // port: 8545, // Standard Ethereum port (default: none) + // network_id: "*", // Any network (default: none) + // }, + + // Another network with more advanced options... + // advanced: { + // port: 8777, // Custom port + // network_id: 1342, // Custom network + // gas: 8500000, // Gas sent with each transaction (default: ~6700000) + // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) + // from:
, // Account to send txs from (default: accounts[0]) + // websockets: true // Enable EventEmitter interface for web3 (default: false) + // }, + + // Useful for deploying to a public network. + // NB: It's important to wrap the provider as a function. + // ropsten: { + // provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`), + // network_id: 3, // Ropsten's id + // gas: 5500000, // Ropsten has a lower block limit than mainnet + // confirmations: 2, // # of confs to wait between deployments. (default: 0) + // timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) + // skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) + // }, + + // Useful for private networks + // private: { + // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), + // network_id: 2111, // This network is yours, in the cloud. + // production: true // Treats this network as if it was a public net. (default: false) + // } + }, + + // Set default mocha options here, use special reporters etc. + mocha: { + // timeout: 100000 + }, + + // Configure your compilers + compilers: { + solc: { + // version: "0.5.1", // Fetch exact version from solc-bin (default: truffle's version) + // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) + // settings: { // See the solidity docs for advice about optimization and evmVersion + // optimizer: { + // enabled: false, + // runs: 200 + // }, + // evmVersion: "byzantium" + // } + } + } +} diff --git a/tests/ethereum_truffle/truffle/maze/contracts/Maze.sol b/tests/ethereum_truffle/truffle/maze/contracts/Maze.sol new file mode 100644 index 000000000..bbe9f40dc --- /dev/null +++ b/tests/ethereum_truffle/truffle/maze/contracts/Maze.sol @@ -0,0 +1,52 @@ +contract Maze { + event Log(string); + uint constant size = 4; + uint16[size] maze; + uint8 x = 0; + uint8 y = 0; + constructor(uint16[size] memory _maze) public { + assert(bit(0) != 0); + maze = _maze; + assert(!wall(x, y)); + } + function move(uint256 _dir) external { + require(!win(), "You already won!"); + int128 dx = 0; + int128 dy = 0; + if (_dir == uint8(byte('E'))) { + dx = 1; + } else if (_dir == uint8(byte('N'))) { + dy = -1; + } else if (_dir == uint8(byte('S'))) { + dy = 1; + } else if (_dir == uint8(byte('W'))) { + dx = -1; + } else { + require(false, "Invalid direction."); + } + require(x != 0 || dx >= 0, "At left boundary."); + require(y != 0 || dy >= 0, "At upper boundary."); + require(x != size - 1 || dx <= 0, "At right boundary."); + require(y != size - 1 || dy <= 0, "At lower boundary."); + require(!wall(x + uint8(dx), y + uint8(dy)), "Ouch! You bumped into a wall."); + fill(x, y); + x += uint8(dx); + y += uint8(dy); + if (win()) { + emit Log("You won!"); + } + assert(!wall(x, y)); + } + function wall(uint8 _x, uint8 _y) internal view returns(bool) { + return (maze[_y] & bit(_x)) != 0; + } + function fill(uint8 _x, uint8 _y) internal { + maze[_y] |= bit(_x); + } + function bit(uint8 _x) internal pure returns(uint16) { + return uint16(1 << (4 * ((size - 1) - _x))); + } + function win() internal view returns(bool) { + return x == size - 1 && y == size - 1; + } +} diff --git a/tests/ethereum_truffle/truffle/maze/contracts/Migrations.sol b/tests/ethereum_truffle/truffle/maze/contracts/Migrations.sol new file mode 100644 index 000000000..51dcdc13c --- /dev/null +++ b/tests/ethereum_truffle/truffle/maze/contracts/Migrations.sol @@ -0,0 +1,23 @@ +pragma solidity >=0.4.21 <0.7.0; + +contract Migrations { + address public owner; + uint public last_completed_migration; + + constructor() public { + owner = msg.sender; + } + + modifier restricted() { + if (msg.sender == owner) _; + } + + function setCompleted(uint completed) public restricted { + last_completed_migration = completed; + } + + function upgrade(address new_address) public restricted { + Migrations upgraded = Migrations(new_address); + upgraded.setCompleted(last_completed_migration); + } +} diff --git a/tests/ethereum_truffle/truffle/maze/migrations/1_initial_migration.js b/tests/ethereum_truffle/truffle/maze/migrations/1_initial_migration.js new file mode 100644 index 000000000..ee2135d29 --- /dev/null +++ b/tests/ethereum_truffle/truffle/maze/migrations/1_initial_migration.js @@ -0,0 +1,5 @@ +const Migrations = artifacts.require("Migrations"); + +module.exports = function(deployer) { + deployer.deploy(Migrations); +}; diff --git a/tests/ethereum_truffle/truffle/maze/migrations/2_deploy_contracts.js b/tests/ethereum_truffle/truffle/maze/migrations/2_deploy_contracts.js new file mode 100644 index 000000000..93c1a42ff --- /dev/null +++ b/tests/ethereum_truffle/truffle/maze/migrations/2_deploy_contracts.js @@ -0,0 +1,10 @@ +const Maze = artifacts.require("Maze"); + +module.exports = function (deployer) { + deployer.deploy(Maze, [ + "0x0000", + "0x0101", + "0x0100", + "0x0110", + ]); +}; diff --git a/tests/ethereum_truffle/truffle/maze/truffle-config.js b/tests/ethereum_truffle/truffle/maze/truffle-config.js new file mode 100644 index 000000000..38656904c --- /dev/null +++ b/tests/ethereum_truffle/truffle/maze/truffle-config.js @@ -0,0 +1,99 @@ +/** + * Use this file to configure your truffle project. It's seeded with some + * common settings for different networks and features like migrations, + * compilation and testing. Uncomment the ones you need or modify + * them to suit your project as necessary. + * + * More information about configuration can be found at: + * + * truffleframework.com/docs/advanced/configuration + * + * To deploy via Infura you'll need a wallet provider (like truffle-hdwallet-provider) + * to sign your transactions before they're sent to a remote public node. Infura accounts + * are available for free at: infura.io/register. + * + * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate + * public/private key pairs. If you're publishing your code to GitHub make sure you load this + * phrase from a file you've .gitignored so it doesn't accidentally become public. + * + */ + +// const HDWalletProvider = require('truffle-hdwallet-provider'); +// const infuraKey = "fj4jll3k....."; +// +// const fs = require('fs'); +// const mnemonic = fs.readFileSync(".secret").toString().trim(); + +module.exports = { + /** + * Networks define how you connect to your ethereum client and let you set the + * defaults web3 uses to send transactions. If you don't specify one truffle + * will spin up a development blockchain for you on port 9545 when you + * run `develop` or `test`. You can ask a truffle command to use a specific + * network from the command line, e.g + * + * $ truffle test --network + */ + + networks: { + // Useful for testing. The `development` name is special - truffle uses it by default + // if it's defined here and no other network is specified at the command line. + // You should run a client (like ganache-cli, geth or parity) in a separate terminal + // tab if you use this network and you must also set the `host`, `port` and `network_id` + // options below to some value. + // + // development: { + // host: "127.0.0.1", // Localhost (default: none) + // port: 8545, // Standard Ethereum port (default: none) + // network_id: "*", // Any network (default: none) + // }, + + // Another network with more advanced options... + // advanced: { + // port: 8777, // Custom port + // network_id: 1342, // Custom network + // gas: 8500000, // Gas sent with each transaction (default: ~6700000) + // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) + // from:
, // Account to send txs from (default: accounts[0]) + // websockets: true // Enable EventEmitter interface for web3 (default: false) + // }, + + // Useful for deploying to a public network. + // NB: It's important to wrap the provider as a function. + // ropsten: { + // provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`), + // network_id: 3, // Ropsten's id + // gas: 5500000, // Ropsten has a lower block limit than mainnet + // confirmations: 2, // # of confs to wait between deployments. (default: 0) + // timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) + // skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) + // }, + + // Useful for private networks + // private: { + // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), + // network_id: 2111, // This network is yours, in the cloud. + // production: true // Treats this network as if it was a public net. (default: false) + // } + }, + + // Set default mocha options here, use special reporters etc. + mocha: { + // timeout: 100000 + }, + + // Configure your compilers + compilers: { + solc: { + // version: "0.5.1", // Fetch exact version from solc-bin (default: truffle's version) + // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) + // settings: { // See the solidity docs for advice about optimization and evmVersion + // optimizer: { + // enabled: false, + // runs: 200 + // }, + // evmVersion: "byzantium" + // } + } + } +} diff --git a/tests/ethereum_truffle/truffle/predeployed/contracts/Migrations.sol b/tests/ethereum_truffle/truffle/predeployed/contracts/Migrations.sol new file mode 100644 index 000000000..51dcdc13c --- /dev/null +++ b/tests/ethereum_truffle/truffle/predeployed/contracts/Migrations.sol @@ -0,0 +1,23 @@ +pragma solidity >=0.4.21 <0.7.0; + +contract Migrations { + address public owner; + uint public last_completed_migration; + + constructor() public { + owner = msg.sender; + } + + modifier restricted() { + if (msg.sender == owner) _; + } + + function setCompleted(uint completed) public restricted { + last_completed_migration = completed; + } + + function upgrade(address new_address) public restricted { + Migrations upgraded = Migrations(new_address); + upgraded.setCompleted(last_completed_migration); + } +} diff --git a/tests/ethereum_truffle/truffle/predeployed/contracts/Predeployed.sol b/tests/ethereum_truffle/truffle/predeployed/contracts/Predeployed.sol new file mode 100644 index 000000000..6680d239c --- /dev/null +++ b/tests/ethereum_truffle/truffle/predeployed/contracts/Predeployed.sol @@ -0,0 +1,10 @@ +contract Predeployed { + uint256 public x; + uint256 public y; + constructor(uint256 _x) public { + x = _x; + } + function set_y(uint256 _y) external { + y = _y; + } +} diff --git a/tests/ethereum_truffle/truffle/predeployed/migrations/1_initial_migration.js b/tests/ethereum_truffle/truffle/predeployed/migrations/1_initial_migration.js new file mode 100644 index 000000000..ee2135d29 --- /dev/null +++ b/tests/ethereum_truffle/truffle/predeployed/migrations/1_initial_migration.js @@ -0,0 +1,5 @@ +const Migrations = artifacts.require("Migrations"); + +module.exports = function(deployer) { + deployer.deploy(Migrations); +}; diff --git a/tests/ethereum_truffle/truffle/predeployed/migrations/2_deploy_contracts.js b/tests/ethereum_truffle/truffle/predeployed/migrations/2_deploy_contracts.js new file mode 100644 index 000000000..ff1cdde82 --- /dev/null +++ b/tests/ethereum_truffle/truffle/predeployed/migrations/2_deploy_contracts.js @@ -0,0 +1,7 @@ +const Predeployed = artifacts.require("Predeployed"); + +module.exports = function (deployer) { + deployer.deploy(Predeployed, "0x" + Buffer.from("constructor").toString("hex")).then(predeployed => { + predeployed.set_y("0x" + Buffer.from("set_y").toString("hex")); + }); +}; diff --git a/tests/ethereum_truffle/truffle/predeployed/truffle-config.js b/tests/ethereum_truffle/truffle/predeployed/truffle-config.js new file mode 100644 index 000000000..38656904c --- /dev/null +++ b/tests/ethereum_truffle/truffle/predeployed/truffle-config.js @@ -0,0 +1,99 @@ +/** + * Use this file to configure your truffle project. It's seeded with some + * common settings for different networks and features like migrations, + * compilation and testing. Uncomment the ones you need or modify + * them to suit your project as necessary. + * + * More information about configuration can be found at: + * + * truffleframework.com/docs/advanced/configuration + * + * To deploy via Infura you'll need a wallet provider (like truffle-hdwallet-provider) + * to sign your transactions before they're sent to a remote public node. Infura accounts + * are available for free at: infura.io/register. + * + * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate + * public/private key pairs. If you're publishing your code to GitHub make sure you load this + * phrase from a file you've .gitignored so it doesn't accidentally become public. + * + */ + +// const HDWalletProvider = require('truffle-hdwallet-provider'); +// const infuraKey = "fj4jll3k....."; +// +// const fs = require('fs'); +// const mnemonic = fs.readFileSync(".secret").toString().trim(); + +module.exports = { + /** + * Networks define how you connect to your ethereum client and let you set the + * defaults web3 uses to send transactions. If you don't specify one truffle + * will spin up a development blockchain for you on port 9545 when you + * run `develop` or `test`. You can ask a truffle command to use a specific + * network from the command line, e.g + * + * $ truffle test --network + */ + + networks: { + // Useful for testing. The `development` name is special - truffle uses it by default + // if it's defined here and no other network is specified at the command line. + // You should run a client (like ganache-cli, geth or parity) in a separate terminal + // tab if you use this network and you must also set the `host`, `port` and `network_id` + // options below to some value. + // + // development: { + // host: "127.0.0.1", // Localhost (default: none) + // port: 8545, // Standard Ethereum port (default: none) + // network_id: "*", // Any network (default: none) + // }, + + // Another network with more advanced options... + // advanced: { + // port: 8777, // Custom port + // network_id: 1342, // Custom network + // gas: 8500000, // Gas sent with each transaction (default: ~6700000) + // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) + // from:
, // Account to send txs from (default: accounts[0]) + // websockets: true // Enable EventEmitter interface for web3 (default: false) + // }, + + // Useful for deploying to a public network. + // NB: It's important to wrap the provider as a function. + // ropsten: { + // provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`), + // network_id: 3, // Ropsten's id + // gas: 5500000, // Ropsten has a lower block limit than mainnet + // confirmations: 2, // # of confs to wait between deployments. (default: 0) + // timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) + // skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) + // }, + + // Useful for private networks + // private: { + // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), + // network_id: 2111, // This network is yours, in the cloud. + // production: true // Treats this network as if it was a public net. (default: false) + // } + }, + + // Set default mocha options here, use special reporters etc. + mocha: { + // timeout: 100000 + }, + + // Configure your compilers + compilers: { + solc: { + // version: "0.5.1", // Fetch exact version from solc-bin (default: truffle's version) + // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) + // settings: { // See the solidity docs for advice about optimization and evmVersion + // optimizer: { + // enabled: false, + // runs: 200 + // }, + // evmVersion: "byzantium" + // } + } + } +}