Skip to content

Commit

Permalink
test: test and document proxy system [APE-197] (#1507)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Jun 29, 2023
1 parent 3327fc6 commit ac107e7
Show file tree
Hide file tree
Showing 12 changed files with 170 additions and 35 deletions.
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
userguides/transactions
userguides/console
userguides/contracts
userguides/proxy
userguides/testing
userguides/scripts
userguides/publishing
Expand Down
38 changes: 38 additions & 0 deletions docs/userguides/proxy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Proxy Contracts

Ape is able to detect proxy contracts so that it uses the target interface when interacting with a contract.
The following proxies are supporting in `ape-ethereum`:

| Proxy Type | Short Description |
| ------------ | --------------------------------- |
| Minimal | EIP-1167 |
| Standard | EIP-1967 |
| Beacon | EIP-1967 |
| UUPS | EIP-1822 |
| Vyper | vyper \<0.2.9 create_forwarder_to |
| Clones | 0xsplits clones |
| Safe | Formerly Gnosis Safe |
| OpenZeppelin | OZ Upgradable |
| Delegate | EIP-897 |
| ZeroAge | A minimal proxy |
| SoladyPush0 | Uses PUSH0 |

Proxy detection occurs when attempting to retrieve contract types in Ape.
Ape uses various sources to find contract types, such as explorer APIs.
See [this guide](./contracts.html) to learn more about initializing contracts.

```python
from ape import Contract

my_contract = Contract("0x...")
```

Ape will check the address you give it and detect if hosts a proxy contract.
In the case where it determines the address is a proxy contract, it resolves the address of the implementation (every proxy is different) and returns the interface for the implementation contract.
This allows you to still call methods as you normally do on proxy contracts.

```python
# `my_contract` address points to a proxy with no methods in the interface
# However, Ape detected the implementation type and can find methods to call that way.
my_contract.my_method(sender=account)
```
22 changes: 2 additions & 20 deletions src/ape_ethereum/ecosystem.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import re
from copy import deepcopy
from enum import IntEnum
from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union, cast

from eth_abi import decode, encode
Expand All @@ -21,7 +20,7 @@
from pydantic import Field, validator

from ape.api import BlockAPI, EcosystemAPI, PluginConfig, ReceiptAPI, TransactionAPI
from ape.api.networks import LOCAL_NETWORK_NAME, ProxyInfoAPI
from ape.api.networks import LOCAL_NETWORK_NAME
from ape.contracts.base import ContractCall
from ape.exceptions import (
ApeException,
Expand Down Expand Up @@ -52,6 +51,7 @@
)
from ape.utils.abi import _convert_kwargs
from ape.utils.misc import DEFAULT_MAX_RETRIES_TX
from ape_ethereum.proxies import ProxyInfo, ProxyType
from ape_ethereum.transactions import (
AccessListTransaction,
BaseTransaction,
Expand All @@ -71,24 +71,6 @@
BLUEPRINT_HEADER = HexBytes("0xfe71")


class ProxyType(IntEnum):
Minimal = 0 # eip-1167 minimal proxy contract
Standard = 1 # eip-1967 standard proxy storage slots
Beacon = 2 # eip-1967 beacon proxy
UUPS = 3 # # eip-1822 universal upgradeable proxy standard
Vyper = 4 # vyper <0.2.9 create_forwarder_to
Clones = 5 # 0xsplits clones
GnosisSafe = 6
OpenZeppelin = 7 # openzeppelin upgradeability proxy
Delegate = 8 # eip-897 delegate proxy
ZeroAge = 9 # a more-minimal proxy
SoladyPush0 = 10 # solady push0 minimal proxy


class ProxyInfo(ProxyInfoAPI):
type: ProxyType


class NetworkConfig(PluginConfig):
required_confirmations: int = 0

Expand Down
39 changes: 39 additions & 0 deletions src/ape_ethereum/proxies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from enum import IntEnum

from ethpm_types import ContractType
from lazyasd import LazyObject # type: ignore

from ape.api.networks import ProxyInfoAPI
from ape.contracts import ContractContainer

MINIMAL_PROXY_BYTES = (
"0x3d602d80600a3d3981f3363d3d373d3d3d363d73"
"bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3"
)


class ProxyType(IntEnum):
Minimal = 0 # eip-1167 minimal proxy contract
Standard = 1 # eip-1967 standard proxy storage slots
Beacon = 2 # eip-1967 beacon proxy
UUPS = 3 # # eip-1822 universal upgradeable proxy standard
Vyper = 4 # vyper <0.2.9 create_forwarder_to
Clones = 5 # 0xsplits clones
GnosisSafe = 6
OpenZeppelin = 7 # openzeppelin upgradeability proxy
Delegate = 8 # eip-897 delegate proxy
ZeroAge = 9 # a more-minimal proxy
SoladyPush0 = 10 # solady push0 minimal proxy


class ProxyInfo(ProxyInfoAPI):
type: ProxyType


def _make_minimal_proxy() -> ContractContainer:
bytecode = {"bytecode": MINIMAL_PROXY_BYTES}
contract_type = ContractType(abi=[], deploymentBytecode=bytecode)
return ContractContainer(contract_type=contract_type)


minimal_proxy = LazyObject(_make_minimal_proxy, globals(), "minimal_proxy")
6 changes: 6 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from ape.exceptions import APINotImplementedError, UnknownSnapshotError
from ape.managers.config import CONFIG_FILE_NAME
from ape.types import AddressType
from ape.utils import ZERO_ADDRESS

# NOTE: Ensure that we don't use local paths for these
ape.config.DATA_FOLDER = Path(mkdtemp()).resolve()
Expand Down Expand Up @@ -340,3 +341,8 @@ def test_skip_from_converter():
return fn

return wrapper


@pytest.fixture
def zero_address():
return ZERO_ADDRESS
5 changes: 5 additions & 0 deletions tests/functional/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -561,3 +561,8 @@ def vyper_factory(owner, get_contract_type):
def vyper_blueprint(owner, vyper_contract_container):
receipt = owner.declare(vyper_contract_container)
return receipt.contract_address


@pytest.fixture
def geth_vyper_contract(owner, vyper_contract_container, geth_provider):
return owner.deploy(vyper_contract_container, 0)
1 change: 1 addition & 0 deletions tests/functional/data/contracts/ethereum/local/beacon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"abi":[{"inputs":[{"internalType":"address","name":"_addr","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"implementation","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"}],"ast":{"ast_type":"SourceUnit","children":[{"ast_type":"ImportDirective","children":[],"classification":0,"col_offset":-1,"end_col_offset":-1,"end_lineno":-1,"lineno":-1,"src":{"contract_id":1,"jump_code":"","length":58}},{"ast_type":"ContractDefinition","children":[{"ast_type":"InheritanceSpecifier","children":[{"ast_type":"IdentifierPath","children":[],"classification":0,"col_offset":-1,"end_col_offset":-1,"end_lineno":-1,"lineno":-1,"name":"IBeacon","src":{"contract_id":1,"jump_code":"","length":7,"start":79}}],"classification":0,"col_offset":-1,"end_col_offset":-1,"end_lineno":-1,"lineno":-1,"src":{"contract_id":1,"jump_code":"","length":7,"start":79}},{"ast_type":"VariableDeclaration","children":[{"ast_type":"ElementaryTypeName","children":[],"classification":0,"col_offset":-1,"end_col_offset":-1,"end_lineno":-1,"lineno":-1,"name":"address","src":{"contract_id":1,"jump_code":"","length":7,"start":93}}],"classification":0,"col_offset":-1,"end_col_offset":-1,"end_lineno":-1,"lineno":-1,"name":"addr","src":{"contract_id":1,"jump_code":"","length":12,"start":93}},{"ast_type":"FunctionDefinition","children":[{"ast_type":"Block","children":[{"ast_type":"ExpressionStatement","children":[{"ast_type":"Assignment","children":[{"ast_type":"Identifier","children":[],"classification":0,"col_offset":-1,"end_col_offset":-1,"end_lineno":-1,"lineno":-1,"name":"addr","src":{"contract_id":1,"jump_code":"","length":4,"start":149}},{"ast_type":"Identifier","children":[],"classification":0,"col_offset":-1,"end_col_offset":-1,"end_lineno":-1,"lineno":-1,"name":"_addr","src":{"contract_id":1,"jump_code":"","length":5,"start":156}}],"classification":0,"col_offset":-1,"end_col_offset":-1,"end_lineno":-1,"lineno":-1,"src":{"contract_id":1,"jump_code":"","length":12,"start":149}}],"classification":0,"col_offset":-1,"end_col_offset":-1,"end_lineno":-1,"lineno":-1,"src":{"contract_id":1,"jump_code":"","length":12,"start":149}}],"classification":0,"col_offset":-1,"end_col_offset":-1,"end_lineno":-1,"lineno":-1,"src":{"contract_id":1,"jump_code":"","length":29,"start":139}},{"ast_type":"ParameterList","children":[{"ast_type":"VariableDeclaration","children":[{"ast_type":"ElementaryTypeName","children":[],"classification":0,"col_offset":-1,"end_col_offset":-1,"end_lineno":-1,"lineno":-1,"name":"address","src":{"contract_id":1,"jump_code":"","length":7,"start":124}}],"classification":0,"col_offset":-1,"end_col_offset":-1,"end_lineno":-1,"lineno":-1,"name":"_addr","src":{"contract_id":1,"jump_code":"","length":13,"start":124}}],"classification":0,"col_offset":-1,"end_col_offset":-1,"end_lineno":-1,"lineno":-1,"src":{"contract_id":1,"jump_code":"","length":15,"start":123}},{"ast_type":"ParameterList","children":[],"classification":0,"col_offset":-1,"end_col_offset":-1,"end_lineno":-1,"lineno":-1,"src":{"contract_id":1,"jump_code":"","start":139}}],"classification":1,"col_offset":-1,"end_col_offset":-1,"end_lineno":-1,"lineno":-1,"name":"","src":{"contract_id":1,"jump_code":"","length":56,"start":112}},{"ast_type":"FunctionDefinition","children":[{"ast_type":"Block","children":[{"ast_type":"Return","children":[{"ast_type":"Identifier","children":[],"classification":0,"col_offset":-1,"end_col_offset":-1,"end_lineno":-1,"lineno":-1,"name":"addr","src":{"contract_id":1,"jump_code":"","length":4,"start":249}}],"classification":0,"col_offset":-1,"end_col_offset":-1,"end_lineno":-1,"lineno":-1,"src":{"contract_id":1,"jump_code":"","length":11,"start":242}}],"classification":0,"col_offset":-1,"end_col_offset":-1,"end_lineno":-1,"lineno":-1,"src":{"contract_id":1,"jump_code":"","length":28,"start":232}},{"ast_type":"ParameterList","children":[],"classification":0,"col_offset":-1,"end_col_offset":-1,"end_lineno":-1,"lineno":-1,"src":{"contract_id":1,"jump_code":"","length":2,"start":197}},{"ast_type":"ParameterList","children":[{"ast_type":"VariableDeclaration","children":[{"ast_type":"ElementaryTypeName","children":[],"classification":0,"col_offset":-1,"end_col_offset":-1,"end_lineno":-1,"lineno":-1,"name":"address","src":{"contract_id":1,"jump_code":"","length":7,"start":223}}],"classification":0,"col_offset":-1,"end_col_offset":-1,"end_lineno":-1,"lineno":-1,"name":"","src":{"contract_id":1,"jump_code":"","length":7,"start":223}}],"classification":0,"col_offset":-1,"end_col_offset":-1,"end_lineno":-1,"lineno":-1,"src":{"contract_id":1,"jump_code":"","length":9,"start":222}}],"classification":1,"col_offset":-1,"end_col_offset":-1,"end_lineno":-1,"lineno":-1,"name":"implementation","src":{"contract_id":1,"jump_code":"","length":86,"start":174}}],"classification":0,"col_offset":-1,"end_col_offset":-1,"end_lineno":-1,"lineno":-1,"name":"Beacon","src":{"contract_id":1,"jump_code":"","length":202,"start":60}}],"classification":0,"col_offset":-1,"end_col_offset":-1,"end_lineno":-1,"lineno":-1,"src":{"contract_id":1,"jump_code":"","length":263}},"contractName":"Beacon","deploymentBytecode":{"bytecode":"0x608060405234801561001057600080fd5b5060405161011438038061011483398101604081905261002f91610054565b600080546001600160a01b0319166001600160a01b0392909216919091179055610084565b60006020828403121561006657600080fd5b81516001600160a01b038116811461007d57600080fd5b9392505050565b6082806100926000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80635c60da1b14602d575b600080fd5b600054604080516001600160a01b039092168252519081900360200190f3fea2646970667358221220f690a74086006b293a82ea58c45ab6814cd9d5774d31a6cd70c9a398b85ee28864736f6c63430008140033"},"devdoc":{"kind":"dev","methods":{"implementation()":{"details":"Must return an address that can be used as a delegate call target. {BeaconProxy} will check that this address is a contract."}},"version":1},"runtimeBytecode":{"bytecode":"0x6080604052348015600f57600080fd5b506004361060285760003560e01c80635c60da1b14602d575b600080fd5b600054604080516001600160a01b039092168252519081900360200190f3fea2646970667358221220f690a74086006b293a82ea58c45ab6814cd9d5774d31a6cd70c9a398b85ee28864736f6c63430008140033"},"sourceId":"Beacon.sol","sourcemap":"60:202:1:-:0;;;112:56;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;149:4;:12;;-1:-1:-1;;;;;;149:12:1;-1:-1:-1;;;;;149:12:1;;;;;;;;;;60:202;;14:290:2;84:6;137:2;125:9;116:7;112:23;108:32;105:52;;;153:1;150;143:12;105:52;179:16;;-1:-1:-1;;;;;224:31:2;;214:42;;204:70;;270:1;267;260:12;204:70;293:5;14:290;-1:-1:-1;;;14:290:2:o;:::-;60:202:1;;;;;;","userdoc":{"kind":"user","methods":{},"version":1}}

Large diffs are not rendered by default.

Large diffs are not rendered by default.

15 changes: 8 additions & 7 deletions tests/functional/test_contract_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
CustomError,
)
from ape.types import AddressType
from ape.utils import ZERO_ADDRESS
from ape_ethereum.transactions import TransactionStatusEnum

MATCH_TEST_CONTRACT = re.compile(r"<TestContract((Sol)|(Vy))")
Expand Down Expand Up @@ -268,10 +267,12 @@ def test_get_empty_tuple_of_dyn_array_structs(contract_instance):
assert actual == expected


def test_get_empty_tuple_of_array_of_structs_and_dyn_array_of_structs(contract_instance):
def test_get_empty_tuple_of_array_of_structs_and_dyn_array_of_structs(
contract_instance, zero_address
):
actual = contract_instance.getEmptyTupleOfArrayOfStructsAndDynArrayOfStructs()
expected_fixed_array = (
ZERO_ADDRESS,
zero_address,
HexBytes("0x0000000000000000000000000000000000000000000000000000000000000000"),
)
assert actual[0] == [expected_fixed_array, expected_fixed_array, expected_fixed_array]
Expand Down Expand Up @@ -422,12 +423,12 @@ def test_get_unnamed_tuple(contract_instance):
assert actual == (0, 0)


def test_get_tuple_of_address_array(contract_instance):
def test_get_tuple_of_address_array(contract_instance, zero_address):
actual = contract_instance.getTupleOfAddressArray()
assert len(actual) == 2
assert len(actual[0]) == 20
assert is_checksum_address(actual[0][0])
assert all(x == ZERO_ADDRESS for x in actual[0][1:])
assert all(x == zero_address for x in actual[0][1:])
assert actual[1] == [0] * 20


Expand Down Expand Up @@ -457,13 +458,13 @@ def test_get_nested_array_mixed_dynamic(contract_instance, owner):
assert actual[2] == actual[3] == actual[4] == []


def test_get_nested_address_array(contract_instance, sender):
def test_get_nested_address_array(contract_instance, sender, zero_address):
actual = contract_instance.getNestedAddressArray()
assert len(actual) == 2
assert len(actual[0]) == 3
assert len(actual[1]) == 3
assert actual[0] == [sender, sender, sender]
assert actual[1] == [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS]
assert actual[1] == [zero_address, zero_address, zero_address]


def test_call_transaction(contract_instance, owner, chain):
Expand Down
12 changes: 4 additions & 8 deletions tests/functional/test_geth.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
OutOfGasError,
TransactionNotFoundError,
)
from ape.utils import ZERO_ADDRESS
from ape_ethereum.ecosystem import Block
from ape_geth.provider import Geth
from tests.conftest import GETH_URI, geth_process_test
Expand Down Expand Up @@ -113,11 +112,6 @@ def geth_receipt(contract_with_call_depth_geth, owner, geth_provider):
return contract_with_call_depth_geth.methodWithoutArguments(sender=owner)


@pytest.fixture
def geth_vyper_contract(owner, vyper_contract_container, geth_provider):
return owner.deploy(vyper_contract_container, 0)


@pytest.fixture
def geth_vyper_receipt(geth_vyper_contract, owner):
return geth_vyper_contract.setNumber(44, sender=owner)
Expand Down Expand Up @@ -426,11 +420,13 @@ def test_return_value_list(geth_account, geth_contract, geth_provider):


@geth_process_test
def test_return_value_nested_address_array(geth_account, geth_contract, geth_provider):
def test_return_value_nested_address_array(
geth_account, geth_contract, geth_provider, zero_address
):
receipt = geth_contract.getNestedAddressArray.transact(sender=geth_account)
expected = [
[geth_account.address, geth_account.address, geth_account.address],
[ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS],
[zero_address, zero_address, zero_address],
]
assert receipt.return_value == expected

Expand Down
64 changes: 64 additions & 0 deletions tests/functional/test_proxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import pytest
from ethpm_types import HexBytes

from ape.contracts import ContractContainer
from ape_ethereum.proxies import ProxyType
from ape_ethereum.proxies import minimal_proxy as minimal_proxy_container
from tests.conftest import geth_process_test


@pytest.fixture
def minimal_proxy(owner):
return owner.deploy(minimal_proxy_container)


@pytest.fixture
def standard_proxy(owner, get_contract_type, geth_vyper_contract):
_type = get_contract_type("eip1967")
contract = ContractContainer(_type)
target = geth_vyper_contract.address
return owner.deploy(contract, target, HexBytes(""))


@pytest.fixture
def beacon(owner, get_contract_type, geth_provider, vyper_contract_instance):
_type = get_contract_type("beacon")
contract = ContractContainer(_type)
return owner.deploy(contract, vyper_contract_instance)


@pytest.fixture
def beacon_proxy(owner, get_contract_type, beacon, geth_provider):
_type = get_contract_type("beacon_proxy")
contract = ContractContainer(_type)
return owner.deploy(contract, beacon, HexBytes(""))


def test_minimal_proxy(ethereum, minimal_proxy):
actual = ethereum.get_proxy_info(minimal_proxy.address)
assert actual is not None
assert actual.type == ProxyType.Minimal
# It is the placeholder value still.
assert actual.target == "0xBEbeBeBEbeBebeBeBEBEbebEBeBeBebeBeBebebe"


@geth_process_test
def test_standard_proxy(ethereum, standard_proxy, geth_provider, geth_vyper_contract):
"""
NOTE: Geth is used here because EthTester does not implement getting storage slots.
"""
actual = ethereum.get_proxy_info(standard_proxy.address)
assert actual is not None
assert actual.type == ProxyType.Standard
assert actual.target == geth_vyper_contract.address


@geth_process_test
def test_beacon_proxy(ethereum, beacon_proxy, geth_provider, vyper_contract_instance):
"""
NOTE: Geth is used here because EthTester does not implement getting storage slots.
"""
actual = ethereum.get_proxy_info(beacon_proxy.address)
assert actual is not None
assert actual.type == ProxyType.Beacon
assert actual.target == vyper_contract_instance.address

0 comments on commit ac107e7

Please sign in to comment.