Error when adding ERC20VotesUpgradeable

I have an upgradeable ERC20 token deployed that I want to add Governance. That said, according to OZ documentation, I should use ERC20VotesUpgradeable. However, when I try to upgrade the token I get to following error:

Error: New storage layout is incompatible

@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol:41: Layout changed for `__gap` (uint256[50] -> uint256[50])
  - Slot changed from 1 to 51
  > Set __gap array to size 0

ERC20Upgradeable: Deleted `__gap`
  > Keep the variable even if unused

It is kinda unexpected. I don't have any variables in the main contract that would clearly issue a storage conflict. Also, the ERC165Upgradeable mentioned in the error is just present in the AccessControlUpgradeable that was already in the version 1.

:1234: Code to reproduce

Version 1

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

import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";

import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

contract TokenV1 is
    ERC20Upgradeable,
    AccessControlUpgradeable,
    PausableUpgradeable,
    UUPSUpgradeable
{
    bytes32 public constant DEPOSITOR_ROLE = keccak256("DEPOSITOR_ROLE");
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");

    function initialize() public initializer {
        __ERC20_init("Token", "TOKEN");
        __AccessControl_init();
        __Pausable_init();
        __UUPSUpgradeable_init();

        _setupRole(DEFAULT_ADMIN_ROLE, _msgSender());
    }

    function pause() external onlyRole(PAUSER_ROLE) {
        _pause();
    }

    function unpause() external onlyRole(PAUSER_ROLE) {
        _unpause();
    }

    /// @notice called when token is deposited on root chain
    /// @dev Should be callable only by ChildChainManager
    /// Should handle deposit by minting the required amount for user
    /// @param user user address for whom deposit is being done
    /// @param depositData abi encoded amount
    function deposit(address user, bytes calldata depositData)
        external
        onlyRole(DEPOSITOR_ROLE)
    {
        uint256 amount = abi.decode(depositData, (uint256));
        _mint(user, amount);
    }

    /// @notice called when user wants to withdraw tokens back to root chain
    /// @dev Should burn user's tokens. This transaction will be verified when exiting on root chain
    /// @param amount amount of tokens to withdraw
    function withdraw(uint256 amount) external {
        _burn(_msgSender(), amount);
    }

    function mint(address user, uint256 amount) external onlyRole(MINTER_ROLE) {
        _mint(user, amount);
    }

    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal override whenNotPaused {
        super._beforeTokenTransfer(from, to, amount);
    }

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

Version 2

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

import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";

import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20VotesUpgradeable.sol";


contract TokenV2 is
    ERC20Upgradeable,
    AccessControlUpgradeable,
    PausableUpgradeable,
    UUPSUpgradeable,
    ERC20VotesUpgradeable
{
    bytes32 public constant DEPOSITOR_ROLE = keccak256("DEPOSITOR_ROLE");
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");

    function pause() external onlyRole(PAUSER_ROLE) {
        _pause();
    }

    function unpause() external onlyRole(PAUSER_ROLE) {
        _unpause();
    }

    function deposit(address user, bytes calldata depositData)
        external
        onlyRole(DEPOSITOR_ROLE)
    {
        uint256 amount = abi.decode(depositData, (uint256));
        _mint(user, amount);
    }

    function withdraw(uint256 amount) external {
        _burn(_msgSender(), amount);
    }

    function mint(address user, uint256 amount) external onlyRole(MINTER_ROLE) {
        _mint(user, amount);
    }

    function _mint(address to, uint256 amount)
        internal
        override(ERC20Upgradeable, ERC20VotesUpgradeable) 
    {
        super._mint(to, amount);
    }

    function _burn(address account, uint256 amount)
        internal
        override(ERC20Upgradeable, ERC20VotesUpgradeable) 
    {
        super._burn(account, amount);
    }

    function _afterTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal override(ERC20Upgradeable, ERC20VotesUpgradeable)  {
        super._afterTokenTransfer(from, to, amount);
    }

    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal override whenNotPaused {
        super._beforeTokenTransfer(from, to, amount);
    }

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

Deployment script

import { ethers, upgrades } from "hardhat";

async function main() {
  const [owner] = await ethers.getSigners();

  const TokenV1 = await ethers.getContractFactory("TokenV1");
  console.log("Deploying proxy and implementation");
  const proxy = await upgrades.deployProxy(TokenV1, {
    initializer: "initialize",
    kind: "uups",
  });
  console.log("First version deployed to: ", proxy.address);

  await proxy.grantRole(await proxy.UPGRADER_ROLE(), owner.address);

  const TokenV2 = await ethers.getContractFactory("TokenV2");
  const upgraded = await upgrades.upgradeProxy(proxy.address, TokenV2);
  console.log("New version deployed to: ", upgraded.address);
  await upgraded.deployed();
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

:computer: Environment

Hardhat: ^2.11.2
@openzeppelin/hardhat-upgrades: ^1.21.0
@openzeppelin/contracts-upgradeable: ^4.8.0-rc.1

This is a consequence of the way that Solidity linearizes the multiple inheritance relationships to construct the storage layout. Sometimes adding something at the end can have an impact on the things at the beginning of the list.

In your case, this is what the linearized contracts look like:

image

You can see that ERC20 and ERC165 are swapped.

Unfortunately we don't have automated tools right now to help you figure out how to solve a situation like this. With some understanding of how the linearization works you should be able to figure out a way to line things up properly.

In this particular case, I think you should be able to fix things if you explicitly add ERC165Upgradeable to your inheritance list after ERC20Upgradeable. You should think of this as kind of "forcing" them to be emitted in that order.

Your TokenV2 would look like this:

contract TokenV2 is
    ERC20Upgradeable,
    ERC165Upgradeable,
    AccessControlUpgradeable,
    PausableUpgradeable,
    UUPSUpgradeable,
    ERC20VotesUpgradeable
{

I haven't tested the upgrade but I believe this will work.

2 Likes

I've created a Hardhat plugin to print the linearization of a contract.

2 Likes