Error Handling for Proxy Contracts in web3 using .error

Hi Andrew (@abcoathup),

It’s been a while since I’ve been on the forum. I trust that you are well and you’re enjoying the Australian summer.

I have been working on error handling recently using the event emitter. If I call a function directly from a contract (using contract.methods.myMethod().send{from: Ganache supplied account}) with an input that violates a require statement, then the .error part of the event emitter is invoked. That works as I would expect.

However, if I call a function through a proxy contract with an input that violates a require statement, the .error part of the event emitter is not invoked. I would note that the ‘transactionHash’, 'receipt, and ‘confirmation’ events of the event emitter work when going through proxy contracts. However, I do receive a notification of an error through the console even though I do not have any console.log() statements in my code. See below:

web3.min.js:20 Uncaught (in promise) Error: Returned error: VM Exception while processing transaction: revert Number of offices can't be less than one
    at Object.ErrorResponse (web3.min.js:20)
    at Object.callback (web3.min.js:20)
    at web3.min.js:30
    at Array.forEach (<anonymous>)
    at s._onMessage (web3.min.js:30)
ErrorResponse @ web3.min.js:20
(anonymous) @ web3.min.js:20
(anonymous) @ web3.min.js:30
s._onMessage @ web3.min.js:30
async function (async)
initializeElectionProxy @ playing.js:743
dispatch @ jquery.min.js:3
r.handle @ jquery.min.js:3

I tried to encapsulate my function call in try catch blocks, but that didn’t do anything.

So, my problem is that I need capture the error information in my javascript code and not just have it displayed in the console. My understanding is that when the transaction reverts due to failing the condition in the require statement, the Yul delegatecall command in the proxy contract returns a ‘0’ which, through a switch statement, calls for a Yul revert command. This Yul revert command, in addition to ending execution and reverting state variables, also returns the failure data which occupies the memory that the normal return data would have occupied.

So, the error information is being returned by the proxy contract. However, it’s not happening in the same way that normal return data would be returned. I set up a test function that returned data and it didn’t simply appear in my console. So, there is some mechanism, outside of the proxy contract, that I don’t understand that is taking the returned error information and logging it to the console.

Do you know anything about this topic? Does the error logged to the console copied earlier in the post mean anything to you? How can I capture the error information when using proxy contracts?

I would appreciate any advice or insight. Cheers.

Sincerely,
Craig

1 Like

Hi Andrew @abcoathup,

I’m not sure why, but the error displayed in console.log now looks slightly different (see below). I can’t seem to recreate the original, but this is essentially the same thing. There are just no ‘web3.min.js’ references.

Uncaught (in promise) Error: Returned error: VM Exception while processing transaction: revert Number of offices can't be less than one
    at Object.ErrorResponse (errors.js:28)
    at Object.callback (index.js:303)
    at index.js:114
    at Array.forEach (<anonymous>)
    at s._onMessage (index.js:102)

Sincerely,
Craig

1 Like

Hi @cjd9s,

I am not familiar with error handling in web3.

I recommend creating a simple example that demonstrates the problem such as modifying the following (from: OpenZeppelin Upgrades: Step by Step Tutorial for Truffle).

Box.sol

// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
 
contract Box {
    uint256 private value;
 
    // Emitted when the stored value changes
    event ValueChanged(uint256 newValue);
 
    // Stores a new value in the contract
    function store(uint256 newValue) public {
        require(newValue > 0, "Box: store non-zero numbers");
        value = newValue;
        emit ValueChanged(newValue);
    }
 
    // Reads the last stored value
    function retrieve() public view returns (uint256) {
        return value;
    }
}

Box.test.js

// test/Box.test.js
// Load dependencies
const { expect } = require('chai');

const {
    BN,           // Big Number support
    constants,    // Common constants, like the zero address and largest integers
    expectEvent,  // Assertions for emitted events
    expectRevert, // Assertions for transactions that should fail
  } = require('@openzeppelin/test-helpers');
 
// Load compiled artifacts
const Box = artifacts.require('Box');
 
// Start test block
contract('Box', function () {
  beforeEach(async function () {
    // Deploy a new Box contract for each test
    this.box = await Box.new();
  });

  it('non-zero number cannot be stored', async function () {
    // Store an invalid value
    await expectRevert(this.box.store(0), 'Box: store non-zero numbers');
  });
});

Box.proxy.test.js

// test/Box.proxy.test.js
// Load dependencies
const { expect } = require('chai');

const {
    BN,           // Big Number support
    constants,    // Common constants, like the zero address and largest integers
    expectEvent,  // Assertions for emitted events
    expectRevert, // Assertions for transactions that should fail
  } = require('@openzeppelin/test-helpers');

