Problems testing Proxy -> Implementation pattern

I've worked through all of the proxy documentation, a bunch of forum posts, and a number of the workshops on the site and am currently unable to find a good example of how to effectively test the Proxy -> Implementation pattern in Hardhat. It seems like I'm missing something obvious but without any examples of how the proxy is implemented I can't seem to figure out what's missing.

This is a pretty barebones example that should work but fails with this call revert exception:

Error: call revert exception (method="version()", errorArgs=null, errorName=null, errorSignature=null, reason=null, code=CALL_EXCEPTION, version=abi/5.5.0)

:1234: Code to reproduce

TestProxy.sol

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

import "@openzeppelin/contracts/proxy/Proxy.sol";

contract TestProxy is Proxy {

    address public implementation;

    constructor(address implementation_) {
        implementation = implementation_;
    }

    function _implementation() internal view virtual override returns (address) {
        return implementation;
    }

}

ERC1155TestV1.sol

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

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import '@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol';
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

contract ERC1155TestV1 is Initializable, ERC1155Upgradeable, UUPSUpgradeable, OwnableUpgradeable {

    function initialize() initializer public {
        __ERC1155_init("");
        __Ownable_init();
        __UUPSUpgradeable_init();
    }

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() initializer {}

    function _authorizeUpgrade(address) internal override onlyOwner {}

    function setURI(string memory newuri) public onlyOwner {
        _setURI(newuri);
    }

    function version() public pure returns (string memory) {
        return "1";
    }

}

TestProxy.js

const { expect } = require("chai");
const { ethers, upgrades } = require("hardhat");

describe("Test a proxy", () => {

  before(async () => {
    [owner, sender] = await ethers.getSigners();

    // instantiate the testproxy contract and deploy
    ERC1155TestV1 = await ethers.getContractFactory("ERC1155TestV1");
    IMPLEMENTATION = await upgrades.deployProxy(ERC1155TestV1, { kind: 'uups' });
    console.log(`IMPLEMENTATION contract: ${IMPLEMENTATION.address}`);
    
    // instantiate the proxy contract and deploy
    TestProxy = await ethers.getContractFactory("TestProxy");
    PROXY = await TestProxy.deploy(IMPLEMENTATION.address);
    console.log(`PROXY contract: ${PROXY.address}`);

    // use our proxy contract to get version of the implementation contract
    let version = await ERC1155TestV1.attach(PROXY.address).version();   // FAILS HERE
    console.log(`VERSION: ${version}`);
  });

  describe("Verify project deployment", async () => {

    it("Should verify that the project contract was deployed and initialized", async () => {
      expect(IMPLEMENTATION.address).to.be.properAddress;
      expect(PROXY.address).to.be.properAddress;
    });

  });

});

I've also attempted to implement ERC1967Proxy which results in a different error but amounts to the same problem: failure with no explanation.

Error: Transaction reverted without a reason string
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract TestProxy is ERC1967Proxy {

    constructor (address _implementation, bytes memory _data) ERC1967Proxy(_implementation, _data) {}

    function getImplementation() public view returns (address) {
        return _getImplementation();
    }

    function upgradeTo(address newImplementation) public {
        _upgradeTo(newImplementation);
    }

}

:computer: Environment

{
  "devDependencies": {
    "@nomiclabs/hardhat-ethers": "^2.0.4",
    "@nomiclabs/hardhat-etherscan": "^3.0.0",
    "@nomiclabs/hardhat-waffle": "^2.0.2",
    "@openzeppelin/hardhat-upgrades": "^1.13.0",
    "chai": "^4.3.6",
    "ethereum-waffle": "^3.4.0",
    "ethers": "^5.5.3",
    "hardhat": "^2.8.3"
  },
  "dependencies": {
    "@openzeppelin/contracts": "^4.5.0",
    "@openzeppelin/contracts-upgradeable": "^4.5.1",
    "hardhat-gas-reporter": "^1.0.7",
    "hardhat-watcher": "^2.1.1"
  }
}

Incidentally, ethers.getContractAt("ERC1155TestV1", PROXY.address) leads to the same error.

Hi @SerBlank,

If you intend to use OpenZeppelin's UUPS proxy pattern, there is no need to implement your own proxy contract. When you call upgrades.deployProxy(...), that handles deploying both your implementation contract AND a proxy contract (ERC1967Proxy is deployed for UUPS). The contract instance returned by deployProxy contains the proxy address.

Also, note that the way you've written TestProxy will cause a storage collision because your variable address public implementation; uses a storage slot which collides with the first storage slot used by your implementation. This is why OpenZeppelin's proxies use unstructured storage instead and you can just use the plugins to help deploy one of the OpenZeppelin proxy contracts for you.

So for your example above, it can be simplified to something like this:

before(async () => {
    [owner, sender] = await ethers.getSigners();

    // deploy implementation and a proxy that points to it
    ERC1155TestV1 = await ethers.getContractFactory("ERC1155TestV1");
    PROXY = await upgrades.deployProxy(ERC1155TestV1, { kind: 'uups' });
    console.log(`PROXY contract: ${PROXY.address}`);
    
    // use our proxy contract to get version of the implementation contract
    let version = await PROXY.version(); 
    // ^The above works because deployProxy returns a contract instance, 
    // but you can do as before: await ERC1155TestV1.attach(PROXY.address).version();
    console.log(`VERSION: ${version}`);
  });

Thanks, @ericglau. That worked.

So am I correct in assuming that were I to want to call ERC1155TestV1 from another contract that I can just create an interface to the proxy contract and call it the same way I'd call any other contract?

I believe I was conflating proxies and interfaces in this case as a result of the proxy + implementation terminology. It wasn't clear to me from the available examples that deployProxy() deploys both the proxy and implementation contracts. It makes sense looking backwards now of course :smile:

Yes, you would call it using the proxy's address.

Excellent. Thank you @ericglau!