StarkNet Guide: Writing Upgradable Contracts using a Proxy

Empiric Network
7 min readAug 18, 2022

--

This guide will cover how to use a proxy to write upgradable contracts on StarkNet. The proxy pattern consists of the proxy contract and the (upgradable) implementation contract. We will discuss a hands-on example of OpenZeppelin’s proxy guidelines. Along the way you will gain some practice using starknet.py.

You should refer to OpenZeppelin’s documentation as the source of truth, especially since the pattern might change in the future. This guide is up to date with v0.3.1.

written by Empiric Network engineer Raphael Ruban

Why use a proxy?

You can do the following without changing the entry point (address) of your dapp…

  • Bug fixes
  • Add functionality to your API

Using a proxy comes at the cost of…

  • More complicated contract deployment.
  • Having to understand the pattern to use it correctly, e.g. constructors vs initializers.
  • Having to use access controls to protect anyone from upgrading the contract.

You also have to consider the impact this pattern is going to have on the trust users put into your codebase. When using a proxy nothing stops you from swapping out your implementation unannounced. This might not be what your users want.

Getting started

  1. Setup your python environment using python 3.7. We recommend using a virtual environment.
  2. Install Nile.
pip install cairo-nile

3. Initialize your project.

mkdir cairo-proxy && cairo-proxy nile init

4. Install the OpenZeppelin library.

pip install openzeppelin-cairo-contracts

5. Install starknet.py so we can deploy and call our proxy.

pip install starknet.py

The Proxy Contract

The proxy contract forwards function calls to another contract by exposing the __default__ method. This method is a fallback method that redirects a function call and associated calldata to the implementation contract. The proxy knows where to delegate the call by storing the class hash of the declared implementation.

# Basic setup
User --- sends tx ---> Proxy --- delegates tx ---> Implementation_v0

Writing the proxy contract is very simple. We are just going to use one of OpenZeppelin’s presets, i.e. ready-to-use contracts. Create a file Proxy.cairo in ./contracts and copy-paste their proxy preset into newly created file. Your ./contracts/Proxy.cairo contract should have some key components.

  • It should import the the Proxy library: from openzeppelin.upgrades.library import Proxy
  • It should have a constructor setting the implementation_hash of your upgradable contract. Ideally the constructor also initializes the implementation contract (more on that in the next section).
  • It should have a __default__ function. (We won’t be interacting with the L1, so there is no need for __l1_default__.)

Making the deployment and initialization atomic

What happens if you forget to initialize the implementation contract?

  • All its functions are immediately callable through the proxy.
  • Making calls to upgrade will fail because admin hasn’t been set, yet.
  • An attacker can call initializer before you.

Currently, deploying the proxy contract and initializing the implementation contract by calling initializer is not an atomic operation. This means if you don’t pay attention, an attacker could call initializer before you and give themselves admin permissions.

We can make sure that we never forget by adding the initializer call to the Proxy’s constructor. This is how Argent implements their proxy. Add the following library_call to the Proxy’s constructor.

Let’s make sure everything is correct so far by running nile compile from the root directory of your project.

The Implementation Contract

The implementation contract, or logic contract, receives the redirected function calls from the proxy contract. Therefore, this is where the core of your application logic lives. The implementation contract should follow OpenZeppelin’s Extensibility pattern and import directly from their Proxy library.

The implementation contract should NOT be deployed like a regular contract. This is because the proxy is unable to call the implementation contract’s constructor and we want the contract’s storage to be in the context of the proxy. Here is a more detailed explanation.

Using a proxy does not make our implementation contract upgradable out of the box. Since all contracts are immutable, we still have to declare a new implementation contract. In order for our proxy to forward calls to this new contract, we need to update the proxy’s implementation_hash to the new class hash. Since the proxy forwards all calls using the __default__ function, it also forwards the call meant to upgrade its implementation hash. Thus, there needs to be a function upgrade on our upgradable implementation contract that will change the implementation hash stored in the proxy.

# After declaring new, upgraded contract
User ---- tx ---> Proxy ----------> Implementation_v0

Implementation_v1
# After the call to v0's `upgrade`
User ---- tx ---> Proxy Implementation_v0
|
------------> Implementation_v1

Rules to follow

There are a few rules to follow while writing your upgradable implementation contract. Start off by copying this upgradable contract into ./contracts/Implementation_v0.cairo. Let’s look at how Implementation_v0.cairo follows OpenZeppelin’s guidelines:

