Understanding upgradeable smart contract

Hello Everyone,
First of all, Thanks to @frangio for creating this amazing video about upgradeable smart-contracts. If anyone is looking for a best resource to know about upgreadable smart-contracts then please go checkout the video.

Back to question now, so I'm trying below code to understand the upgradeable SC. I was able to test contract Box successfully but when I'm trying to test contract BoxV2 then it is giving me the error.

My questions are : -

  1. Is it possible to add a new state variable uint d and assigned a value to it just like I did in contract BoxV2? If not then what's wrong here?

  2. In the error it is mentioned that Move the assignment to the initializer. How am I suppose to do it because function initialize only execute once and I can not overide it in contract BoxV2?
    OR
    Does it mean that I can't inherit contract Box instead I have to put the entire code of contract Box in Boxv2.sol file with additional function(functions I want to add) and also initialize the variable d inside function initialize

Error

 Box (proxy)
 value of result is:  13
    ✓ This is a negative case for function initialize to check if func initialize works then it wil return a value which is not equal to 3
 value of result is:  13
    ✓ Func initialize works and it wil return a value which is equal to 13

  BoxV2 (upgrade-proxy)
    1) "before all" hook for "returns 35 as a result"


  2 passing (1s)
  1 failing

  1) BoxV2 (upgrade-proxy)
       "before all" hook for "returns 35 as a result":
     Error: Contract `BoxV2` is not upgrade safe

contracts/Box.sol:43: Variable `d` is assigned an initial value
    Move the assignment to the initializer
    https://zpl.in/upgrades/error-004
      at Object.assertUpgradeSafe (node_modules/@openzeppelin/upgrades-core/src/validate/query.ts:17:11)
      at Object.deployImpl (node_modules/@openzeppelin/hardhat-upgrades/src/utils/deploy-impl.ts:31:3)
      at Proxy.upgradeProxy (node_modules/@openzeppelin/hardhat-upgrades/src/upgrade-proxy.ts:36:22)
      at Context.<anonymous> (test/boxv2-proxy-test.js:17:13)

:1234: Code to reproduce

// SPDX-License-Identifier: Unlicenced

pragma solidity ^0.8.4;

import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract Box is Initializable, PausableUpgradeable, OwnableUpgradeable {
    
    uint c;
    
    function initialize() initializer public {
        __Pausable_init();
        __Ownable_init();
        c = 10;
    }
    
    function pause() public onlyOwner {
        _pause();
    }

    function unpause() public onlyOwner {
        _unpause();
    }

    function A(uint a, uint b) public view whenNotPaused returns (uint) {
        
        return a + b + c;
    }
}

// Box version v2
contract BoxV2 is Box {

    uint d=20;

    function B(uint e) public view whenNotPaused returns(uint)  {

        uint x = A(1,3) + d + e;
        return x;
    }
}

code for testing Box contract

// test/Box.proxy.js
// Load dependencies
const { expect } = require('chai');
 
let Box;
let box;
 
// Start test block
describe('Box (proxy)', function () {
  
  before(async function () {
    Box = await ethers.getContractFactory("Box");
    // initialize only run once
    box = await upgrades.deployProxy(Box, [], {initializer: 'initialize'});
    //console.log("box is: ", box)
  });
 
  // Test case
  it('This is a negative case for function initialize to check if func initialize works then it wil return a value which is not equal to 3', async function () {
    // Test if the returned value is the same one
    // Note that we need to use strings to compare the 256 bit integers
    const result = await box.A(1,2);
    //await result.wait();
    console.log(" value of result is: ", result.toString());
    expect(result.toString()).not.equal('3');
  });

  it('Func initialize works and it wil return a value which is equal to 13', async function () {
    // Test if the returned value is the same one
    // Note that we need to use strings to compare the 256 bit integers
    const result = await box.A(1,2);
    
    console.log(" value of result is: ", result.toString());
    expect(result.toString()).to.equal('13');
  });
});

code for testing contract BoxV2

// test/BoxV2.proxy.js
// Load dependencies
const { expect } = require('chai');
 
let Box;
let BoxV2;
let box;
let boxV2;
 
// Start test block
describe('BoxV2 (upgrade-proxy)', function () {
  before(async function () {
    Box = await ethers.getContractFactory("Box");
    BoxV2 = await ethers.getContractFactory("BoxV2");

    box = await upgrades.deployProxy(Box, [], {initializer: 'initialize'});
    boxV2 = await upgrades.upgradeProxy(box.address, BoxV2);
  });
 
  // Test case
  it('returns 35 as a result when successful', async function () {
    const result = await boxV2.B(1);
    console.log("result is: ", result.toString());
    expect((result).toString()).to.equal('35');
  });
});

:computer: Environment

Hardhat

Please help.

Glad you enjoyed the presentation!

It's possible to add a new state variable, but not possible to set an initial value (as explained in the link included with the error!)

You can move the initial value to the initializer. But you're right that this phrasing is confusing when talking about an upgrade, such as BoxV2. When upgrading, the initializer is not going to run again, so you need to move the assignment into a function that you can then use to set the initial values of the new version. Something like:

contract BoxV2 is Box {
    uint d;
    bool v2Initialized;
    function initializeV2() {
        require(!v2Initialized);
        d = 20;
        v2Initialized = true;
    }
1 Like