Call UUPS proxied contract with a web3 node.js app

Sorry for posting this again but I really need help as I can't find resources and still can't figure it out on my own.

I have two ERC20 tokens already deployed on the Ropsten testnet (let's call them V1 and V2). V1 is a simple unproxied ERC20 token while V2 is a UUPSUpgradeable ERC20 token (see codes below). These contracts were generated from wizard.openzeppelin and were not modified. Both contracts pass the get owner, symbol, and name truffle tests.

To interact with V2 using the same web3 nodejs code, I tried updating the build/abi as well as process.env.CONTRACT_ADDRESS from V1's address to V2's. However, whenever I retrieve the owner using the same code, it always returns the zero address.

To be clear, I'm not trying to upgrade from V1 to V2. I just used versioning to differentiate them in my post. Also, I use truffle instead of hardhat for migrations and tests.

I think the call should be proxied or something, but I don't know how and I can't find resources (docs/tutorials) on interacting with UUPS contracts using truffle. Would appreciate some help.

:1234: Code to reproduce

V1 contract:

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

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyToken is ERC20, Ownable {
    constructor() ERC20("MyToken", "MTK") {}

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

node.js app to interact with V1:

const Web3 = require('web3');
const MyToken = require('./build/contracts/MyToken.json');
const HDWalletProvider = require('@truffle/hdwallet-provider');

const provider = new HDWalletProvider(process.env.ACCOUNT_SECRET, process.env.INFURA_URL);
const web3 = new Web3(provider);
const contract = new web3.eth.Contract(MyToken.abi, process.env.CONTRACT_ADDRESS);
await contact.methods.owner().call();

V2 contract:

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

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

    function initialize() initializer public {
        __ERC20_init("MyToken", "MTK");

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

    function _authorizeUpgrade(address newImplementation)

:computer: Environment

Ubuntu 20.04
npm v8.1.0
node v14.18.0
truffle v5.4.18

Have you tried verifying them on etherscan and then directly interacting with them? That could help you to gain more insight.

For the UUPS proxy....the proxy itself will normally be verified during deployment by the plugin. So then you'd need to verify the implementation contract (via plugin or via web) and then you can use this page here to tell the proxy that he is a proxy and that the implementation is verified, too....

And then go to the proxy address of your V2 and check owner, etc. again....

I didn't know that it needed to be verified before interacting with it. Will try this now and update this thread. Thank you for this!

It doesnt need to. You can also use scripts or other contracts to interact with it.
But it may lead to some more insights.