Error: No ProxyAdmin was found in the network manifest

I have an ERC20 upgradeable contract (see MyToken.sol below) that I am deploying to Rinkeby (using 2_deploy_token.js and 3_transfer_ownership.js for migration files). I'm trying to transfer the proxy contract ownership to my Gnosis Safe so that only it can upgrade the contract next time. However, I get an error:

Error: No ProxyAdmin was found in the network manifest

How do I address this error?

I haven't seen any post addressing this exact error, only Error: Proxy admin is not the one registered in the network manifest (like this post), which is different.

:1234: Code to reproduce

MyToken.sol:

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

import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.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, ERC20BurnableUpgradeable, PausableUpgradeable, OwnableUpgradeable, UUPSUpgradeable {
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() initializer {}

    function initialize(string memory _name, string memory _symbol) initializer public {
        __ERC20_init(_name, _symbol);
        __ERC20Burnable_init();
        __Pausable_init();
        __Ownable_init();
        __UUPSUpgradeable_init();
    }

    // other functions
}

2_deploy_token.js:

const { deployProxy } = require('@openzeppelin/truffle-upgrades');
const MyToken = artifacts.require('MyToken.sol');

module.exports = async function (deployer) {
    const name = 'MyToken';
    const symbol = 'MTN';
    const instance = await deployProxy(
        MyToken, 
        [name, symbol], 
        { deployer }
    );
};

3_transfer_ownership.js:

const { admin } = require('@openzeppelin/truffle-upgrades');
 
module.exports = async function (deployer, network) {
  // Use address of your Gnosis Safe
  const gnosisSafe = 'GNOSIS_SAFE';

  // The owner of the ProxyAdmin can upgrade our contracts
  await admin.transferProxyAdminOwnership(gnosisSafe);
  console.log('Ownership transferred to', gnosisSafe);
  }
};

truffle-config.js network:

rinkeby: {
      networkCheckTimeout: 1000000, // 10000,
      provider: () => new HDWalletProvider({
        mnemonic: {
          phrase: process.env.MNEMONIC
        },
        providerOrUrl: `wss://rinkeby.infura.io/ws/v3/${process.env.INFURA_PROJECT_ID}`
      }),
      network_id: 4,       // Rinkeby's id
      gas: 4500000,        // Rinkeby has a lower block limit than mainnet
      timeoutBlocks: 300,  // # of blocks before a deployment times out  (minimum/default: 50)
      skipDryRun: true     // Skip dry run before migrations? (default: false for public nets )
}

:computer: Environment

  • Truffle ^5.4.29
  • Node v16.13.1
  • npm v8.1.2

Since you are using UUPSUpgradable, your contract is deployed as a UUPS proxy.

UUPS proxies do not use a proxy admin, so admin.transferProxyAdminOwnership(gnosisSafe); is not the right way to transfer its ownership.

Instead, just use instance.transferOwnership(gnosisSafe) (your contract needs to implement _authorizeUpgrade() to restrict upgrades to only the owner or another role or address if appropriate)

1 Like

I see. Thanks for your answer! This did it. So does that mean that the owner (from Ownable) is also the "proxy admin" (the one who can upgrade the implementation contract)?

1 Like

In the simplest case, yes. But this can be customized in your _authorizeUpgrade() function.

Hello Eric! Im running into the same error. However, my contract is not Ownable, it uses AccessControlUpgradeable though so I was trying to call the instance with the grantRole function in this way:

// transfer_ownership.ts 
import { ethers } from "hardhat";

async function main() {
  const gnosisSafe = "GNOSIS_SAFE";
  const signers = await ethers.getSigners();
  const instance = await ethers.getContractAt(
    "MyUpgradeable",
    "ADDRESS",
    signers[0]
  );

  console.log("Transferring ownership of ProxyAdmin...");
  const role = "DEFAULT_ADMIN_ROLE";
  // The owner of the ProxyAdmin can upgrade our contracts
  await instance.grantRole(role, gnosisSafe);
  console.log("Transferred ownership of ProxyAdmin to:", gnosisSafe);
}

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

However, I'm not able to call this function. I tried with hardhat console but apparently the deployed instance's 'DEFAULT_ADMIN_ROLE' is still the 0x00 address even though I granted my dev wallet the DEFAULT_ADMIN_ROLE during initialization as you can see:

//MyUpgradeable.sol
contract MyUpgradeable is
  Initializable,
  AccessControlUpgradeable,
  PausableUpgradeable,
  UUPSUpgradeable
{
  bytes32 public constant URI_SETTER_ROLE = keccak256("URI_SETTER_ROLE");
  bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
  bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");

  using CountersUpgradeable for CountersUpgradeable.Counter;
  CountersUpgradeable.Counter private _eventIds;

  function initialize(uint8 _eventNumber, string memory _seriesName)
    public
    initializer
  {
    __AccessControl_init();
    __Pausable_init();
    __UUPSUpgradeable_init();
    _grantRole(DEFAULT_ADMIN_ROLE, MYDEVWALLETADDRESS); 
    _grantRole(URI_SETTER_ROLE, msg.sender);
    _grantRole(PAUSER_ROLE, msg.sender);
  1. Why is it not my dev wallet the DEFAULT_ADMIN_ROLE when I check it on the console?
  2. How can I retrieve the DEFAULT_ADMIN_ROLE value on hardhat transfer_ownership script? (assuming that the function Im trying to call is the proper one).

Thank you in advance for your help!

You can check if an address has a certain role by calling the hasRole(bytes32 role, address account) function.

For example, the role identifier of DEFAULT_ADMIN_ROLE is 0x00 as bytes32, so you can check it like this in Hardhat:

console.log(await instance.hasRole(hre.ethers.utils.formatBytes32String(0x00), YOUR_ADDRESS));

I see that the DEFAULT_ADMIN_ROLE is defined as 0x00 in OZ contracts. When called by the contract itself it throws the following bytes32 value made of 66 characters:

0x0000000000000000000000000000000000000000000000000000000000000000

However, when trying to use it with ethers (ethers.utils.formatBytes32) with the value it was initialised (“0x00”) it throws: '0x3078303000000000000000000000000000000000000000000000000000000000’.
Why is it different? I understand that ethers.utils.formatBytes32: “ Returns a bytes32 string representation of text”.
I am creating a test to check if the contract is granting the DEFAULT_ADMIN_ROLE to a specific address but:

  1. The method is typed to allow only strings.
  2. The method allows to put non-string text in the console.
    So what does the method do when you are not entering a string vs when you are?

I'm a bit confused about which one should I use as the parameter for the grantRole function?
const defaultAdminRole =
"0x3078303000000000000000000000000000000000000000000000000000000000";

or

const defaultAdminRole =
"0x0000000000000000000000000000000000000000000000000000000000000000"; ?

Hi,

grantRole's first parameter is the hash of the Role string. For 'DEFAULT_ADMIN_ROLE' is 0x1effbbff9c66c5e59634f24fe842750c60d18891155c32dd155fc2d661a4c86d.

Hope this helps :wink:

H

Hello @Ache ! How did you figure out that value?

You can get it like this:
const adminHash = web3.utils.soliditySha3('DEFAULT_ADMIN_ROLE')