How to initialize a new variable from a new contract when upgrading original contract (UUPSUpgradeable)?

I have a UUPS Upgradeable ERC20 contract deployed to a testnet using Truffle. Let's call it ContractA.sol, which looks like the following:

contractA.sol:

// pragma, imports and docstrings

contract ContractA is Initializable, ERC20Upgradeable, ERC20BurnableUpgradeable, AnotherContractA, UUPSUpgradeable {

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

    function initialize(
        string memory _name,
        string memory _symbol,
        address _somevariable1
    ) public initializer {
        __ERC20_init(_name, _symbol);
        __ERC20Burnable_init();
        __UUPSUpgradeable_init();
        __AnotherContractA_init(_somevariable1);
    }

    function _authorizeUpgrade(address newImplementation) onlyOwner internal override {}
}

Now I want to add new features to this contract by inheriting AnotherContractB, which includes _somevariable2 that also needs to be initialized like _somevariable1. So I create ContractB that will inherit ContractA and AnotherContractB like so:

contractB.sol:

// pragma, imports and docstrings

contract ContractB is ContractA, AnotherContractB {

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

    function initialize(
        address _somevariable2
    ) public initializer {
        __AnotherContractB_init(_somevariable2);
    }
}

However, I keep getting the error The contract code couldn't be stored, please check your gas limit. when migrating. I think it's because I have two initializers (one in ContractA and another in ContractB)? Basically, how do I initialize a new variable from a new contract when upgrading?

Hi @lalalei, I'm not sure if your error is related, but I noticed a few issues with your contract:

  1. It is using onlyOwner so it should have import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";, extend from OwnableUpgradable, and call __Ownable_init(); in its initializer
  2. contractB extends from contractA so they cannot both have constructor() initializer {} because the initializer modifier can only be called once.

Regarding your question, an initializer function cannot be called during upgrade because the contract instance (the proxy itself) was already initialized when it was first deployed. Instead, you can call an arbitrary function during upgrade (but that function cannot be an initializer).

For example, you can upgrade and call a function at the same time like this:

const upgraded = await upgradeProxy(contractA.address, ContractB, {
   call: { fn: 'yourUpgradeFunction', args: ['argument for your upgrade function'] },
});
1 Like

Thanks so much for your answer, @ericglau !

Regarding point 1, yes, I actually imported OwnableUpgradeable. I removed it along with the other inherited contracts in my snippet for conciseness and I forgot that the contract uses onlyOwner. But thanks for pointing that out! Will edit it.

Regarding point 2 and your answer, I changed ContractB.sol to just:

ContractB.sol

// pragma, imports and docstrings

contract ContractB is ContractA, AnotherContractB {
}

While my deployer looks like this now:

let somevariable2 = 'foo';
const instance = await upgradeProxy(contractA.address, 
        ContractB, {
        deployer, call: { fn: '__AnotherContractB_init', args: [somevariable2] },
});

However, when migrating, I get this error:

TypeError: contract.methods[call.fn] is not a function

Edit: Oops. My bad. __AnotherContractB_init has the internal modifier, which should be made external/public. Andinitializer modifier, which should be removed. This solves the issue.

Thanks very much @ericglau ! You've been very helpful.