0

I'm trying to programmatically read an upgradeable smart contract (USDC token) on Polygon and on Ethereum mainnet, using javascript (web3 1.6.0 in a nodeJS v14 environment) and multiple websocket endpoints. I'm encountering different issues on Ethereum vs polygon, but I can't get my balance on either, even though I can get my balance for non-upgreadable smart contracts easily. Is there anything special I need to do in order to read upgradeable smart contract implementations vs "regular" ones? I'd like to get the same result using Javascript as when using the "Read as Proxy" tab in [polygon, ether]scan.com (at https://polygonscan.com/address/0x2791bca1f2de4661ed88a30c99a7a9449aa84174#readProxyContract)

Here's some of the relevant code I'm using to make the calls:


function callContractMethod({chainID, contractAddress, address, methodName, parameters, returnIndeces}){
  if (!chainID){
    return Promise.reject("no chainID");
  }
  return getAbi(chainID, contractAddress)
  .then(abi => {
    var web3 = get(chainID);
    web3.eth.defaultAccount = address;
    
    var contract = new web3.eth.Contract(abi, contractAddress);
    
    if(_.isFunction(_.get(contract, `methods.${methodName}`))){
      var p;
      if (parameters){
        p = contract.methods[methodName].apply(this, parameters.split(' ')).call()//({from:address})
      } else {
        p = contract.methods[methodName]().call()
      }
      return p.then(result => {
        console.log('contract result');
        console.log(JSON.stringify(result, null, 2));
        if(returnIndeces){
          var data = {};
          _.each(returnIndeces.split(' '), i =>{
            _.set(data, i, _.get(result,i));
          });
          return data;
        } else {
          return result;
        }
      }).catch(err => {
        console.log("contract read error:",err);
      });
    } else if (_.isFunction(_.get(contract, 'methods.implementation'))){
      //      return Promise.reject("proxy contracts do not work");
      
      console.log(`${chainID} ${contractAddress} is a proxy contract. accessing implementation`);
      return contract.methods.implementation().call()
      .then(implementationContract => {
        
        console.log(`got implementation for ${chainID} ${contractAddress}. it is: ${implementationContract}. calling ${methodName} on that contract...`);
        return callContractMethod({
          chainID,
          contractAddress:implementationContract,
          address,
          methodName,
          parameters,
          returnIndeces
        });
      })
      .catch(err =>{
        console.log("cannot get implementation ", err);
      })
    } else {
      return Promise.reject(`no method ${methodName} in contract ${contractAddress} on chain ${chainID}`);
    }
  });
  
}

var wsMap = {};

function get(chainID){
  var wsUrl = _.get(chains, `${chainID}.ws`);
  var httpUrl = _.get(chains, `${chainID}.http`);
  
  const options = {
    // Enable auto reconnection
    reconnect: {
      auto: true,
      delay: 5000, // ms
      maxAttempts: 5,
      onTimeout: false
    }
  };
  
  var api;
  if (_.isObject(wsMap[chainID])) {
    return wsMap[chainID];
  } else if (wsUrl){
    api = new Web3(wsUrl);
    api.setProvider(new Web3.providers.WebsocketProvider(wsUrl, options));
    _.set(wsMap, chainID, api);
    return api;
  } else if (httpUrl){
    
    var api = new Web3(httpUrl);
    api.setProvider(new Web3.providers.HttpProvider(httpUrl));
    _.set(wsMap, chainID, api);
    return api;
  } else {
    throw new Exception("unrecognized chain id "+chainID);
  }
  
  api.eth.handleRevert = true;
  return api;
}

// chains is a map of chain data indexed by the chain id that allows me to dynamically initialize multiple instances of web3 for different chains. I am withholding my full private urls

var chains = {
  "43114":{
    name: "avalanche",
    lookup: "avalanche",
    http:"https://speedy-nodes-nyc.moralis.io/abc/avalanche/mainnet",
    scan:"https://api.snowtrace.io/api",
    native:"AVAX"
  },
  "137":{
    name:"polygon-pos",
    ws:"wss://polygon-mainnet.g.alchemy.com/v2/abcde",
    scan:"https://api.polygonscan.com/api",
    native:"MATIC",
    lookup:"polygon"
  },
  "1":{
    name:"ethereum",
    lookup:"eth",
    ws:"wss://speedy-nodes-nyc.moralis.io/abcd/eth/mainnet/ws",
    scan:"https://api.etherscan.io/api",
    native:"ETH"
  }
///... and so on
};

Using this code, I can get the balance of any smart contract on any configured chain as long as it is not an "upgradeable" smart contract. If I try with an upgradeable smart contract. say, USDC: for ethereum mainnet, the line contract.methods.implementation().call() fails with:

cannot get implementation  Error: Returned error: execution reverted
    at Object.ErrorResponse (/app/node_modules/web3-core-helpers/lib/errors.js:28:19)
    at Object.callback (/app/node_modules/web3-core-requestmanager/lib/index.js:302:36)
    at /app/node_modules/web3-providers-ws/lib/index.js:114:45
    at Array.forEach (<anonymous>)
    at WebsocketProvider._onMessage (/app/node_modules/web3-providers-ws/lib/index.js:102:69)
    at W3CWebSocket._dispatchEvent [as dispatchEvent] (/app/node_modules/yaeti/lib/EventTarget.js:115:12)
    at W3CWebSocket.onMessage (/app/node_modules/websocket/lib/W3CWebSocket.js:234:14)
    at WebSocketConnection.<anonymous> (/app/node_modules/websocket/lib/W3CWebSocket.js:205:19)
    at WebSocketConnection.emit (events.js:400:28)
    at WebSocketConnection.processFrame (/app/node_modules/websocket/lib/WebSocketConnection.js:554:26)
    at /app/node_modules/websocket/lib/WebSocketConnection.js:323:40
    at processTicksAndRejections (internal/process/task_queues.js:77:11) {
  data: null
}

Strangely, for Polygon PoS, contract.methods.implementation().call() succeeds, and returns the address of the implementation contract. But, when I proceed to make calls to that contract directly, calling "decimals" returns "0" and calling "balanceOf" for my private address returns "0", but these values aren't correct.

I'm getting the same results from different web3 providers: moralis, alchemy and infura, as well as public endpoints.

7
  • Hi Brad! Welcome to Ethereum Stackexchange! Did you try with another web3 provider? Another thing you might try is upgrading/downgrading web3js, sometimes sidechains aren't tested that well with new releases.
    – Ismael
    Nov 16, 2021 at 13:03
  • @Ismael Yeah, I've tried multiple providers, but none work. AXS token on Binance smart chain and USDC on Ethereum mainnet also fail, with "execution reverted", these both are also migratable smart contracts. Using the latest version of web3.
    – bluegreen
    Nov 22, 2021 at 10:18
  • Sometimes web3js is the problem, did you try querying directly the endpoint with curl? which web3 provider are you using? Infure, etherscan, etc... are you connected to the right network? Can you show the code you are using?
    – Ismael
    Nov 22, 2021 at 17:07
  • @Ismael thanks for the suggestions. I modified the question to include some of the code and more details about what is happening. I don't know how to query directly with curl, but if you have a resource for how to do that, I'll try it.
    – bluegreen
    Nov 24, 2021 at 7:43
  • What does getAbi do? My guess is that it is reading the proxy ABI and not the conotract ABI. The issue there is that there are different proxy types, so there's no standard way to determine the proxied address that has the correct ABI. Etherscan has it but I'm not sure if they provide an API.
    – Ismael
    Dec 3, 2021 at 5:20

1 Answer 1

1

To get data from an upgradeable smart contract, you should interact with the Proxy Contract Address using the ABI of the Implementation Contract.

When you read data directly from the Implementation contract (in the case of USDC, the FiatTokenV2_1), it will return empty values. This happens because the data is stored at the Proxy Contract. The Implementation Contract stores just the logic, which is accessed by the Proxy Contract.

The code below uses contract-loader to perform this action. Contract-loader will search for the artifact from a specific folder (./build/contracts),

const Web3 = require('web3');
const { setupLoader } = require('@openzeppelin/contract-loader');
const mnemonic = "put-here-the-12-word-mnemonic";
const HDWalletProvider = require('@truffle/hdwallet-provider');
const provider = new HDWalletProvider(mnemonic, "put-here-the-Infura-URL");


    const web3 = new Web3(provider);
    // Set up a web3 contract, representing a deployed ERC20, using the contract loader
    const address = 'put-here-the-proxy-contract-address';
    const loader = setupLoader({ provider: web3 }).web3;
    const token = loader.fromArtifact('FiatTokenV2_1', address);
   
        // Call the deployed token contract
const name = await token.methods.name().call();
const symbol = await token.methods.symbol().call();
const decimals = await token.methods.decimals().call();
const totalSupply = await token.methods.totalSupply().call();

console.log(`${name} (${symbol}) - Decimals:${decimals} Total Supply:${totalSupply}`);

Edit 1: You can also use the following method for loading the token ABI on proxy address:

let abi = require("./build/contracts/FiatTokenV1.json").abi;
let tokenAsProxy = new web3.eth.Contract(abi, 'put-here-the-proxy-address');
tokenAsProxy.methods.balanceOf('address').call()

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.