const { deployProxy } = require('@openzeppelin/truffle-upgrades');
 
// Load compiled artifacts
const Box = artifacts.require('Box');
 
// Start test block
contract('Box (proxy)', function () {
  beforeEach(async function () {
    // Deploy a new Box contract for each test
    this.box = await deployProxy(Box, [42], {initializer: 'store'});
  });
 
  // Test case
  it('non-zero number cannot be stored', async function () {
    // Store an invalid value
    await expectRevert(this.box.store(0), 'Box: store non-zero numbers');
  });
});
$ npx truffle test

Compiling your contracts...
===========================
> Compiling ./contracts/Box.sol
> Compiling ./contracts/Migrations.sol
> Artifacts written to /tmp/test--9290-Ur5S92vRB2B2
> Compiled successfully using:
   - solc: 0.7.3+commit.9bfce1f6.Emscripten.clang



  Contract: Box (proxy)
    ✓ non-zero number cannot be stored (67ms)

  Contract: Box
    ✓ non-zero number cannot be stored (54ms)


  2 passing (3s)

Hi Andrew @abcoathup,

I trust that you are enjoying a pleasant weekend.

I wanted to give a quick update as I haven’t yet replied. First, thank you for the template. It has been an excellent starting point for understanding the differences between our setups and why yours works and mine doesn’t.

I believe that the gist is that your contract object contains elements of both web3.js and Truffle whereas mine only have web3.js. So, whereas .catch works with this.box.store(0), it doesn’t for my contract object and function call. It all appears to be related to how you deploy your proxy as seen below.

const { deployProxy } = require('@openzeppelin/truffle-upgrades');

I’ve been trying to set up my contract in your setup and your contract in my setup, but it’s not straightforward. So, instead, I’m going to go through your deployProxy code until I understand it. Hopefully, that will give me some insight into the differences. I’ll let you know when I have something. Cheers.

Sincerely,
Craig

1 Like

Hi Andrew @abcoathup ,

As I mentioned in my last post, I believe that this issue is related to the differences between the Truffle contract instance and the web3.js contract object. I realize that neither of these products is owned by OpenZeppelin, but it would be great if could perhaps confirm the issue. Or, even better, point out the simple mistake I’ve made. As always, any advice or comments that you’re willing to provide would be much appreciated.

So, I took your files and saw that they worked as I would have expected, unlike mine. I initially thought the problem was that I was using web3.js’s sendTransaction function to send an encoded function call whereas you directly called the function using an implementation ABI laid over a proxy contract address. However, I took the same approach in my contract, but still got the same incorrect result.

That’s when I thought that the difference must be that you were using Truffle contract instances and I was using web3.js contract objects. I’ve put together some files that illustrate the issue on github. There are instructions in the readme file on how to get it running, how to run the functions, and what to look for. Hopefully, that’s enough to show you the problem I’m experiencing. If not, please let me know and I’ll clarify. If you wouldn’t mind taking a look and letting me know what you think, that would be excellent.

The link to the github repository is https://github.com/cjd9s/ProxyErrorWeb3jsDemo.

Again, thanks so much for your time; I’ve very grateful. Cheers.

Sincerely,

Craig

1 Like

Hi @cjd9s,

It looks like the issue is how to generally handle errors in web3. As it looks like you are interacting with a non-upgradeable contract in your example code.

Unfortunately front end development is not an area of expertise for me. I haven’t found a quick answer online.

How are you expecting users to interact with your dapp? e.g. will they be using MetaMask? As you should setup your dapp in the way you are expecting them to work.

I would deploy your Box contract to a public test network and interact with it there.

As an aside, I prefer to install Truffle locally rather than globally.

Hi Andrew @abcoathup,

I appreciate the reply.

I assume that when you say non-upgradeable, you mean interacting directly with the implementation contract and not through a proxy contract. Is that correct?

If so, I would point out that I am calling the function from an implementation contract through a proxy contract. If you look in the Truffle migrations file, 2_deploy_contracts.js, you’ll see that I deploy an implementation contract (Box) and a proxy contract (InitializableAdminUpgradeabilityProxy).

let ProxyFile = artifacts.require("InitializableAdminUpgradeabilityProxy");
let Box = artifacts.require("Box");

await deployer.deploy(Box);
const BoxInstance = await Box.deployed();

await deployer.deploy(ProxyFile);
const ProxyFileInstance = await ProxyFile.deployed();

I then initialize the proxy contract and give it the address for the implementation contract
as well as set the admin address to a Ganache-supplied addressed (_accounts[1]).

  await ProxyFileInstance.initialize(BoxInstance.address,_accounts[1],'0x'); 

