Introduction
Upgrades are where production bugs hide: missed initializers, wrong admin, or corrupted storage. Proxy patterns let you upgrade contracts, but they introduce complexity that traditional testing misses. Wake’s Python-first tests catch these issues before they reach mainnet.
The result is clean test code. Calling implementation functions through a proxy is straightforward:
contract = ExampleERC20Upgradeable(proxy)
Here’s how to test proxy contracts in Wake.
1. Import the Proxy Contract
Wake needs to compile your proxy contract to generate Python type bindings (pytypes). If the proxy contract lives in your library directory, Wake won’t compile it by default.
In wake.toml, the default configuration is exclude_paths = ["script", ".venv", "venv", "node_modules", "lib", "test"]. This means contracts in these paths are not compiled unless imported from a non-excluded file.
To make the contract available in your project, import it from somewhere outside exclude_paths. See the documentation for more details: https://ackee.xyz/wake/docs/latest/compilation/
Create tests/imports.sol to make pytypes available:
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
2. Import the Proxy in Python
Run wake up again to compile. Wake generates Python bindings for both your implementation and the proxy contract. Import them into your test file:
tests/test_upgradable.py
from pytypes.contracts.ExampleERC20Upgradeable import ExampleERC20Upgradeable
from pytypes.openzeppelin.contracts.proxy.ERC1967.ERC1967Proxy import ERC1967Proxy
3. Deploy and Initialize
Deploy the implementation contract first, then create the proxy pointing to it. The proxy’s initialization data encodes a call to the implementation’s `initialize` function:
@chain.connect()
@on_revert(revert_handler)
def test_default():
impl_erc20 = ExampleERC20Upgradeable.deploy()
proxy = ERC1967Proxy.deploy(
implementation =impl_erc20,
_data=abi.encode_call(ExampleERC20Upgradeable.initialize, ("Upgradable Token", "UPG", 10**20, chain.accounts[0])),
from_=chain.accounts[0]
)
The `_data` parameter encodes the initialization call that runs during proxy deployment. This replaces the constructor pattern used in non-upgradable contracts.
4. Access Implementation Functions Through the Proxy
Wrap the proxy address with the implementation contract class. This tells Wake to route all function calls through the proxy while using the implementation’s ABI:
contract = ExampleERC20Upgradeable(proxy)
Wake handles the delegatecall routing automatically, so you can interact with the contract as if it were a simple deployment.
5. Call Implementation Functions
All implementation functions are now available through the wrapped proxy. You can verify the contract’s behavior, inspect events, and test state changes:
# Verify initial balance
assert contract.balanceOf(chain.accounts[1]) == 0
# Execute transfer
tx = contract.transfer(chain.accounts[1], 10**18, from_=chain.accounts[0])
# Inspect emitted events
event = next(event for event in tx.events if isinstance(event, ExampleERC20Upgradeable.Transfer))
assert event.from_ == chain.accounts[0].address
assert event.to == chain.accounts[1].address
assert event.value == 10**18
# Verify updated balance
assert contract.balanceOf(chain.accounts[1]) == 10**18
The test confirms the proxy correctly delegates to the implementation and maintains state as expected.
Conclusion
Wake simplifies proxy testing through its Python bindings. Wrap the proxy address with the implementation class and call functions directly. The same approach works with unit tests and Manually Guided Fuzzing (MGF), letting you test upgradable contracts with the same tools you use for standard contracts.
This catches upgrade bugs (missed initializers, storage collisions, access control issues) before they become exploits. Test your proxy patterns the same way you test everything else.
Learn more here, look: A Beginner’s Guide to Manually Guided Fuzzing
Appendix: Full Test Code
import math
from wake.testing import *
from dataclasses import dataclass
from pytypes.contracts.ExampleERC20Upgradeable import ExampleERC20Upgradeable
from pytypes.openzeppelin.contracts.proxy.ERC1967.ERC1967Proxy import ERC1967Proxy
# Print failing tx call trace
def revert_handler(e: RevertError):
if e.tx is not None:
print(e.tx.call_trace)
@chain.connect()
@on_revert(revert_handler)
def test_default():
impl_erc20 = ExampleERC20Upgradeable.deploy()
proxy = ERC1967Proxy.deploy(
implementation =impl_erc20,
_data=abi.encode_call(ExampleERC20Upgradeable.initialize, ("Upgradable Token", "UPG", 10**20, chain.accounts[0])),
from_=chain.accounts[0]
)
contract = ExampleERC20Upgradeable(proxy) # Just wrap the proxy with the contract Class to call functions
assert contract.balanceOf(chain.accounts[1]) == 0
tx = contract.transfer(chain.accounts[1], 10**18, from_=chain.accounts[0])
event = next(event for event in tx.events if isinstance(event, ExampleERC20Upgradeable.Transfer))
assert event.from_ == chain.accounts[0].address
assert event.to == chain.accounts[1].address
assert event.value == 10**18
assert contract.balanceOf(chain.accounts[1]) == 10**18