diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d75b00b..549b009 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -27,11 +27,11 @@ jobs: echo "Installing zkvyper and era_test_node" # Install zkvyper and era_test_node from binary repositories - curl --location https://raw.githubusercontent.com/matter-labs/zkvyper-bin/v1.5.3/linux-amd64/zkvyper-linux-amd64-musl-v1.5.3 \ + curl --location https://raw.githubusercontent.com/matter-labs/zkvyper-bin/v1.5.4/linux-amd64/zkvyper-linux-amd64-musl-v1.5.4 \ --silent --output /usr/local/bin/zkvyper && \ chmod +x /usr/local/bin/zkvyper && \ zkvyper --version - curl --location https://github.com/matter-labs/era-test-node/releases/download/v0.1.0-alpha.25/era_test_node-v0.1.0-alpha.25-x86_64-unknown-linux-gnu.tar.gz \ + curl --location https://github.com/matter-labs/era-test-node/releases/download/v0.1.0-alpha.27/era_test_node-v0.1.0-alpha.27-x86_64-unknown-linux-gnu.tar.gz \ --silent --output era_test_node.tar.gz && \ tar --extract --file=era_test_node.tar.gz && \ mv era_test_node /usr/local/bin/era_test_node && \ diff --git a/boa_zksync/contract.py b/boa_zksync/contract.py index 4107455..8040795 100644 --- a/boa_zksync/contract.py +++ b/boa_zksync/contract.py @@ -137,7 +137,7 @@ def __init__(self, fn: ContractFunctionT, contract: ZksyncContract): "name": f"__boa_private_{fn.name}__", "type": "function", } - super().__init__(abi, contract._name) + super().__init__(abi, contract.contract_name) self.contract = contract self.func_t = fn @@ -163,7 +163,7 @@ def __init__(self, var: VarInfo, name: str, contract: ZksyncContract): "constant": True, "type": "function", } - super().__init__(abi, contract._name) + super().__init__(abi, contract.contract_name) self.contract = contract self.var = var self.var_name = name @@ -198,7 +198,7 @@ def __init__(self, code: str, contract: ZksyncContract): "name": "__boa_debug__", "type": "function", } - super().__init__(abi, contract._name) + super().__init__(abi, contract.contract_name) self.contract = contract self.code = code diff --git a/boa_zksync/deployer.py b/boa_zksync/deployer.py index 1f0761e..9d5d4df 100644 --- a/boa_zksync/deployer.py +++ b/boa_zksync/deployer.py @@ -9,7 +9,7 @@ from boa_zksync.compile import compile_zksync, compile_zksync_source from boa_zksync.contract import ZksyncContract -from boa_zksync.types import ZksyncCompilerData +from boa_zksync.types import ZksyncCompilerData, DEFAULT_SALT if TYPE_CHECKING: from boa_zksync.environment import ZksyncEnv @@ -41,10 +41,23 @@ def _compile( def from_abi_dict(cls, abi, name="", filename=None): raise NotImplementedError("ZksyncDeployer does not support loading from ABI") - def deploy(self, *args, value=0, **kwargs) -> ZksyncContract: + def deploy( + self, + *args, + value=0, + gas=None, + dependency_bytecodes=(), + salt=DEFAULT_SALT, + max_priority_fee_per_gas=None, + **kwargs, + ) -> ZksyncContract: address, _ = self.env.deploy_code( bytecode=self.zkvyper_data.bytecode, value=value, + gas=gas, + dependency_bytecodes=dependency_bytecodes, + salt=salt, + max_priority_fee_per_gas=max_priority_fee_per_gas, constructor_calldata=( self.constructor.prepare_calldata(*args, **kwargs) if args or kwargs diff --git a/boa_zksync/environment.py b/boa_zksync/environment.py index 234b82b..829e7fa 100644 --- a/boa_zksync/environment.py +++ b/boa_zksync/environment.py @@ -3,7 +3,6 @@ from hashlib import sha256 from pathlib import Path from typing import Any, Iterable, Optional, Type -from unittest.mock import MagicMock from boa.contracts.abi.abi_contract import ABIContract, ABIContractFactory from boa.environment import _AddressType @@ -17,10 +16,14 @@ from boa_zksync.deployer import ZksyncDeployer from boa_zksync.node import EraTestNode -from boa_zksync.types import DeployTransaction, ZksyncComputation, ZksyncMessage +from boa_zksync.types import ( + CONTRACT_DEPLOYER_ADDRESS, + ZERO_ADDRESS, + DeployTransaction, + ZksyncComputation, + ZksyncMessage, DEFAULT_SALT, +) -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" -_CONTRACT_DEPLOYER_ADDRESS = "0x0000000000000000000000000000000000008006" with open(Path(__file__).parent / "IContractDeployer.json") as f: CONTRACT_DEPLOYER = ABIContractFactory.from_abi_dict( json.load(f), "ContractDeployer" @@ -52,8 +55,8 @@ def create(self): @property def vm(self): if self._vm is None: - # todo: vyper base contract calls this property - self._vm = MagicMock(state=_RPCState(self._rpc)) + self._vm = lambda: None + self._vm.state = _RPCState(self._rpc) return self._vm def _reset_fork(self, block_identifier="latest"): @@ -132,11 +135,11 @@ def execute_code( "debug_traceCall", [args.as_json_dict(), "latest", {"tracer": "callTracer"}], ) - traced_computation = ZksyncComputation.from_call_trace(trace_call) + traced_computation = ZksyncComputation.from_call_trace(self, trace_call) except (RPCError, HTTPError): output = self._rpc.fetch("eth_call", [args.as_json_dict(), "latest"]) traced_computation = ZksyncComputation( - args, bytes.fromhex(output.removeprefix("0x")) + self, args, bytes.fromhex(output.removeprefix("0x")) ) if is_modifying: @@ -147,11 +150,13 @@ def execute_code( assert ( traced_computation.is_error == trace.is_error ), f"VMError mismatch: {traced_computation.error} != {trace.error}" - return ZksyncComputation.from_debug_trace(trace.raw_trace) + return ZksyncComputation.from_debug_trace(self, trace.raw_trace) except _EstimateGasFailed: if not traced_computation.is_error: # trace gives more information - return ZksyncComputation(args, error=VMError("Estimate gas failed")) + return ZksyncComputation( + self, args, error=VMError("Estimate gas failed") + ) return traced_computation @@ -163,7 +168,8 @@ def deploy_code( bytecode=b"", constructor_calldata=b"", dependency_bytecodes: Iterable[bytes] = (), - salt=b"\0" * 32, + salt=DEFAULT_SALT, + max_priority_fee_per_gas=None, **kwargs, ) -> tuple[Address, bytes]: """ @@ -175,6 +181,7 @@ def deploy_code( :param constructor_calldata: The calldata for the contract constructor. :param dependency_bytecodes: The bytecodes of the blueprints. :param salt: The salt for the contract deployment. + :param max_priority_fee_per_gas: The max priority fee per gas for the transaction. :param kwargs: Additional parameters for the transaction. :return: The address of the deployed contract and the bytecode hash. """ @@ -199,10 +206,10 @@ def deploy_code( bytecode_hash = _hash_code(bytecode) tx = DeployTransaction( sender=sender, - to=_CONTRACT_DEPLOYER_ADDRESS, + to=CONTRACT_DEPLOYER_ADDRESS, gas=gas or 0, gas_price=gas_price, - max_priority_fee_per_gas=kwargs.pop("max_priority_fee_per_gas", gas_price), + max_priority_fee_per_gas=max_priority_fee_per_gas or gas_price, nonce=nonce, value=value, calldata=self.create.prepare_calldata( @@ -234,7 +241,7 @@ def get_code(self, address: Address) -> bytes: return self._rpc.fetch("eth_getCode", [address, "latest"]) def set_code(self, address: Address, bytecode: bytes): - return self._rpc.fetch("hardhat_setCode", [address, list(bytecode)]) + return self._rpc.fetch("hardhat_setCode", [address, f"0x{bytecode.hex()}"]) def generate_address(self, alias: Optional[str] = None) -> _AddressType: """ diff --git a/boa_zksync/types.py b/boa_zksync/types.py index 37ff64c..ac7f0f7 100644 --- a/boa_zksync/types.py +++ b/boa_zksync/types.py @@ -1,8 +1,9 @@ from dataclasses import dataclass, field from functools import cached_property -from typing import Optional +from typing import TYPE_CHECKING, Optional import rlp +from boa.contracts.call_trace import TraceFrame from boa.contracts.vyper.vyper_contract import VyperDeployer from boa.interpret import compiler_data from boa.rpc import fixup_dict, to_bytes, to_hex @@ -15,6 +16,13 @@ from vyper.compiler import CompilerData from vyper.compiler.settings import OptimizationLevel +if TYPE_CHECKING: + from boa_zksync import ZksyncEnv + +ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" +CONTRACT_DEPLOYER_ADDRESS = "0x0000000000000000000000000000000000008006" +DEFAULT_SALT = b"\0" * 32 + _EIP712_TYPE = bytes.fromhex("71") _EIP712_TYPES_SPEC = { "EIP712Domain": [ @@ -76,7 +84,7 @@ def get_estimate_tx(self): "to": self.to, "gas": f"0x{self.gas:0x}", "gasPrice": f"0x{self.gas_price:0x}", - "maxPriorityFeePerGas": f"0x{self.max_priority_fee_per_gas :0x}", + "maxPriorityFeePerGas": f"0x{self.max_priority_fee_per_gas:0x}", "nonce": f"0x{self.nonce:0x}", "value": f"0x{self.value:0x}", "data": f"0x{self.calldata.hex()}", @@ -218,9 +226,14 @@ def as_json_dict(self, sender_field="from"): def as_tx_params(self): return self.as_json_dict(sender_field="from_") + @property + def is_create(self) -> bool: + return self.to == CONTRACT_DEPLOYER_ADDRESS + @dataclass class ZksyncComputation: + env: "ZksyncEnv" msg: ZksyncMessage output: bytes | None = None error: VMError | None = None @@ -231,7 +244,7 @@ class ZksyncComputation: value: int = 0 @classmethod - def from_call_trace(cls, output: dict) -> "ZksyncComputation": + def from_call_trace(cls, env: "ZksyncEnv", output: dict) -> "ZksyncComputation": """Recursively constructs a ZksyncComputation from a debug_traceCall output.""" error = None if output.get("error") is not None: @@ -240,6 +253,7 @@ def from_call_trace(cls, output: dict) -> "ZksyncComputation": error = Revert(output["revertReason"]) return cls( + env=env, msg=ZksyncMessage( sender=Address(output["from"]), to=Address(output["to"]), @@ -249,7 +263,9 @@ def from_call_trace(cls, output: dict) -> "ZksyncComputation": ), output=to_bytes(output["output"]), error=error, - children=[cls.from_call_trace(call) for call in output.get("calls", [])], + children=[ + cls.from_call_trace(env, call) for call in output.get("calls", []) + ], gas_used=int(output["gasUsed"], 16), revert_reason=output.get("revertReason"), type=output.get("type", "Call"), @@ -257,7 +273,7 @@ def from_call_trace(cls, output: dict) -> "ZksyncComputation": ) @classmethod - def from_debug_trace(cls, output: dict): + def from_debug_trace(cls, env: "ZksyncEnv", output: dict): """ Finds the actual transaction computation, since zksync has system contract calls in the trace. @@ -270,12 +286,12 @@ def _find(calls: list[dict]): if found := _find(trace["calls"]): return found if trace["to"] == to and trace["from"] == sender: - return cls.from_call_trace(trace) + return cls.from_call_trace(env, trace) if result := _find(output["calls"]): return result # in production mode the result is not always nested - return cls.from_call_trace(output) + return cls.from_call_trace(env, output) @property def is_success(self) -> bool: @@ -302,3 +318,18 @@ def raise_if_error(self) -> None: def get_gas_used(self): return self.gas_used + + @property + def net_gas_used(self) -> int: + return self.get_gas_used() + + @property + def call_trace(self) -> TraceFrame: + return self._get_call_trace() + + def _get_call_trace(self, depth=0) -> TraceFrame: + address = self.msg.to + contract = self.env.lookup_contract(address) + source = contract.trace_source(self) if contract else None + children = [child._get_call_trace(depth + 1) for child in self.children] + return TraceFrame(self, source, depth, children) diff --git a/boa_zksync/util.py b/boa_zksync/util.py index cde6de0..c2270ae 100644 --- a/boa_zksync/util.py +++ b/boa_zksync/util.py @@ -39,7 +39,7 @@ def stop_subprocess(proc: Popen[bytes]): def install_zkvyper_compiler( - source="https://raw.githubusercontent.com/matter-labs/zkvyper-bin/v1.5.3/linux-amd64/zkvyper-linux-amd64-musl-v1.5.3", # noqa: E501 + source="https://raw.githubusercontent.com/matter-labs/zkvyper-bin/v1.5.4/linux-amd64/zkvyper-linux-amd64-musl-v1.5.4", # noqa: E501 destination="/usr/local/bin/zkvyper", ): """ @@ -58,7 +58,7 @@ def install_zkvyper_compiler( def install_era_test_node( - source="https://github.com/matter-labs/era-test-node/releases/download/v0.1.0-alpha.25/era_test_node-v0.1.0-alpha.25-x86_64-unknown-linux-gnu.tar.gz", # noqa: E501 + source="https://github.com/matter-labs/era-test-node/releases/download/v0.1.0-alpha.27/era_test_node-v0.1.0-alpha.27-x86_64-unknown-linux-gnu.tar.gz", # noqa: E501 destination="/usr/local/bin/era_test_node", ): """ diff --git a/pyproject.toml b/pyproject.toml index 3b58ef7..53af012 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "titanoboa-zksync" -version = "0.2.2" +version = "0.2.3" description = "A Zksync plugin for the Titanoboa Vyper interpreter" license = { file = "LICENSE" } readme = "README.md" @@ -14,7 +14,7 @@ keywords = [ ] classifiers = ["Topic :: Software Development"] -dependencies = ["titanoboa>=0.2.0"] +dependencies = ["titanoboa>=0.2.2"] [project.optional-dependencies] forking-recommended = ["ujson"] diff --git a/tests/conftest.py b/tests/conftest.py index bcce22a..1534a7c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,7 +23,7 @@ def zksync_sepolia_fork(account): fork_url = os.getenv("FORK_URL", "https://sepolia.era.zksync.dev") boa_zksync.set_zksync_fork( fork_url, - block_identifier=1689570, + block_identifier=3000000, node_args=("--show-calls", "all", "--show-outputs", "true"), ) boa.env.add_account(account, force_eoa=True) diff --git a/tests/test_browser.py b/tests/test_browser.py index 2edd91d..39deee0 100644 --- a/tests/test_browser.py +++ b/tests/test_browser.py @@ -14,6 +14,9 @@ def _javascript_call(js_func: str, *args, timeout_message: str) -> Any: if method == "evm_snapshot": return 1 + if method == "eth_requestAccounts": + return [ZERO_ADDRESS] + if method == "evm_revert": assert args[1:] == ([1],), f"Bad args passed to mock: {args}" return None @@ -26,7 +29,7 @@ def _javascript_call(js_func: str, *args, timeout_message: str) -> Any: raise KeyError(args) - if js_func == "loadSigner": + if js_func in "loadSigner": return ZERO_ADDRESS raise KeyError(js_func) diff --git a/tests/test_computation.py b/tests/test_computation.py index 31481dd..ec1acf7 100644 --- a/tests/test_computation.py +++ b/tests/test_computation.py @@ -37,7 +37,7 @@ def test_from_debug_trace_nested(): }, ], } - assert ZksyncComputation.from_debug_trace(output).output == result + assert ZksyncComputation.from_debug_trace(boa.env, output).output == result def test_from_debug_trace_production_mode(): @@ -51,4 +51,4 @@ def test_from_debug_trace_production_mode(): "calls": [], **_required_fields, } - assert ZksyncComputation.from_debug_trace(output).output == result + assert ZksyncComputation.from_debug_trace(boa.env, output).output == result diff --git a/tests/test_deploy.py b/tests/test_deploy.py index b7c184f..27bc842 100644 --- a/tests/test_deploy.py +++ b/tests/test_deploy.py @@ -2,6 +2,7 @@ import pytest from boa import BoaError from boa.contracts.base_evm_contract import StackTrace +from boa.contracts.call_trace import TraceFrame STARTING_SUPPLY = 100 @@ -149,11 +150,12 @@ def get_name_of(addr: HasName) -> String[32]: with pytest.raises(BoaError) as ctx: caller_contract.get_name_of(called_contract) - (trace,) = ctx.value.args - assert trace == StackTrace( + (call_trace, stack_trace) = ctx.value.args + called_addr = called_contract.address + assert stack_trace == StackTrace( [ " Test an error( (file CalledContract).name() -> ['string'])", + f"{called_addr}> (file CalledContract).name() -> ['string'])", " Test an error( (file " "CallerContract).get_name_of(address) -> ['string'])", @@ -164,6 +166,21 @@ def get_name_of(addr: HasName) -> String[32]: "['string'])", ] ) + assert isinstance(call_trace, TraceFrame) + assert str(call_trace).split("\n") == [ + f'[E] [24523] CallerContract.get_name_of(addr = "{called_addr}") <0x>', + " [E] [23592] Unknown contract 0x0000000000000000000000000000000000008002.0x4de2e468", + " [566] Unknown contract 0x000000000000000000000000000000000000800B.0x29f172ad", + " [1909] Unknown contract 0x000000000000000000000000000000000000800B.0x06bed036", + " [159] Unknown contract 0x0000000000000000000000000000000000008010.0x00000000", + " [449] Unknown contract 0x000000000000000000000000000000000000800B.0xa225efcb", + " [2226] Unknown contract 0x0000000000000000000000000000000000008002.0x4de2e468", + " [427] Unknown contract 0x000000000000000000000000000000000000800B.0xa851ae78", + " [398] Unknown contract 0x0000000000000000000000000000000000008004.0xe516761e", + " [E] [2566] Unknown contract 0x0000000000000000000000000000000000008009.0xb47fade1", + f' [E] [1383] CallerContract.get_name_of(addr = "{called_addr}") <0x>', + " [E] [403] CalledContract.name() <0x>", + ] def test_private(zksync_env):