Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat: add error traces for vvm #373

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
4 changes: 4 additions & 0 deletions boa/contracts/abi/abi_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,10 @@ def __init__(self, name: str, abi: list[dict], filename: Optional[str] = None):
def abi(self):
return self._abi

@cached_property
def name(self):
return self._name

@property
def functions(self):
return [
Expand Down
1 change: 1 addition & 0 deletions boa/contracts/base_evm_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ def __str__(self):
err = frame

ret = f"{err}\n\n{self.stack_trace}"

call_tree = str(self.call_trace)
ledge = "=" * 72
return f"\n{ledge}\n{call_tree}\n{ledge}\n\n{ret}"
110 changes: 94 additions & 16 deletions boa/contracts/vvm/vvm_contract.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,80 @@
from dataclasses import dataclass
from functools import cached_property
from typing import Optional

import vyper.utils

from boa.contracts.abi.abi_contract import ABIContract, ABIContractFactory, ABIFunction
from boa.contracts.base_evm_contract import StackTrace
from boa.environment import Env
from boa.util.abi import Address
from boa.util.eip5202 import generate_blueprint_bytecode


class VVMContract(ABIContract):
def __init__(
self,
name: str,
abi: list[dict],
functions: list[ABIFunction],
events: list[dict],
address: Address,
deployer: "VVMDeployer",
**kwargs,
):
self._deployer = deployer
super().__init__(name, abi, functions, events, address, **kwargs)

@property
def deployer(self) -> "VVMDeployer":
# override deployer getter in ABIContract
return self._deployer

@cached_property
def source_code(self) -> str:
return self.deployer.source_code

@cached_property
def source_map(self) -> dict:
return self.deployer.source_map

def stack_trace(self, computation=None):
computation = computation or self._computation
code_stream = computation.code

error_map = self.source_map["pc_pos_map"]

error = None
for pc in reversed(code_stream._trace):
pc = str(pc)
if pc in error_map:
error = error_map[pc]
break

# this condition is because source maps are not yet implemented for
# the ctor
if error is not None:
# we only report the line for simplicity, could be more precise
lineno, *_ = error

annotated_error = vyper.utils.annotate_source_code(
self.source_code, lineno, context_lines=3, line_numbers=True
)

return StackTrace([VVMErrorDetail(annotated_error)])
return StackTrace([])


@dataclass
class VVMErrorDetail:
# minimal class for now, will be expanded when source_map
# based reporting is improved in the future (similarly
# to ErrorDetail).
annotated_source: str

def __str__(self):
return self.annotated_source


class VVMBlueprint(ABIContract):
def __init__(self, deployer: "VVMDeployer", address: Address):
name = deployer.name or "<unknown>" # help mypy
Expand All @@ -25,37 +93,32 @@ def deployer(self):
return self._deployer


class VVMDeployer:
class VVMDeployer(ABIContractFactory):
"""
A deployer that uses the Vyper Version Manager (VVM).
This allows deployment of contracts written in older versions of Vyper that
can interact with new versions using the ABI definition.
"""

def __init__(self, abi, bytecode, name, filename):
def __init__(self, abi, bytecode, name, filename, source_code, source_map):
"""
Initialize a VVMDeployer instance.
:param abi: The contract's ABI.
:param bytecode: The contract's bytecode.
:param filename: The filename of the contract.
"""
self.abi: dict = abi
self.bytecode: bytes = bytecode
self.name: Optional[str] = name
self.filename: str = filename
self.source_map: dict = source_map
self.source_code = source_code
super().__init__(name, abi, filename=filename)

@classmethod
def from_compiler_output(cls, compiler_output, name, filename):
def from_compiler_output(cls, compiler_output, name, filename, source_code):
abi = compiler_output["abi"]
bytecode_nibbles = compiler_output["bytecode"]
bytecode = bytes.fromhex(bytecode_nibbles.removeprefix("0x"))
return cls(abi, bytecode, name, filename)

@cached_property
def factory(self):
return ABIContractFactory.from_abi_dict(
self.abi, name=self.name, filename=self.filename
)
source_map = compiler_output["source_map"]
return cls(abi, bytecode, name, filename, source_code, source_map)

@cached_property
def constructor(self):
Expand Down Expand Up @@ -119,5 +182,20 @@ def deploy_as_blueprint(self, env=None, blueprint_preamble=None, **kwargs):
def __call__(self, *args, **kwargs):
return self.deploy(*args, **kwargs)

def at(self, address, nowarn=False):
return self.factory.at(address, nowarn=nowarn)
def at(self, address: Address | str, nowarn=False) -> VVMContract:
"""
Create an VVMContract object for a deployed contract at `address`.
"""
address = Address(address)
contract = VVMContract(
self._name,
self.abi,
self.functions,
self.events,
address,
self,
nowarn=nowarn,
)

contract.env.register_contract(address, contract)
return contract
9 changes: 5 additions & 4 deletions boa/interpret.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,15 +295,16 @@ def _compile():
# separate _handle_output and _compile so that we don't trample
# name and filename in the VVMDeployer from separate invocations
# (with different values for name+filename).
def _handle_output(compiled_src):
def _handle_output(compiled_src, source_code):
# pprint.pp(compiled_src)
compiler_output = compiled_src["<stdin>"]
return VVMDeployer.from_compiler_output(
compiler_output, name=name, filename=filename
compiler_output, source_code=source_code, name=name, filename=filename
)

# Ensure the cache is initialized
if _disk_cache is None:
return _handle_output(_compile())
return _handle_output(_compile(), source_code)

# Generate a unique cache key
cache_key = f"{source_code}:{version}"
Expand All @@ -317,7 +318,7 @@ def _handle_output(compiled_src):
_disk_cache.invalidate(cache_key)
ret = _disk_cache.caching_lookup(cache_key, _compile)

return _handle_output(ret)
return _handle_output(ret, source_code)


def from_etherscan(
Expand Down
38 changes: 38 additions & 0 deletions tests/unitary/contracts/vvm/test_vvm.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,41 @@ def baz(x: uint256, _from: address, y: uint256) -> (MyStruct1, MyStruct2):
# assert type(v).__name__ == "MyStruct2"
assert v._0 == addy
assert v.x == 4


def test_vvm_source_maps():
code = """
# pragma version 0.3.10

struct MyStruct1:
x: uint256

@external
def foo(x: uint256) -> MyStruct1:
if x == 0:
return MyStruct1({x: x})

if x == 1:
raise "x is 1"

return MyStruct1({x: x})

@external
def bar() -> uint256:
return 42
"""

c = boa.loads(code)

with boa.reverts():
c.foo(1)

error = """ 10 return MyStruct1({x: x})
11
12 if x == 1:
---> 13 raise "x is 1"
14
15 return MyStruct1({x: x})
16"""

assert error == str(c.stack_trace().last_frame), "incorrect reported error source"
Loading