How to use TransparentUpgradeableProxy as Transparent Proxy

I'm Solidity Newbie.

I'm learning how to implement Transparent Proxy using Openzeppelin's TransparentUpgradeableProxy contract, but am having some problems.

Step 1: I tried to deploy a simple contract MyConV0, then implemented deploy and call method, everything is fine.

// File MyConV0.sol
pragma solidity ^0.8.0;

import "hardhat/console.sol";

contract MyConV0 {    
    string private _name;
    string private _symbol;

    constructor(string memory name_, string memory symbol_) public {            
      _name = name_;
      _symbol = symbol_;    
      console.log(_symbol);
    }
    
    function symbol() public view returns (string memory) {
        console.log(_symbol);
        return _symbol;
    }

    function name() public view returns (string memory) {
        console.log('Name: ');
        console.log(_name);
        return _name;
    }

    function getVersion() pure external returns(uint256) {
        return 0;
    }    
}

Step 2: I tried to upgrade to MyConV1 to be able to Upgradable with TransparentUpgradeableProxy but failed.

// File: MyConV1
pragma solidity ^0.8.0;

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "hardhat/console.sol";

contract MyConV1 is Initializable, OwnableUpgradeable {   
    string private _name;
    string private _symbol;

    function initialize(string memory name_, string memory symbol_) initializer public {      
      __Ownable_init();
      _name = name_;
      _symbol = symbol_;
      
      console.log(_symbol);
    }

    
    function symbol() public view returns (string memory) {
        console.log('Symbol: ');
        console.log(_symbol);
        return _symbol;
    }

    function name() public view returns (string memory) {
        console.log('Name: ');
        console.log(_name);
        return _name;
    }

    function getVersion() pure external returns(uint256) {
        return 1;
    }
    
}

Refer TransparentUpgradableProxy: https://docs.openzeppelin.com/contracts/4.x/api/proxy#TransparentUpgradeableProxy

// The test file in JS using ethers.js and Hardhat environment
async function main() {

  let t0, t1, t2, t3, t4, v1, v2, v3, v4;
  
  const [owner, proxyAdmin, user, other] = await ethers.getSigners();
  
  // --- 0. Deploy MyConV0
  let input0 = [ 'TotoName', 'Toto' ];
  let con0 = await deploy_verify_contract("MyConV0", input0);  
  t0 = await con0.symbol() ;
  console.log(t0); // worked -> Toto

  // --- 1. Deploy MyConV1
  let input1 = [ ];
  let con1 = await deploy_verify_contract("MyConV1", input1);  


   // --- 2. get data
  let abi = [ `function initialize(   string name_,
                                      string symbol_,                                      
                                  )` ];
  let iface = new ethers.utils.Interface(abi);
  let data = iface.encodeFunctionData("initialize", [ 'TotoName', 'Toto' ]);  
  
  
  // --- 3. deploy trans proxy
  let input2 = [ con1.address, owner.address, data ];  
  let con2 = await deploy_verify_contract("TransparentUpgradeableProxy1", input2);  

  // --- 4. call proxy method
  t2 = await con2.implementation();
  console.log(t2); // DO NOT WORK, t2 is object tx, and do not contains the results like step 0

  // --- 5. call MyConV1 contact via proxy -> ERROR: "TypeError: con2.symbol is not a function"
  t3 = await con2.symbol();
  console.log(t3);

}

async function deploy_verify_contract(contractName, input, lib = {}){
  
  const _contract = await hre.ethers.getContractFactory(contractName, lib);
  const contract = await _contract.deploy(...input);

  await contract.deployed();

  console.log( contractName + " deployed to:", contract.address );

  return contract;

}

I used Hardhat's console.log function and it seems to have successfully deployed the Proxy, and sent the correct data to the MyConV1.initialize function, but don't know how to call the proxy properly. Specifically with the above code, I don't understand a few points:

  • Have I deployed the proxy correctly?
  • Why can't I get the correct return data of the proxy's implementation() function?
  • Why can't I call MyConV1's function through the proxy?

Hope you guys can help me how to correct the code, I have not been able to solve this problem for a few days.

Hi @Stone, here are a few suggestions:

  • Your first contract MyConV0 should also have an initializer instead of a constructor (if you will be using it with a proxy / if you will be upgrading it).
  • Instead of deploying the proxy contract directly, consider using OpenZeppelin's Hardhat Upgrades plugin. This simplifies the process since you can just call its deployProxy function, which will deploy both the implementation and proxy contract (as well as a proxy admin contract for the transparent proxy). The benefit of using this plugin is that it will verify that your implementation is upgrade safe.
  • More generally, to get the implementation address, you can use erc1967.getImplementationAddress from the above plugin to read the implementation address from the proxy's storage slots.