From bb5e75b046f5365efecb462141e8cf98159d0e8f Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 14 Feb 2024 13:11:18 -0500 Subject: [PATCH 1/3] refactor: Address inherit from bytes it was previously inheriting from str so that the abi encoder/decoder could work, but now we have a custom implementation which handles Address. change the inheritance and remove explicit usages of `\.canonical_address`. --- boa/contracts/abi/abi_contract.py | 2 +- boa/contracts/vyper/vyper_contract.py | 6 +++--- boa/environment.py | 22 ++++++++++----------- boa/interpret.py | 2 +- boa/test/strategies.py | 20 +++++-------------- boa/util/abi.py | 27 ++++++++++++++------------ tests/unitary/jupyter/test_browser.py | 2 +- tests/unitary/strategy/test_address.py | 11 ++++------- tests/unitary/test_blueprints.py | 2 +- 9 files changed, 42 insertions(+), 52 deletions(-) diff --git a/boa/contracts/abi/abi_contract.py b/boa/contracts/abi/abi_contract.py index 9f22b051..d3cbcb47 100644 --- a/boa/contracts/abi/abi_contract.py +++ b/boa/contracts/abi/abi_contract.py @@ -194,7 +194,7 @@ def __init__( super().__init__(env, filename=filename, address=address) self._name = name self._functions = functions - self._bytecode = self.env.vm.state.get_code(address.canonical_address) + self._bytecode = self.env.vm.state.get_code(address) if not self._bytecode: warn( f"Requested {self} but there is no bytecode at that address!", diff --git a/boa/contracts/vyper/vyper_contract.py b/boa/contracts/vyper/vyper_contract.py index 5ef18264..834d7611 100644 --- a/boa/contracts/vyper/vyper_contract.py +++ b/boa/contracts/vyper/vyper_contract.py @@ -107,7 +107,7 @@ def at(self, address: Any) -> "VyperContract": filename=self.filename, ) vm = ret.env.vm - bytecode = vm.state.get_code(address.canonical_address) + bytecode = vm.state.get_code(address) ret._set_bytecode(bytecode) @@ -348,7 +348,7 @@ def setpath(lens, path, val): class StorageVar: def __init__(self, contract, slot, typ): self.contract = contract - self.addr = self.contract._address.canonical_address + self.addr = self.contract._address self.accountdb = contract.env.vm.state._account_db self.slot = slot self.typ = typ @@ -655,7 +655,7 @@ def event_for(self): def decode_log(self, e): log_id, address, topics, data = e - assert self._address.canonical_address == address + assert self._address == address event_hash = topics[0] event_t = self.event_for[event_hash] diff --git a/boa/environment.py b/boa/environment.py index 00c5f6da..5cba2425 100644 --- a/boa/environment.py +++ b/boa/environment.py @@ -126,11 +126,11 @@ def register_raw_precompile(address, fn, force=False): address = Address(address) if address in _precompiles and not force: raise ValueError(f"Already registered: {address}") - _precompiles[address.canonical_address] = fn + _precompiles[address] = fn def deregister_raw_precompile(address, force=True): - address = Address(address).canonical_address + address = Address(address) if address not in _precompiles and not force: raise ValueError("Not registered: {address}") _precompiles.pop(address, None) @@ -489,14 +489,14 @@ def reset_gas_metering_behavior(self) -> None: # set balance of address in py-evm def set_balance(self, addr, value): - self.vm.state.set_balance(Address(addr).canonical_address, value) + self.vm.state.set_balance(Address(addr), value) # get balance of address in py-evm def get_balance(self, addr): - return self.vm.state.get_balance(Address(addr).canonical_address) + return self.vm.state.get_balance(Address(addr)) def register_contract(self, address, obj): - addr = Address(address).canonical_address + addr = Address(address) self._contracts[addr] = obj # also register it in the registry for @@ -513,13 +513,13 @@ def _lookup_contract_fast(self, address: PYEVM_Address): def lookup_contract(self, address: _AddressType): if address == b"": return None - return self._contracts.get(Address(address).canonical_address) + return self._contracts.get(Address(address)) def alias(self, address, name): - self._aliases[Address(address).canonical_address] = name + self._aliases[Address(address)] = name def lookup_alias(self, address): - return self._aliases[Address(address).canonical_address] + return self._aliases[Address(address)] # advanced: reset warm/cold counters for addresses and storage def _reset_access_counters(self): @@ -574,7 +574,7 @@ def _get_sender(self, sender=None) -> PYEVM_Address: sender = self.eoa if self.eoa is None: raise ValueError(f"{self}.eoa not defined!") - return Address(sender).canonical_address + return Address(sender) def _update_gas_used(self, gas_used: int): self._gas_tracker += gas_used @@ -607,7 +607,7 @@ def deploy_code( gas=gas, value=value, code=bytecode, - create_address=target_address.canonical_address, + create_address=target_address, data=b"", ) @@ -669,7 +669,7 @@ def execute_code( sender = self._get_sender(sender) - to = Address(to_address).canonical_address + to = Address(to_address) bytecode = override_bytecode if override_bytecode is None: diff --git a/boa/interpret.py b/boa/interpret.py index ae13c338..51f6c0f5 100644 --- a/boa/interpret.py +++ b/boa/interpret.py @@ -153,7 +153,7 @@ def load_partial(filename: str, compiler_args=None) -> VyperDeployer: # type: i def from_etherscan( address: Any, name=None, uri="https://api.etherscan.io/api", api_key=None ): - addr = Address(address) + addr = Address(address).checksum_address abi = fetch_abi_from_etherscan(addr, uri, api_key) return ABIContractFactory.from_abi_dict(abi, name=name).at(addr) diff --git a/boa/test/strategies.py b/boa/test/strategies.py index c075663c..288ada9c 100644 --- a/boa/test/strategies.py +++ b/boa/test/strategies.py @@ -3,13 +3,13 @@ from typing import Any, Callable, Iterable, Optional, Union from eth_abi.grammar import BasicType, TupleType, parse -from eth_utils import to_checksum_address from hypothesis import given from hypothesis import strategies as st from hypothesis.strategies import SearchStrategy from hypothesis.strategies._internal.deferred import DeferredStrategy from boa.contracts.vyper.vyper_contract import VyperFunction +from boa.util.abi import Address # hypothesis fuzzing strategies, adapted from brownie 0.19.2 (86258c7bd) # in the future these may be superseded by eth-stdlib. @@ -87,25 +87,15 @@ def _decimal_strategy( return st.decimals(min_value=min_value, max_value=max_value, places=places) -def format_addr(t): - if isinstance(t, str): - t = t.encode("utf-8") - return to_checksum_address(t.rjust(20, b"\x00")) - - -def generate_random_string(n): - return ["".join(random.choices(string.ascii_lowercase, k=5)) for i in range(n)] +def generate_random_strings(n): + return [b"".join(random.choices(string.ascii_lowercase, k=5)) for i in range(n)] @_exclude_filter -def _address_strategy(length: Optional[int] = 100) -> SearchStrategy: - random_strings = generate_random_string(length) +def _address_strategy() -> SearchStrategy: # TODO: add addresses from the environment. probably everything in # boa.env._contracts, boa.env._blueprints and boa.env.eoa. - accounts = [format_addr(i) for i in random_strings] - return _DeferredStrategyRepr( - lambda: st.sampled_from(list(accounts)[:length]), "accounts" - ) + return st.binary(min_size=20, max_size=20).map(Address) @_exclude_filter diff --git a/boa/util/abi.py b/boa/util/abi.py index 634f1c3e..1b56c15b 100644 --- a/boa/util/abi.py +++ b/boa/util/abi.py @@ -1,5 +1,5 @@ # wrapper module around whatever encoder we are using -from typing import Annotated, Any +from typing import Any from eth.codecs.abi import nodes from eth.codecs.abi.decoder import Decoder @@ -7,7 +7,6 @@ from eth.codecs.abi.exceptions import ABIError from eth.codecs.abi.nodes import ABITypeNode from eth.codecs.abi.parser import Parser -from eth_typing import Address as PYEVM_Address from eth_utils import to_canonical_address, to_checksum_address from boa.util.lrudict import lrudict @@ -15,15 +14,13 @@ _parsers: dict[str, ABITypeNode] = {} -# XXX: inherit from bytes directly so that we can pass it to py-evm? -# inherit from `str` so that ABI encoder / decoder can work without failing -class Address(str): # (PYEVM_Address): +class Address(bytes): # converting between checksum and canonical addresses is a hotspot; # this class contains both and caches recently seen conversions - __slots__ = ("canonical_address",) + # TODO: maybe this belongs in its own module _cache = lrudict(1024) - canonical_address: Annotated[PYEVM_Address, "canonical address"] + checksum_address: str def __new__(cls, address): if isinstance(address, Address): @@ -34,15 +31,14 @@ def __new__(cls, address): except KeyError: pass - checksum_address = to_checksum_address(address) - self = super().__new__(cls, checksum_address) - self.canonical_address = to_canonical_address(address) + canonical_address = to_canonical_address(address) + self = super().__new__(cls, canonical_address) + self.checksum_address = to_checksum_address(address) cls._cache[address] = self return self def __repr__(self): - checksum_addr = super().__repr__() - return f"_Address({checksum_addr})" + return f"_Address({self.checksum_address})" class _ABIEncoder(Encoder): @@ -54,6 +50,13 @@ class _ABIEncoder(Encoder): @classmethod def visit_AddressNode(cls, node: nodes.AddressNode, value) -> bytes: value = getattr(value, "address", value) + + if isinstance(value, Address): + assert len(value) == 20 # guaranteed by to_canonical_address + # for performance, inline the implementation + # return the bytes value, left-pad with zeros + return value.rjust(32, b"\x00") + return super().visit_AddressNode(node, value) diff --git a/tests/unitary/jupyter/test_browser.py b/tests/unitary/jupyter/test_browser.py index 259631bd..c3cfa59b 100644 --- a/tests/unitary/jupyter/test_browser.py +++ b/tests/unitary/jupyter/test_browser.py @@ -157,7 +157,7 @@ def test_browser_sign_typed_data( browser, display_mock, mock_inject_javascript, mock_callback ): signer = browser.BrowserSigner(boa.env.generate_address()) - signature = boa.env.generate_address() + signature = boa.env.generate_address().checksum_address mock_callback("signTypedData", signature) data = signer.sign_typed_data({"name": "My App"}, {"types": []}, {"data": "0x1234"}) assert data == signature diff --git a/tests/unitary/strategy/test_address.py b/tests/unitary/strategy/test_address.py index f30578d7..07601ad2 100644 --- a/tests/unitary/strategy/test_address.py +++ b/tests/unitary/strategy/test_address.py @@ -1,18 +1,15 @@ from hypothesis import HealthCheck, given, settings -from hypothesis.strategies._internal.deferred import DeferredStrategy +from hypothesis.strategies import SearchStrategy from boa.test import strategy +from boa.util.abi import Address def test_strategy(): - assert isinstance(strategy("address"), DeferredStrategy) + assert isinstance(strategy("address"), SearchStrategy) @given(value=strategy("address")) @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) def test_given(value): - assert isinstance(value, str) - - -def test_repr(): - assert repr(strategy("address")) == "sampled_from(accounts)" + assert isinstance(value, Address) diff --git a/tests/unitary/test_blueprints.py b/tests/unitary/test_blueprints.py index ca6d9374..09a431a7 100644 --- a/tests/unitary/test_blueprints.py +++ b/tests/unitary/test_blueprints.py @@ -28,6 +28,6 @@ def test_create2_address(): blueprint_bytecode = boa.env.vm.state.get_code( to_canonical_address(blueprint.address) ) - assert child_contract_address == get_create2_address( + assert child_contract_address.checksum_address == get_create2_address( blueprint_bytecode, factory.address, salt ) From 09396eaf07fabe0cfee7857c7411af9730fbec41 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Fri, 16 Feb 2024 06:15:40 -0800 Subject: [PATCH 2/3] add comment from code review Co-authored-by: Daniel Schiavini --- boa/util/abi.py | 1 + 1 file changed, 1 insertion(+) diff --git a/boa/util/abi.py b/boa/util/abi.py index 1b56c15b..a1ccfd0d 100644 --- a/boa/util/abi.py +++ b/boa/util/abi.py @@ -14,6 +14,7 @@ _parsers: dict[str, ABITypeNode] = {} +# inherit from bytes so we don't need conversion when interacting with pyevm class Address(bytes): # converting between checksum and canonical addresses is a hotspot; # this class contains both and caches recently seen conversions From 8e978c39fda52c25b0621dffc1b1b16159ade9b4 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Fri, 16 Feb 2024 09:16:04 -0500 Subject: [PATCH 3/3] fix a comment --- boa/util/abi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boa/util/abi.py b/boa/util/abi.py index a1ccfd0d..61c81b58 100644 --- a/boa/util/abi.py +++ b/boa/util/abi.py @@ -18,7 +18,7 @@ class Address(bytes): # converting between checksum and canonical addresses is a hotspot; # this class contains both and caches recently seen conversions - # TODO: maybe this belongs in its own module + # TODO: maybe this class belongs in its own module _cache = lrudict(1024) checksum_address: str