Foundry Upgradeable Contracts Error

Hi everyone,

Im in the process of trying to deploy an upgradeable ERC-20 token using OpenZeppelin and Foundry.

Im in the testing phase where my test code looks like this:

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

import "forge-std/Test.sol";
import "../src/MyToken.sol";
import "forge-std/console.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol";

contract MyTokenTest is Test {
    MyToken myToken;
    ERC1967Proxy proxy;
    address implementation;
    address owner;
    address newOwner;


    function setUp() public {
        MyToken implementation = new MyToken();
        address owner = vm.addr(12324);
        proxy = new ERC1967Proxy(address(implementation), abi.encodeCall(implementation.initialize, owner));
        console.log(address(proxy));
        myToken = MyToken(address(proxy));
        newOwner = address(1);
    }

    function testERC20Functionality() public {
        vm.prank(owner);
        myToken.mint(address(2), 1000);
        assertEq(myToken.balanceOf(address(2)), 1000);
    }

    function testUpgradeability() public {
        vm.prank(owner);
        MyToken newMyToken = new MyToken();
        
        ERC1967Utils.upgradeToAndCall(address(newMyToken), "");
        MyToken(address(proxy)).mint(address(3), 1000);
        assertEq(MyToken(address(proxy)).balanceOf(address(3)), 1000);

    }
}

However, im getting the error:

Failing tests:
Encountered 2 failing tests in test/MyTokenTest.t.sol:MyTokenTest
[FAIL. Reason: OwnableUnauthorizedAccount(0x0000000000000000000000000000000000000000)] testERC20Functionality() (gas: 17701)
[FAIL. Reason: OwnableUnauthorizedAccount(0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496)] testUpgradeability() (gas: 1586136)

Does my deployment and setup of the proxy look correct? I am using the same owner address to set as the proxy owner and trying to mint after. In the setup function, i do create an instance of the proxy to call mint on (e.g., myToken = MyToken(address(proxy));)

From the looks of it ,the setUp function looks correct but not sure why i am getting an unauthorized when trying to interact with the proxy/erc-20.

Appreciate any help!

Hi, you may want to try out our Foundry Upgrades library https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades to help with this. It provides functions that make it easy to deploy and upgrade proxies, which would simplify your scripts/tests.

A few observations about your code:

  • In your testERC20Functionality, the error message showing OwnableUnauthorizedAccount(0x0000000000000000000000000000000000000000) seems to suggest that the owner address for the vm.prank is not set properly.
  • vm.prank applies to the following line only. So in testUpgradability, I don't think that applies to the upgradeToAndCall
  • ERC1967Utils.upgradeToAndCall does an upgrade for the current contract (which in this case is the test script itself!), so that won't work in this context. What you can do is use the upgradeProxy function from the above Foundry Upgrades library, and include your owner address for the tryCaller parameter of that function which would use that for the prank.

eric,

I appreciate the information! I got the first issue solved regarding OwnableUnauthorizedAccount(0x0000000000000000000000000000000000000000) (i had shadowing declarations)

I tried to solve the seconding using upgradeProxy but no luck yet.

My code is as follows:

  function testUpgradeability() public {
      vm.prank(owner);
      Upgrades.upgradeProxy(address(proxy), "MyToken.sol:MyToken", "", owner);
  }

When i run forge test, i get an error [FAIL. Reason: FFI disabled: run again with --ffi if you want to allow tests to call external scripts.]. When I include the flag, the test just hangs and never finishes...

Any idea of this or potential alternatives?

Thanks

You can remove vm.prank(owner); before Upgrades.upgradeProxy since the latter handles that automatically.

Can you check if you did the "Before Running" steps in the readme? https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades#before-running

Alternatively, you can try the below for now, which skips the upgrade safety checks (but we don't recommend skipping the checks for actual deployments)

      Options memory opts;
      opts.unsafeSkipAllChecks = true;
      Upgrades.upgradeProxy(address(proxy), "MyToken.sol:MyToken", "", opts, owner);

I rechecked the README and added an annotion to a new token contract (V2) e.g., @custom:oz-upgrades-from MyToken and re-ran the test but it still hangs on the test... (other sample tests work)

When I add opts to the line: Upgrades.upgradeProxy(address(proxy), "MyToken.sol:MyToken", "", opts, owner); My code doesn't find the function anymore which is odd: Member "upgradeProxy" not found or not visible after argument-dependent lookup in type(library Upgrades).

If I keep it an empty string instead of opts, it just hangs still.

Note I do run forge clean before running forge test

I resolved my issue regarding the not found issue and am trying it via the skipAllChecks method, but now getting error:

[FAIL. Reason: OwnableUnauthorizedAccount(0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496)]

    function testUpgradeability() public {
        Options memory opts;
        opts.unsafeSkipAllChecks = true;
        vm.prank(owner);
        Upgrades.upgradeProxy(address(proxy), "MyTokenV2.sol:MyTokenV2", "", opts);
    }

I tried with and without vm.prank (didnt make a difference).

Any ideas?

Got the tests working by adding the owner parameter to the function. Would be nice to figure it out without unsafeChecks so will keep trying.

    function testUpgradeability() public {
        Options memory opts;
        opts.unsafeSkipAllChecks = true;
        Upgrades.upgradeProxy(address(proxy), "MyTokenV2.sol:MyTokenV2", "", opts, owner);
    }
1 Like

If you run without --ffi and it gives an error like:

    ├─ [0] VM::ffi([npx, @openzeppelin/upgrades-core@^1.31.0, validate, out/build-info, --contract, MyTokenV2, --requireReference]) 
    │   └─ ← "FFI disabled: run again with `--ffi` if you want to allow tests to call external scripts."

Then you can try copy and pasting that command to run it manually in a terminal, but remove the commas, for example:

npx @openzeppelin/upgrades-core@^1.31.0 validate out/build-info --contract MyTokenV2 --requireReference

Can you try that and see what is the result when you run it manually?

I ended up removing the unSafeCheck and made sure my annotation was properly setup (before it was set INSIDE the contract, now its right above the contract declaration:

/// @custom:oz-upgrades-from MyToken.sol:MyToken
contract MyTokenV2 is Initializable, ERC20Upgradeable, OwnableUpgradeable, ERC20PermitUpgradeable, UUPSUpgradeable {

My current code looks like this:

    // Test the upgradeability of the MyToken contract
    function testUpgradeability() public {
        // Upgrade the proxy to a new version of MyToken (MyTokenV2)
        vm.prank(owner); //note i tried with and without this
        Upgrades.upgradeProxy(address(proxy), "MyTokenV2.sol:MyTokenV2", "", owner);
    }

But result in:

[FAIL. Reason: Upgrade safety validation failed: ] testUpgradeability() (gas: 44545)

As per your example code using npx, i got:

Error: The contract src/MyTokenV2.sol:MyTokenV2 does not specify what contract it upgrades from. Add the `@custom:oz-upgrades-from <REFERENCE_CONTRACT>` annotation to the contract, or include the reference contract name when running the validate command or function.

Which flow do you suggest I try for a mainnet deployment? (I prefer not to use safeUnchecks)

Can you please do the following:

  1. In your MyTokenV2, annotate it as:
/// @custom:oz-upgrades-from MyToken
contract MyTokenV2 is Initializable, ERC20Upgradeable, OwnableUpgradeable, ERC20PermitUpgradeable, UUPSUpgradeable {
  1. Run the following and provide all of the output
forge clean && forge build && npx @openzeppelin/upgrades-core@^1.31.0 validate out/build-info --contract MyTokenV2 --requireReference
  1. Run the following and provide all of the output
forge clean && forge build && forge test --ffi -vvv

For 2.

 ✘  src/MyTokenV2.sol:MyTokenV2 (upgrades from src/MyToken.sol:MyToken)

      src/MyTokenV2.sol:12: Contract `MyTokenV2` has a constructor
          Define an initializer instead
          https://zpl.in/upgrades/error-001

FAILED

I checked MyTokenV2 (same code as original, just naming differences and it has no constructor, it has a initialize func.

For 3:

Running 2 tests for test/MyTokenTest.t.sol:MyTokenTest
[PASS] testERC20Functionality() (gas: 48673)
Logs:
  0x7e5f4552091a69125d5dfcb7b8c2659029395bdf

[FAIL. Reason: Upgrade safety validation failed: ✘  src/MyTokenV2.sol:MyTokenV2 (upgrades from src/MyToken.sol:MyToken)

      src/MyTokenV2.sol:12: Contract `MyTokenV2` has a constructor
          Define an initializer instead
          https://zpl.in/upgrades/error-001

FAILED] testUpgradeability() (gas: 46530)
Logs:
  0x7e5f4552091a69125d5dfcb7b8c2659029395bdf

Traces:
  [46530] MyTokenTest::testUpgradeability() 
    ├─ [0] VM::prank(0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf) 
    │   └─ ← ()
    ├─ [0] VM::startPrank(0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf) 
    │   └─ ← "You cannot overwrite `prank` until it is applied at least once"
    ├─ [0] VM::startPrank(0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf) 
    │   └─ ← "You cannot overwrite `prank` until it is applied at least once"
    ├─ [0] VM::ffi([npx, @openzeppelin/upgrades-core@^1.31.0, validate, out/build-info, --contract, MyTokenV2, --requireReference]) 
    │   └─ ← 0xe29c9820207372632f4d79546f6b656e56322e736f6c3a4d79546f6b656e5632202875706772616465732066726f6d207372632f4d79546f6b656e2e736f6c3a4d79546f6b656e290a0a2020202020207372632f4d79546f6b656e56322e736f6c3a31323a20436f6e747261637420604d79546f6b656e56326020686173206120636f6e7374727563746f720a20202020202020202020446566696e6520616e20696e697469616c697a657220696e73746561640a2020202020202020202068747470733a2f2f7a706c2e696e2f75706772616465732f6572726f722d3030310a0a4641494c4544
    └─ ← "Upgrade safety validation failed: ✘  src/MyTokenV2.sol:MyTokenV2 (upgrades from src/MyToken.sol:MyToken)

      src/MyTokenV2.sol:12: Contract `MyTokenV2` has a constructor
          Define an initializer instead
          https://zpl.in/upgrades/error-001

FAILED"

Test result: FAILED. 1 passed; 1 failed; 0 skipped; finished in 2.80s
 
Ran 2 test suites: 3 tests passed, 1 failed, 0 skipped (4 total tests)

Failing tests:
Encountered 1 failing test in test/MyTokenTest.t.sol:MyTokenTest
[FAIL. Reason: Upgrade safety validation failed: ✘  src/MyTokenV2.sol:MyTokenV2 (upgrades from src/MyToken.sol:MyToken)

      src/MyTokenV2.sol:12: Contract `MyTokenV2` has a constructor
          Define an initializer instead
          https://zpl.in/upgrades/error-001

FAILED] testUpgradeability() (gas: 46530)

Thanks for providing those details and I appreciate your patience with this. The validations seem to be detecting a constructor at MyTokenV2.sol line 12.

If you don't mind, could you share the code for both versions of your MyToken and MyTokenV2, and also the .json file(s) under out/build-info? Or if you can you share your project as a GitHub repository, that would be easier and I can try to take a look.

Actually i do have a constructor sorry:

    constructor() {
        _disableInitializers();
    }

Here are both versions:

MyToken:

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

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

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

    function initialize(address initialOwner) initializer public {
        __ERC20_init("MyToken", "MTK");
        __Ownable_init(initialOwner);
        __ERC20Permit_init("MyToken");
        __UUPSUpgradeable_init();

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

    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }

    function _authorizeUpgrade(address newImplementation)
        internal
        onlyOwner
        override
    {}
}

MyTokenV2:

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

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

/// @custom:oz-upgrades-from MyToken
contract MyTokenV2 is Initializable, ERC20Upgradeable, OwnableUpgradeable, ERC20PermitUpgradeable, UUPSUpgradeable {
    constructor() {
        _disableInitializers();
    }

    function initialize(address initialOwner) initializer public {
        __ERC20_init("MyTokenV2", "MTKV2");
        __Ownable_init(initialOwner);
        __ERC20Permit_init("MyTokenV2");
        __UUPSUpgradeable_init();

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

    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }

    function _authorizeUpgrade(address newImplementation)
        internal
        onlyOwner
        override
    {}
}

Can get you those json files if you really need

No need, I see the issue now. You just need to add /// @custom:oz-upgrades-unsafe-allow constructor to the constructor in MyTokenV2:

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }
1 Like

Appreciate your time with this!

1 Like