Proxies

Expect rapid iteration as this pattern matures and more patterns potentially emerge.

Quickstart

The general workflow is:

  1. Declare an implementation contract class.

  2. Deploy proxy contract with the implementation contract’s class hash, and the inputs describing the call to initialize the proxy from the implementation (if required). This will redirect the call as a library_call to the implementation (similar to Solidity delegatecall).

In Python, this would look as follows:

# declare implementation contract
IMPLEMENTATION = await starknet.declare(
    "path/to/implementation.cairo",
)

# deploy proxy
selector = get_selector_from_name('initializer')
params = [
    proxy_admin   # admin account
]
PROXY = await starknet.deploy(
    "path/to/proxy.cairo",
    constructor_calldata=[
        IMPLEMENTATION.class_hash,  # set implementation contract class hash
        selector,                   # initializer function selector
        len(params),                # calldata length in felt
        *params                     # actual calldata
    ]
)

Solidity/Cairo upgrades comparison

Constructors

OpenZeppelin Contracts for Solidity requires the use of an alternative library for upgradeable contracts. Consider that in Solidity constructors are not part of the deployed contract’s runtime bytecode; rather, a constructor’s logic is executed only once when the contract instance is deployed and then discarded. This is why proxies can’t imitate the construction of its implementation, therefore requiring a different initialization mechanism.

The constructor problem in upgradeable contracts is resolved by the use of initializer methods. Initializer methods are essentially regular methods that execute the logic that would have been in the constructor. Care needs to be exercised with initializers to ensure they can only be called once. Thus, OpenZeppelin offers an upgradeable contracts library where much of this process is abstracted away. See OpenZeppelin’s Writing Upgradeable Contracts for more info.

The Cairo programming language does not support inheritance. Instead, Cairo contracts follow the Extensibility Pattern which already uses initializer methods to mimic constructors. Upgradeable contracts do not, therefore, require a separate library with refactored constructor logic.

Storage

OpenZeppelin’s alternative Upgrades library also implements unstructured storage for its upgradeable contracts. The basic idea behind unstructured storage is to pseudo-randomize the storage structure of the upgradeable contract so it’s based on variable names instead of declaration order, which makes the chances of storage collision during an upgrade extremely unlikely.

The StarkNet compiler, meanwhile, already creates pseudo-random storage addresses by hashing the storage variable names (and keys in mappings) by default. In other words, StarkNet already uses unstructured storage and does not need a second library to modify how storage is set. See StarkNet’s Contracts Storage documentation for more information.

Proxies

A proxy contract is a contract that delegates function calls to another contract. This type of pattern decouples state and logic. Proxy contracts store the state and redirect function calls to an implementation contract that handles the logic. This allows for different patterns such as upgrades, where implementation contracts can change but the proxy contract (and thus the state) does not; as well as deploying multiple proxy instances pointing to the same implementation. This can be useful to deploy many contracts with identical logic but unique initialization data.

In the case of contract upgrades, it is achieved by simply changing the proxy’s reference to the class hash of the declared implementation. This allows developers to add features, update logic, and fix bugs without touching the state or the contract address to interact with the application.

Proxy contract

The Proxy contract includes two core methods:

  1. The __default__ method is a fallback method that redirects a function call and associated calldata to the implementation contract.

  2. The __l1_default__ method is also a fallback method; however, it redirects the function call and associated calldata to a layer one contract. In order to invoke __l1_default__, the original function call must include the library function send_message_to_l1. See Cairo’s Interacting with L1 contracts for more information.

Since this proxy is designed to work both as an UUPS-flavored upgrade proxy as well as a non-upgradeable proxy, it does not know how to handle its own state. Therefore it requires the implementation contract class to be declared beforehand, so its class hash can be passed to the Proxy on construction time.

When interacting with the contract, function calls should be sent by the user to the proxy. The proxy’s fallback function redirects the function call to the implementation contract to execute.

Implementation contract

The implementation contract, also known as the logic contract, receives the redirected function calls from the proxy contract. The implementation contract should follow the Extensibility pattern and import directly from the Proxy library.

