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.
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.