How I understand upgrade wrong

Hi experts,

I've playing with upgrade suite. Met some unexpected behavior.

First let me reproduce my actions

1. Deploy v1

Contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

contract MyToken is
    Initializable,
    ERC20Upgradeable,
    AccessControlUpgradeable,
    UUPSUpgradeable
{
    bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() initializer {}

    function initialize() public initializer {
        __ERC20_init("MyToken", "MTK");
        __AccessControl_init();
        __UUPSUpgradeable_init();

        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(UPGRADER_ROLE, msg.sender);
    }

    function _authorizeUpgrade(address newImplementation)
        internal
        override
        onlyRole(UPGRADER_ROLE)
    {}
}

JS:

async function deploy() {
  const HCETokenV1 = await ethers.getContractFactory("MyToken");
  const hce = await upgrades.deployProxy(HCETokenV1, []);
  await hce.deployed();
  console.log("Box deployed to:", hce.address);
}

Call:

v1.symbol() // ==> MTK

2. Upgrade to v2

Contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

contract MyTokenV2 is
    Initializable,
    ERC20Upgradeable,
    AccessControlUpgradeable,
    UUPSUpgradeable
{
    bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() initializer {}

    function initialize() public initializer {
        __ERC20_init("MyToken V2", "MTK V2"); // Update the name & symbol
        __AccessControl_init();
        __UUPSUpgradeable_init();

        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(UPGRADER_ROLE, msg.sender);
    }
    
    // new function
    function hello() public pure returns (string memory) {
        return "hello";
    }

    function _authorizeUpgrade(address newImplementation)
        internal
        override
        onlyRole(UPGRADER_ROLE)
    {}
}

JS:

async function upgrade() {
  const ADDR = "0x0165878A594ca255338adfa4d48449f69242Eb8F";
  const HCETokenV2Test = await ethers.getContractFactory("MyTokenV2");
  const upgraded = await upgrades.upgradeProxy(ADDR, HCETokenV2Test);

  console.log("Upgrade finished:");
}

Call:

async function testD() {
  const ADDR = "0x0165878A594ca255338adfa4d48449f69242Eb8F";

  const HCETokenV2Test = await ethers.getContractFactory("MyTokenV2");
  const upgraded = HCETokenV2Test.attach(ADDR);
  console.log(await upgraded.symbol()); // ==> Still `MTK`, not `MTK V2`
  console.log(await upgraded.hello()); // ==> works
}

Question

  • hello() working should mean the underlying impl is upgraded successfully. right?
  • But how symbol() still return v1's setup? shouldn't all call delegated to v2?

Thanks!

Right.

  • But how symbol() still return v1's setup? shouldn't all call delegated to v2?

The initializer for v2 is not called during the upgrade. The token name is part of the proxy's state (since it is stored in storage), and since it was initialized in v1, it does not change. However, upgradeProxy has a call option for calling a function during the upgrade, which you can use to call a custom function to migrate the state.

Also, the release candidate for Contracts 4.6 has support for reinitializers (which can be the function that you call with the call option during an upgrade).

Thanks for the answer! Very clear.

The initializer for v2 is not called during the upgrade.

Just trying to understand more on the upgrade process. When proxy upgrade to a new impl, it just updates the impl addr without doing anything else, since all impl contract's constructors are empty by design. Am I correct.

Yes, that's correct. But with the call option as mentioned above, you can upgrade AND call an arbitrary function in the same transaction.

1 Like