The implementation contract should:

  • Import Proxy namespace.

  • Provide an external initializer function (calling Proxy.initializer) to intialize the proxy immediately after deployment.

If the implementation is upgradeable, it should:

  • Include a method to upgrade the implementation (i.e. upgrade).

  • Use access control to protect the contract’s upgradeability.

The implementation contract should NOT:

  • Be deployed like a regular contract. Instead, the implementation contract should be declared (which creates a DeclaredClass containing its hash and abi).

  • Set its initial state with a traditional constructor (decorated with @constructor). Instead, use an initializer method that invokes the Proxy constructor.

The Proxy constructor includes a check that ensures the initializer can only be called once; however, _set_implementation does not include this check. It’s up to the developers to protect their implementation contract’s upgradeability with access controls such as assert_only_admin.

For a full implementation contract example, please see:

Upgrades library API

Methods

func initializer(proxy_admin: felt) {
}

func assert_only_admin() {
}

func get_implementation_hash() -> (implementation: felt) {
}

func get_admin() -> (admin: felt) {
}

func _set_admin(new_admin: felt) {
}

func _set_implementation_hash(new_implementation: felt) {
}

initializer

Initializes the proxy contract with an initial implementation.

Parameters:

proxy_admin: felt

Returns: None.

assert_only_admin

Reverts if called by any account other than the admin.

Parameters: None.

Returns: None.

get_implementation

Returns the current implementation hash.

Parameters: None.

Returns:

implementation: felt

get_admin

Returns the current admin.

Parameters: None.

Returns:

admin: felt

_set_admin

Sets new_admin as the admin of the proxy contract.

Parameters:

new_admin: felt

Returns: None.

_set_implementation_hash

Sets new_implementation as the implementation’s contract class. This method is included in the proxy contract’s constructor and can be used to upgrade contracts.

Parameters:

new_implementation: felt

Returns: None.

Events

func Upgraded(implementation: felt) {
}

func AdminChanged(previousAdmin: felt, newAdmin: felt) {
}

Upgraded

Emitted when a proxy contract sets a new implementation class hash.

Parameters:

implementation: felt

AdminChanged

Emitted when the admin changes from previousAdmin to newAdmin.

Parameters:

previousAdmin: felt
newAdmin: felt

Using proxies

Contract upgrades

To upgrade a contract, the implementation contract should include an upgrade method that, when called, changes the reference to a new deployed contract like this:

# declare first implementation
IMPLEMENTATION = await starknet.declare(
    "path/to/implementation.cairo",
)

# deploy proxy
PROXY = await starknet.deploy(
    "path/to/proxy.cairo",
    constructor_calldata=[
        IMPLEMENTATION.class_hash,  # set implementation hash
        0,                          # selector set to 0 ignores initialization
        0,                          # calldata length in felt
        *[]                         # empty calldata
    ]
)

# declare implementation v2
IMPLEMENTATION_V2 = await starknet.declare(
    "path/to/implementation_v2.cairo",
)

# call upgrade with the new implementation contract class hash
await signer.send_transaction(
    account, PROXY.contract_address, 'upgrade', [
        IMPLEMENTATION_V2.class_hash
    ]
)

For a full deployment and upgrade implementation, please see:

Declaring contracts

StarkNet contracts come in two forms: contract classes and contract instances. Contract classes represent the uninstantiated, stateless code; whereas, contract instances are instantiated and include the state. Since the Proxy contract references the implementation contract by its class hash, declaring an implementation contract proves sufficient (as opposed to a full deployment). For more information on declaring classes, see StarkNet’s documentation.

Testing method calls

As with most StarkNet contracts, interacting with a proxy contract requires an account abstraction. Due to limitations in the StarkNet testing framework, however, @view methods also require an account abstraction. This is only a requirement when testing. The differences in getter methods written in Python, for example, are as follows:

# standard ERC20 call
result = await erc20.totalSupply().call()

# upgradeable ERC20 call
result = await signer.send_transaction(
    account, PROXY.contract_address, 'totalSupply', []
)

Presets

Presets are pre-written contracts that extend from our library of contracts. They can be deployed as-is or used as templates for customization.

Some presets include: