Expect rapid iteration as this pattern matures and more patterns potentially emerge.
The general workflow is:
- deploy implementation contract
- deploy proxy contract with the implementation contract's address 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 and behave like the implementation contract's constructor
In Python, this would look as follows:
# deploy implementation
IMPLEMENTATION = await starknet.deploy(
"path/to/implementation.cairo",
constructor_calldata=[]
)
# deploy proxy
PROXY = await starknet.deploy(
"path/to/proxy.cairo",
constructor_calldata=[
IMPLEMENTATION.contract_address, # set implementation address
]
)
# users should only interact with the proxy contract
await signer.send_transaction(
account, PROXY.contract_address, 'initialize', [
arg_1,
arg_2
]
)
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 implementation contract. 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:
-
The
__default__
method is a fallback method that redirects a function call and associated calldata to the implementation contract. -
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 functionsend_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 to be deployed beforehand, so its address 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:
- import
Proxy_initializer
andProxy_set_implementation
- initialize the proxy immediately after contract 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:
- deploy with a traditional constructor. Instead, use an initializer method that invokes
Proxy_initializer
.
Note that the imported
Proxy_initializer
includes a check the ensures the initializer can only be called once; however,Proxy_set_implementation
does not include this check. It's up to the developers to protect their implementation contract's upgradeability with access controls such asProxy_only_admin
.
For a full implementation contract example, please see:
func Proxy_initializer(proxy_admin: felt):
end
func Proxy_set_implementation(new_implementation: felt):
end
func Proxy_only_admin():
end
func Proxy_get_admin() -> (admin: felt):
end
func Proxy_get_implementation() -> (implementation: felt):
end
func Proxy_set_admin(new_admin: felt):
end
Initializes the proxy contract with an initial implementation.
Parameters:
proxy_admin: felt
Returns:
None.
Sets the implementation contract. This method is included in the proxy contract's constructor and is furthermore used to upgrade contracts.
Parameters:
new_implementation: felt
Returns:
None.
Throws if called by any account other than the admin.
Parameters:
None.
Returns:
None.
Returns the current admin.
Parameters:
None.
Returns:
admin: felt
Returns the current implementation address.
Parameters:
None.
Returns:
implementation: felt
Sets the admin of the proxy contract.
Parameters:
new_admin: felt
Returns:
None.
func Upgraded(implementation: felt):
end
Emitted when a proxy contract sets a new implementation address.
Parameters:
implementation: felt
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:
# deploy first implementation
IMPLEMENTATION = await starknet.deploy(
"path/to/implementation.cairo",
constructor_calldata=[]
)
# deploy proxy
PROXY = await starknet.deploy(
"path/to/proxy.cairo",
constructor_calldata=[
IMPLEMENTATION.contract_address, # set implementation address
]
)
# deploy implementation v2
IMPLEMENTATION_V2 = await starknet.deploy(
"path/to/implementation_v2.cairo",
constructor_calldata=[]
)
# call upgrade with the new implementation contract address
await signer.send_transaction(
account, PROXY.contract_address, 'upgrade', [
IMPLEMENTATION_V2.contract_address
]
)
For a full deployment and upgrade implementation, please see:
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. account contracts. 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 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:
- ERC20_Upgradeable
- more to come! have an idea? open an issue!