|Expect rapid iteration as this pattern matures and more patterns potentially emerge.|
The general workflow is:
declare an implementation contract class
deploy proxy contract with the implementation contract’s class hash set in the proxy’s constructor calldata
initialize the implementation contract by sending a call to the proxy contract. This will redirect the call to the implementation contract class and behave like the implementation contract’s constructor
In Python, this would look as follows:
# declare implementation contract 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 contract class hash ] ) # users should only interact with the proxy contract await signer.send_transaction( account, PROXY.contract_address, 'initializer', [ proxy_admin ] )
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.
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.
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.
The Proxy contract includes two core methods:
__default__method is a fallback method that redirects a function call and associated calldata to the implementation contract.
__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.
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:
initialize the proxy immediately after contract deployment with
If the implementation is upgradeable, it should:
include a method to upgrade the implementation (i.e.
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
DeclaredClasscontaining its hash and abi)
set its initial state with a traditional constructor (decorated with
@constructor). Instead, use an initializer method that invokes the Proxy
For a full implementation contract example, please see:
func initializer(proxy_admin: felt): end func assert_only_admin(): end func get_implementation_hash() -> (implementation: felt): end func get_admin() -> (admin: felt): end func _set_admin(new_admin: felt): end func _set_implementation_hash(new_implementation: felt): end
Initializes the proxy contract with an initial implementation.
Reverts if called by any account other than the admin.
Returns the current implementation hash.
new_admin as the admin of the proxy contract.
func Upgraded(implementation: felt): end func AdminChanged(previousAdmin: felt, newAdmin: felt): end
Emitted when a proxy contract sets a new implementation class hash.
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 ] ) # 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:
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.
As with most StarkNet contracts, interacting with a proxy contract requires an account abstraction.
One notable difference with proxy contracts versus other contract implementations is that calling
@view methods also requires an account abstraction.
As of now, direct calls to default entrypoints are only supported by StarkNet’s
syscalls from other contracts i.e.
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',  )