I am trying to create a new token contract using the ERC20 Upgradeable contracts provided from Openzeppelin (OZ). I created the token contract and then made some basic unit tests. However, in one of the tests, I expect to see "TransparentUpgradeableProxy: admin cannot fallback to proxy target", when calling an implementation method from the Proxy admin. In one of my projects it works fine, but in another made from scratch - it does not. The error that I receive is "Error: missing revert data in call exception; Transaction reverted without a reason string..."

Here is the code for the one that is failing:

// the contract ---
contract TokenName is ERC20PresetMinterPauserUpgradeable {
    /**
     * @dev Used to prevent implementation manipulation
     */
    constructor() initializer {}

    /**
     * @dev Grants `DEFAULT_ADMIN_ROLE`, `MINTER_ROLE`, `PAUSER_ROLE` to the owner
     * specified in the owner param.
     * @param owner is the owner of the contract after initialization
     * See {ERC20-constructor}.
     */
    function initialize(string memory name, string memory symbol, address owner) public initializer {
        require(owner != address(0x0), "New owner cannot be 0");
        __ERC20PresetMinterPauser_init(name, symbol);
        __ERC20_init(name, symbol);
        _setupRole(DEFAULT_ADMIN_ROLE, owner);
        _setupRole(MINTER_ROLE, owner);
        _setupRole(PAUSER_ROLE, owner);
        revokeRole(PAUSER_ROLE, _msgSender());
        revokeRole(MINTER_ROLE, _msgSender());
        revokeRole(DEFAULT_ADMIN_ROLE, _msgSender());
     }


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

// token unit tests ---
describe.only("Upgradeable token tests", function () {
  let accounts;
  let deployer;
  let owner;

  let tokenInstance;

  let tokenName = "Token Name";
  let tokenSymbol = "symbol";
  const tokenAmount = '10000000000000000000';

  before(async function() {
    accounts = await ethers.getSigners();
    deployer = accounts[0];
    owner = accounts[1];
    user = accounts[2];
  })

  it("should throw error when calling the implementation contract with the proxy admin", async function() {
    const tokenContract = await ethers.getContractFactory(tokenSymbol);
    tokenInstance = await upgrades.deployProxy(tokenContract, [tokenName, tokenSymbol, owner.address], { initializer: 'initialize(string,string,address)', unsafeAllow: ['constructor'] });
    await tokenInstance.deployed();

    console.log("default admin addr: ", await upgrades.erc1967.getAdminAddress(tokenInstance.address));
    console.log("token instance addr: ", tokenInstance.address);
    await upgrades.admin.changeProxyAdmin(tokenInstance.address, owner.address);

    console.log("changed admin addr: ", await upgrades.erc1967.getAdminAddress(tokenInstance.address));
    expect(await upgrades.erc1967.getAdminAddress(tokenInstance.address)).to.equal(owner.address);

    //console.log("tokenInstance", tokenInstance);
    console.log("owner addr: ", owner.address);
    console.log("deployer addr: ", deployer.address);

    console.log("admin changed ---");
    
    await expect(tokenInstance.connect(owner).name()).to.be.revertedWith('TransparentUpgradeableProxy: admin cannot fallback to proxy target');
  });
})

There is no much need to add the code of the other project, because it is exactly the same, but I will do in order to have it as comparison.

// token contract ---
contract MarsCoin is ERC20PresetMinterPauserUpgradeable {
    uint counter;

    function increment() public {
        counter += 1;
    }

    function getCounter() public view returns (uint) {
        return counter;
    }

    /**
     * @dev Used to prevent implementation manipulation
     */
    constructor() initializer {}

    /**
     * @dev Grants `DEFAULT_ADMIN_ROLE`, `MINTER_ROLE`, `PAUSER_ROLE` to the owner
     * specified in the owner param.
     * @param owner is the owner of the contract after initialization
     * See {ERC20-constructor}.
     */
    function initialize(string memory name, string memory symbol, address owner) public initializer {
        require(owner != address(0x0), "New owner cannot be 0");
        counter = 0;
        __ERC20PresetMinterPauser_init(name, symbol);
        __ERC20_init(name, symbol);
        _setupRole(DEFAULT_ADMIN_ROLE, owner);
        _setupRole(MINTER_ROLE, owner);
        _setupRole(PAUSER_ROLE, owner);
        revokeRole(PAUSER_ROLE, _msgSender());
        revokeRole(MINTER_ROLE, _msgSender());
        revokeRole(DEFAULT_ADMIN_ROLE, _msgSender());
     }


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

// token unit tests ---
const { expect } = require("chai");
const { ethers, upgrades } = require("hardhat");

describe.only("Mars Coin tests", function () {
  let accounts;
  let deployer;
  let owner;

  let tokenInstance;

  let tokenName = "Mars Coin";
  let tokenSymbol = "MARS";
  const tokenAmount = '10000000000000000000';

  before(async function() {
    accounts = await ethers.getSigners();
    deployer = accounts[0];
    owner = accounts[1];
    user = accounts[2];
  })

  it("Should deploy MARS token with correct owner set", async function () {
    const MarsCoin = await ethers.getContractFactory("MarsCoin");
    tokenInstance = await upgrades.deployProxy(MarsCoin, [tokenName, tokenSymbol, owner.address], { initializer: 'initialize(string,string,address)', unsafeAllow: ['constructor'] });
    await tokenInstance.deployed();

    const adminRole = await tokenInstance.DEFAULT_ADMIN_ROLE();
    const minterRole = await tokenInstance.MINTER_ROLE();
    const pauserRole = await tokenInstance.PAUSER_ROLE();

    expect(await tokenInstance.name()).to.equal(tokenName);
    expect(await tokenInstance.symbol()).to.equal(tokenSymbol);

    expect(await tokenInstance.hasRole(adminRole, deployer.address)).to.equal(false);
    expect(await tokenInstance.hasRole(minterRole, deployer.address)).to.equal(false);
    expect(await tokenInstance.hasRole(pauserRole, deployer.address)).to.equal(false);

    expect(await tokenInstance.hasRole(adminRole, owner.address)).to.equal(true);
    expect(await tokenInstance.hasRole(minterRole, owner.address)).to.equal(true);
    expect(await tokenInstance.hasRole(pauserRole, owner.address)).to.equal(true);
  });

  it("Should mint tokens to user account", async function() {
    const tokenInstanceWithOwner = tokenInstance.connect(owner);
    await tokenInstanceWithOwner.mint(user.address, tokenAmount);
    const accountBalance = (await tokenInstance.balanceOf(user.address)).toString();
    expect(accountBalance).to.equal(tokenAmount)
  })

  it("Should try to call contract implementation contract with deployer", async function () {
    const tokenInstanceWithDeployer = tokenInstance.connect(deployer);
    expect(await tokenInstanceWithDeployer.name()).to.equal(tokenName)
  })

  it("Should change the MARS token proxy admin correctly", async function() {
    await upgrades.admin.changeProxyAdmin(tokenInstance.address, owner.address);
    expect(await upgrades.erc1967.getAdminAddress(tokenInstance.address)).to.equal(owner.address);
  })
  
  it.only("Should throw on trying to call contract implementation from new proxy admin owner", async function () {
    const MarsCoin = await ethers.getContractFactory("MarsCoin");
    tokenInstance = await upgrades.deployProxy(MarsCoin, [tokenName, tokenSymbol, owner.address], { initializer: 'initialize(string,string,address)', unsafeAllow: ['constructor'] });
    await tokenInstance.deployed();
    
    await upgrades.admin.changeProxyAdmin(tokenInstance.address, owner.address);
    expect(await upgrades.erc1967.getAdminAddress(tokenInstance.address)).to.equal(owner.address);

    await expect(tokenInstance.connect(owner).name()).to.be.revertedWith('TransparentUpgradeableProxy: admin cannot fallback to proxy target');
  })
});

Keep in mind the .only test that I am running, thus, the rest can be skipped, but I just paste it to have the exact code.

1

There are 1 answers

0
VitoCK On

Okay, I succeeded to fix this by just creating the project from scratch.

npm init -> then follow the basic instructions
npm install hardhat
npx hardhat init -> create an advanced sample project and follow the instructions
npm install - all the necessary packages that are missing (some of the existing ones were re-installed with older version, because it seems the problem comes from a newer version library)

Very interesting stuff.. previously I just tried to delete all the folders like .cache, node_module, the built folder, package-lock.json and then to hit the npm install, but without success. I could not understand what was exactly the issue.

However, these are the packages that I am using:

   "devDependencies": {
    "@nomiclabs/hardhat-ethers": "^2.0.2",
    "@nomiclabs/hardhat-etherscan": "^3.0.3",
    "@nomiclabs/hardhat-waffle": "^2.0.1",
    "chai": "^4.3.4",
    "dotenv": "^10.0.0",
    "eslint": "^7.32.0",
    "eslint-config-prettier": "^8.5.0",
    "eslint-config-standard": "^16.0.3",
    "eslint-plugin-import": "^2.26.0",
    "eslint-plugin-node": "^11.1.0",
    "eslint-plugin-prettier": "^3.4.1",
    "eslint-plugin-promise": "^5.2.0",
    "ethereum-waffle": "^3.4.0",
    "ethers": "^5.4.5",
    "hardhat": "^2.6.1",
    "hardhat-gas-reporter": "^1.0.8",
    "prettier": "^2.6.2",
    "prettier-plugin-solidity": "^1.0.0-beta.13",
    "solhint": "^3.3.7",
    "solidity-coverage": "^0.7.20"
  },
  "dependencies": {
    "@openzeppelin/contracts": "^4.3.0",
    "@openzeppelin/contracts-upgradeable": "^4.3.2",
    "@openzeppelin/hardhat-upgrades": "^1.10.0"
  }