The implementation contract should:

  • import Proxy namespace
  • initialize the proxy immediately after contract deployment with Proxy.initializer. We made sure you don’t forget this by including it in the proxy constructor.
from openzeppelin.upgrades.library import Proxy

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. This is not built in!

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.

Check out other functions that the implementation contract can expose.

Once again let’s make sure everything is correct so far by running nile compile from the root directory of your project.

Proxy in Action

Let’s walk through an example to illustrate how to upgrade a contract.

Making two implementation contracts

  1. Remove the storage variable value_2 and the external function setValue2 from the contract Implementation_v0.cairo. Then, change getValue2 to return a constant.

2. Duplicate Implementation_v0.cairo and name the new file Implementation_v1.cairo. Now, replace return (3) with return (7).

Setting up the testing environment

  1. Open a new terminal window, navigate to the folder, activate the virtual environment and run the following to create a local devnet.
nile node

2. Create a python script proxy_script.py that we will use to deploy our proxy. Create the network_client and the account_client as follows.

Setting up the proxy

We are going to follow these general steps by OpenZeppelin.

  1. Declare an implementation contract class. Remember that we don’t deploy the implementation contract.
  2. Deploy the proxy contract with the implementation contract’s class hash set in the proxy’s constructor calldata.
  3. Initialize the implementation contract’s access controls by sending an initializer call to the proxy contract. This will redirect the call to the implementation contract class and behave like the implementation contract’s constructor.

Remember that we ensured that step 2 and 3 happen atomically when calling the Proxy’s constructor. Now, let’s see that in action.

  1. Declare Implementation_v0.cairo.

2. Next, let’s deploy the proxy making sure that we call initializer. You should pass in the address of the admin account for the proxy_admin input.

3. When calling the proxy contract, you still have to provide the ABI of the implementation class, which is where all the function are defined. To do this, we have to redefine the proxy contract.

Checking it works

Let’s check that everything works so far. Add the following main method to your python script. It sets up the account and deploys the proxy. Finally, it checks the admin is correct.

Run it to make sure everything works.

Upgrading the implementation contract

  1. Declare a new implementation contract (Implementation_v1.cairo).

2. Upgrade the implementation contract and optionally the ABI. Note that the upgrade call has to come from the admin, which is why we use the execute function.

3. Check that it worked.

Only the admin can upgrade

Let’s now check that no one else can upgrade the contract. We first need to active an account that’s not the admin.

  1. We need another “evil” account that wants upgrade our implementation contract. Add the following function to your script.

2. Check that evil can’t upgrade.

A word about unstructured storage

There is a known pitfall in Solidity that can cause storage collisions when using a Proxy with structured storage. Luckily, the StarkNet compiler creates pseudo-random storage addresses on its own. Therefore, OpenZeppelin’s proxy pattern inherently uses unstructured storage, which takes care of storage collisions for you.

Final Words

That’s it! Congratulations on making it all the way through. You just learned how to write upgradable contracts on StarkNet.

Please remember that the pattern might have changed since this guide came out and double check OpenZeppelin’s Contracts for Cairo. If you have any suggestions, please reach out. Let’s make our community better together.

References

A lot of the content in this guide is from OpenZeppelin’s Contracts for Cairo and their Ethereum Proxy Upgrade Pattern.

Code

You can find all of the code in this article in our guides repository.

About Empiric Network

Empiric Network is a next-generation oracle built leveraging the power of zk-rollups. Empiric is the leading oracle on StarkNet (live with 20+ price feeds), built in partnership with StarkWare. Empiric is powered by data sources that send their proprietary market data directly on-chain to Empiric’s smart contracts (including Alameda Research, Jane Street, Gemini, CMT Digital, Flow Traders and more). Because Empiric Network is entirely on-chain, protocols using Empiric finally have access to data that is as verifiable, transparent and secure as the smart contracts themselves. Since launching, Empiric has built a strong track record of robustness, high frequency updates and precision — their feeds have been updated more than 3.5M times, all of which is recorded by auditable and transparent on-chain events.

Empiric recently raised a $7M seed round led by Variant with participation by StarkWare and a stellar list of angels.

Empiric Labs is hiring! Check out our open positions and reach out to us via email, Telegram (@JonasNelle) or Twitter (DMs open).

Empiric Network Academy graphic

--

--