In the playing.js file, I create web3.js contract objects from the implementation and proxy contracts I deployed and initialized.

    initContracts: function() {
        $.getJSON('InitializableAdminUpgradeabilityProxy.json', function(data) {
            web3.eth.net.getId().then(networkId => {
                let Proxy = data;
                const deployedNetworkAddress = Proxy.networks[networkId].address;
                App.contracts.Proxy = new web3.eth.Contract(Proxy.abi, deployedNetworkAddress);
                App.contracts.Proxy.setProvider(App.web3Provider);
            });
        });

        $.getJSON('Box.json', function(data) {
            web3.eth.net.getId().then(networkId => {
                let Box = data;
                const deployedNetworkAddress = Box.networks[networkId].address;
                App.contracts.Box = new web3.eth.Contract(Box.abi, deployedNetworkAddress);
                App.contracts.Box.setProvider(App.web3Provider);
            });
        });

Then I ‘truffle migrate’ the files onto Ganache and run the files on lite-server using ‘npm run dev’. On the webpage, I enter a value into the Store Value input box and click on the Store - Event Emitter button. The function grabs the value from the input box,

let storeValue = document.getElementById("storeValue").value;

encodes a store function call using storeValue as the argument,

let storeEncoded = await App.contracts.Box.methods.store(storeValue).encodeABI();

calculates a gas estimate,

let gasEstimate = await web3.eth.estimateGas({from:App.accounts[0],to:App.contracts.Proxy.options.address,data:storeEncoded});

and sends the encoded function call to the proxy contract.

await web3.eth.sendTransaction({from:App.accounts[0],to:App.contracts.Proxy.options.address,data:storeEncoded,gas:gasEstimate})

Does that make sense?

Any ideas on why the .error aspect of the event emitter (or .catch) would work in the Truffle test environment when using proxy contracts, but not in the web3.js environment? It appears to be a difference between the Truffle contract abstraction and the web3.js contract object.

Do you concur that the event emitter (or .catch) of the sendTransaction function doesn’t work with web3.js contract objects when using proxy contracts?

No, the users will not be using Metamask. We will handle the private keys in the dapp and sign the transactions for them. So, ultimately, we will use signTransaction and sendSignedTransaction.

I agree that we should set up the dapp in the way that we are expecting it to work. When I’m a bit further along, I will switch over to signTransaction and sendSignedTransaction.

I’m not quite ready to switch to testnets, yet. Unless you’re suggesting that would have an effect on the issue we’re discussing?

Why do you prefer to install Truffle locally?

As always, I appreciate any advice or assistance. Thanks so much. Cheers.

Sincerely,
Craig

1 Like

Hi @cjd9s,

I had missed that you were using the proxy in your dapp (sorry, I am not a front end developer).

I would still encourage you to use OpenZeppelin Upgrades Plugins for deploying and testing upgradeable contracts.

I don’t know why this is. I would change your app to use the implementation directly to see if you get the same result, to rule out the proxy.

In case there is different behavior between ganache and a public testnet.

It is easier to manage versions of tools per project. I tend to only have ganache-cli installed globally.

Installing packages locally rather than globally (npx)


If you are using gasless meta transactions, I recommend watching and playing with the workshop code:

Hi Andrew,

I trust that you are having a pleasant weekend.

I appreciate your suggestion of using the Upgrades Plugins. However, I’m comfortable migrating and testing proxy contracts manually.

I’m embarrassed to report that I found my problem. It had nothing to do with the Truffle contract abstraction or web3.js contract object. The problem was in my use of the web3.js function estimateGas. This function doesn’t just give you a gas estimate, it simulates the transaction and so throws any errors that the function call would throw when used in anger. I didn’t have encapsulated it within a catch statement so the error appeared in the console as uncaught. When it estimated a function call that violated a require statement for the contract, it threw the appropriate revert error.

The following link gives a brief explanation.

I would note that it was your suggestion of trying a testnet that finally helped me understand the root cause. It wasn’t specific to your suggestion, just further playing around which led me to the problem. So, as always, you’re help is useful and appreciated.

I suspect that you’re right about installing truffle locally. However, it hasn’t caused me any problems yet so I probably won’t change my behavior until it does. Or maybe I’ll give it a try on the next project.

I’m not using gasless meta transactions, but thanks for sending me the link. I’ve watched some of the video and it’s interesting stuff.

Feel free to close this issue. Thanks for everything. Cheers.

Sincerely,
Craig

1 Like

Hi @cjd9s,

I am glad that you have resolved. I hadn’t spotted the estimateGas.

Creating a simple example to demonstrate the issue often leads to identifying the issue. I do this a lot.

Another reason, is when I install a cloned project, installing the dependencies with truffle installed locally, means I get the version the developer was using, rather than potentially running into version issues.


As an aside, we have a workshop on creating clones that may be of interest: