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.
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;
});
Environment
Hardhat: ^2.11.2
@openzeppelin/hardhat-upgrades: ^1.21.0
@openzeppelin/contracts-upgradeable: ^4.8.0-rc.1