Proper Pattern for 'upgradeAndCall' with OZ upgrades

Hey everyone!

I may be missing something simple, but I'm having some trouble finding both documentation and methodology on the best way to implement a Transparent Proxy with upgradeAndCall using OZ upgrades.

Context: I implemented an ERC721 using upgrades.deployProxy, that roughly starts like:

contract Box is ERC721EnumerableUpgradeable {
    function initialize(
        uint16 royaltyBeeps_
    ) public initializer {
        // Ensures ERC721
        __ERC721_init("Box", "Box");

        // Ensures ERC2981 standard (royalties)
        _registerInterface(_INTERFACE_ID_ERC2981);

       royaltyBeeps = royaltyBeeps_;
   }
}

I now have a need to upgrade the contact and to add ownership capabilities:

contract BoxV2 is OwnableUpgradeable, Box { 

    function upgradeToV2(address owner_) external {
        require(owner() == address(0), "upgradeToV2::Owner already set");
        _transferOwnership(owner_);
    }
 }

As (from what I can tell) there is no simple mechanism to using OZ Upgrades with upgradeAndCall, I define the following upgrade script:

export const deployUpgradeToken = async (hre: HardhatRuntimeEnvironment) => {
  // Move the deployment back to the proper spot
  cpOzDeployment(hre, "Box");

  const boxV2 = await deployNonUpgradeableContract(hre, "BoxV2");
  console.log("BoxV2 Implementation:", boxV2.address);

  const data = (await hre.ethers.getContractFactory("BoxV2")).interface.encodeFunctionData(
    "upgradeToV2",
    [await boxV2.signer.getAddress()]
  );

  const { address: oldAddress } = getDeployment(hre.network.name, "Box");

  const proxy = await hre.upgrades.admin.getInstance();
  const c = await (await proxy.upgradeAndCall(oldAddress, boxV2.address, data)).wait();

  console.log("Upgraded 'Box' to 'BoxV2'");
  return c;
};

For context, cpOzDeployment simple places the original output of upgrades.deployProxy back in .openzeppelin, and getDeployment fetches the address of the original deployment for Box.

However, after running everything through, boxv2.name() and boxv2.symbol() return an empty string, while, strangely, royaltyBeeps (also defined in the original initialize function) is defined.

Any thoughts on potential problems with my current pattern? Is there a recommended way to do this?

Thanks!

upgradeAndCall is currently not an option with the Upgrades Plugins. But we're about to merge a PR to make it available. If you could wait one day this will be easier for you.

Ah, I'll know for next time. I used a modified version of the above, which was sketchy, but worked :+1: .

However, any chance you can spot what the bug would have caused that? Is there something about including OwnableUpgradeable that would have overwritten something in storage?

When you do boxv2.name(), you're probably calling the name function on the implementation contract. You should call the function on the proxy, that's where the value is actually initialized. Not sure why royaltyBeeps would be defined though