UUPS Proxy Contract Deployment

I'm deploying my MyToken contract like so:

const ERC721 = await ethers.getContractFactory("MyContract");
const erc721 = await upgrades.deployProxy(ERC721, [], { kind: "uups" })

This gives me two addresses:

1/ Address A for the original contract
2/ Address B for the proxy contract

Now when I try to deploy ProxyTest like so:

const proxy = await ethers.getContractFactory("ProxyTest");
const proxyDeployed2 = await proxy.deploy("...")

when I call await proxy.deploy with Address A, this works, but it doesn't work with Address B and fails with:

error={"name":"ProviderError","code":-32603,"_isProviderError":true,"data":{"message":"Error: VM Exception while processing transaction: reverted with reason string 'Address: low-level delegate call failed'","data":"0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000027416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c656400000000000000000000000000000000000000000000000000"}}, code=UNPREDICTABLE_GAS_LIMIT, version=providers/5.6.8)
    at step (/Users/pw/Documents/coding/contracts/node_modules/@ethersproject/providers/lib/json-rpc-provider.js:48:23)
    at EthersProviderWrapper.<anonymous> (/Users/pw/Documents/coding/contracts/node_modules/@ethersproject/providers/src.ts/json-rpc-provider.ts:603:20)
    at checkError (/Users/pw/Documents/coding/contracts/node_modules/@ethersproject/providers/src.ts/json-rpc-provider.ts:78:20)
    at Logger.throwError (/Users/pw/Documents/coding/contracts/node_modules/@ethersproject/logger/src.ts/index.ts:273:20)
    at Logger.makeError (/Users/pw/Documents/coding/contracts/node_modules/@ethersproject/logger/src.ts/index.ts:261:28) {
  reason: "Error: VM Exception while processing transaction: reverted with reason string 'Address: low-level delegate call failed'",
  code: 'UNPREDICTABLE_GAS_LIMIT',
  method: 'estimateGas',

Any idea why this is? I want to utilize the original proxy contract as a way to upgrade the implementation MyToken contract - when I do make this upgrade I don't want to be responsible for upgrading all the ProxyTest contracts, so I would rather have ProxyTest point to the UUPS upgradeable proxy (vs. the original MyToken).

Let me know if there's anything I can do here.

:1234: Code to reproduce

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

import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.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, ERC721Upgradeable, OwnableUpgradeable, UUPSUpgradeable {
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    function initialize() initializer public {
        __ERC721_init("MyToken", "MTK");
        __Ownable_init();
        __UUPSUpgradeable_init();
    }

    function _authorizeUpgrade(address newImplementation)
        internal
        onlyOwner
        override
    {}
}
contract ProxyTest is Proxy {
    constructor(address addr) {
        assert(
            _IMPLEMENTATION_SLOT ==
                bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)
        );
        StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = addr;
        Address.functionDelegateCall(
            addr,
            abi.encodeWithSignature("initialize()")
        );
    }

    /**
     * @dev Storage slot with the address of the current implementation.
     * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is
     * validated in the constructor.
     */
    bytes32 internal constant _IMPLEMENTATION_SLOT =
        0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

    /**
     * @dev Returns the current implementation address.
     */
    function implementation() public view returns (address) {
        return _implementation();
    }

    function _implementation() internal view override returns (address) {
        return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value;
    }
}

:computer: Environment

Hardhat

Hello @jayepeg

You cannot chain proxies.

  • You can have A, the base contract
  • You can have B, a proxy that points to A
  • You cannot have C, a proxy that points to B

This is because B is a proxy that relies on storage for delegating, so if C marks B to be its target, and tried to execute B's code ... that is the code of a proxy ... that will fetch the target from storage ... and since we are in the context of C, that is B, so we re-delegate to B ... and that is an infinite loop that never goes to A