State in upgradeable contracts

I was asked where state is stored in upgradeable contracts.

The OpenZeppelin SDK Upgrades Pattern documentation explains:

https://docs.openzeppelin.com/sdk/2.5/pattern
A very important thing to note is that the code makes use of the EVM’s delegatecall opcode which executes the callee’s code in the context of the caller’s state. That is, the logic contract controls the proxy’s state and the logic contract’s state is meaningless. Thus, the proxy doesn’t only forward transactions to and from the logic contract, but also represents the pair’s state. The state is in the proxy and the logic is in the particular implementation that the proxy points to.

To see the state being stored in the Proxy contract for myself I deployed a simple upgradeable contract and read the contract storage of the proxy contract.

Setup

Assumes openzeppelin/cli and ganache-cli already installed.

Create an OpenZeppelin SDK project

mkdir storage
cd storage
npm init -y
oz init

Run ganache-cli -d to start a local blockchain (if you haven't got one running already).

Counter.sol

Create a simple contract in the OpenZeppelin SDK project (in the contracts directory)

pragma solidity ^0.5.0;

contract Counter {
  uint256 public value;

  function increase() public {
    value++;
  }
}

Create

Create the contract using OpenZeppelin SDK.

$ oz create
✓ Compiled contracts with solc 0.5.12 (commit.7709ece9)
? Pick a contract to instantiate Counter
? Pick a network development
✓ Added contract Counter
✓ Contract Counter deployed
All contracts have been deployed
? Do you want to call a function on the instance after creating it? No
✓ Setting everything up to create contract instances
✓ Instance created at 0xCfEB869F69431e42cdB54A4F4f105C19C080A601
0xCfEB869F69431e42cdB54A4F4f105C19C080A601

index.js

The following script uses OpenZeppelin Network.js.
Install using npm i @openzeppelin/network
Paste the following into a file called index.js

The contents at a storage location are obtained using getStorageAt

The Proxy contract address was obtained when we created it above.
value for the Counter is at index 0 of the Proxy contract
The logic contract address is at index 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc which is bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) of the Proxy contract (as per the OpenZeppelin SDK Upgrades Pattern documentation)

const start = async function () {
    const networkjs = require('@openzeppelin/network');
    const web3Context = await networkjs.fromConnection('http://127.0.0.1:8545');

    console.log('Counter value: ' + await web3Context.lib.eth.getStorageAt("0xCfEB869F69431e42cdB54A4F4f105C19C080A601", 0));
    
    console.log('Logic contract address: ' + await web3Context.lib.eth.getStorageAt("0xCfEB869F69431e42cdB54A4F4f105C19C080A601", "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"));
    return;
}

start();

Check storage

You may need to stop the script (e.g. Ctrl+C).

$ node index.js
Counter value: 0x0
Logic contract address: 0xe78a0f7e598cc8b0bb87894b0f60dd2a88d6a8ab

Increment counter

$ oz send-tx
? Pick a network development
? Pick an instance Counter at 0xCfEB869F69431e42cdB54A4F4f105C19C080A601
? Select which function increase()
✓ Transaction successful. Transaction hash: 0x1993a8b6774ce05f2f2da0c5fc1174de46a3630e642fac81cf71bec28864e451

Check storage (again)

Notice that the Counter value has increased.

$ node index.js
Counter value: 0x01
Logic contract address: 0xe78a0f7e598cc8b0bb87894b0f60dd2a88d6a8ab
1 Like

Hi Andrew,

Thank you for your clear explanation. The examples make it really clear the data is stored in the proxy and not in the logic contract (which I was looking for).
The only question I still have is: Where (in Solidity code) does the storage of the proxy contract get allocated? And when an upgrade happens, on which place in the contract code does a new variable get added to the proxy contract?

Thanks a lot!

Christiaan

1 Like

Hi @Chrissie,

The implementation slot on the Proxy contract is defined in BaseUpgradeabilityProxy:

Upgrades set a new implementation in the proxy contract:

The new implementation is set at the implementation slot in the proxy contract:

The Proxy Forwarding documentation explains how it works, with the actual code shown in the Proxy contract below:

The Storage collisions between implementation versions documentation shows how new versions of contracts should add variables to avoid storage collisions.

Let me know if you need more information.

Once again: Thank you for your explanation.

If I understand it correctly: The logic contract is using the state of the proxy/data contract directly (due to the nature of delegatecall). This is why the variable value retention when upgrading is happening: the logic contract variables are still mapped to the same location in the proxy contract.

As for any newly declared variables: the Solidity compiler compiles a variable declaration to the location of the variable, and because delegatecall is used, the new variable is added/edited in the state of the proxy contract.

Right? :slight_smile:

1 Like

Hi @Chrissie,

That is correct. The proxy contract contains the state and the logic contract contains the logic.

1 Like