ERC20 Upgradeable Contract Issues

I apologize if this has been asked before, but I can't find what I'm looking for.

So, I want to build an ERC20 token that is Upgradeable. I started with the basic build the OZ Wizard creates:

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

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

contract MyToken is Initializable, ERC20Upgradeable, ERC20SnapshotUpgradeable, OwnableUpgradeable, UUPSUpgradeable {
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

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

        _mint(msg.sender, 10000 * 10 ** decimals());
    }

    function snapshot() public onlyOwner {
        _snapshot();
    }

    function _authorizeUpgrade(address newImplementation)
        internal
        onlyOwner
        override
    {}

    // The following functions are overrides required by Solidity.

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

I then wanted to modify the transfer function to add in some features like burning and auto liquidity, but when I add the transfer function from the original ERC20Upgradeable contract it throw the "Undeclared Identifier" error on all of the "_balances". I saw that it has to do with the mapping (which I don't understand because it should be pulling it from the ERC20Upgradeable import), so I throw it in there and the errors go away.

Now, when I compile this (on RemixIDE web) and deploy it to the London VM with a Proxy, it deploys successfully; however, when I try to transfer some of the tokens it throws:

The transaction has been reverted to the initial state.
Reason provided by the contract: "ERC20: transfer amount exceeds balance".
Debug the transaction to get more information.

I imagine this is due to the fact that it is trying to pull the balance from the mapping, but there wouldn't be anything there. This is just the start of my issues I've been running in to, but since I decided to try and build this contract from scratch this would be the starting place for my problems. I need help lol.

This is the deployed contract and as you can see I haven't even made any real changes:

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

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

contract MyToken is Initializable, ERC20Upgradeable, ERC20SnapshotUpgradeable, OwnableUpgradeable, UUPSUpgradeable {
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

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

        _mint(msg.sender, 10000 * 10 ** decimals());
    }

    function snapshot() public onlyOwner {
        _snapshot();
    }

    function _authorizeUpgrade(address newImplementation)
        internal
        onlyOwner
        override
    {}

    // The following functions are overrides required by Solidity.

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

    /////////
    mapping(address => uint256) private _balances;

    function transfer(address to, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender();
        _transfer(owner, to, amount);
        return true;
    }

    function _transfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual override {
        require(from != address(0), "ERC20: transfer from the zero address");
        require(to != address(0), "ERC20: transfer to the zero address");

        _beforeTokenTransfer(from, to, amount);

        uint256 fromBalance = _balances[from];
        require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
        unchecked {
            _balances[from] = fromBalance - amount;
            // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
            // decrementing then incrementing.
            _balances[to] += amount;
        }

        emit Transfer(from, to, amount);

        _afterTokenTransfer(from, to, amount);
    }
}

Of course I post this and then THINK I figure out where I went wrong. I would still love some feedback, but may have found the reasoning.

Hi, welcome to the community! :wave:

I think the type of the variable _balances in the ERC20Upgradeable is private, so you can not access to it in your contract MyToken.

Yeah, you set the same variable _balances in your contract MyToken, and actually, your action does effect on the variable _balances in the ERC20Upgradeable.

I'm not entirely sure, but it seems like my writing of a new transfer and _transfer function into the contract seemed to negate the importing of the ERC20Upgradeable contract, at least when it comes to those functions that pulls from the _balances variable and mapping causing it to throw the errors. My (possibly temporary) fix is to not import the ERC20Upgradeable contract and just have it in my contract in its entirety. This seemed to fix the issue, but I was so elated that it worked I decided to call it a day after spending all weekend on the issue.

If there are more learned minds that can explain if this would be the correct (or, one of many) solution/workaround or if there is something else I should be doing.. I would love to learn. Solidity is only my third language :stuck_out_tongue:

I don't know where you copied the code from originally, but _balances is a private variable in OpenZeppelin Contracts so you can't access it in inherited contracts.

Yes, of course! You introduced a new _balances mapping completely separate from the actual balance accounting of the parent ERC20 contract you are extending. When you extend via inheritance you shouldn't simply override functions and replace them, you can override to add minimal logic and then include a "super" call to execute the original code.

1 Like