Manual and unaided debugging of Solidity Python blockchain applications.

n4n0b1t3
5 min readMar 2, 2022

--

I haven’t worked directly in software development for many years, so I’m not an expert when it comes to programming. But somehow I know it differently. Finding bugs in code used to be a lot easier. Maybe it’s just me, but I find debugging blockchain applications quite tedious. Now, in the last few months, I’ve become more and more active in the discussion group around the excellent youtube tutorial by Patrick Collins. Judging by the questions I keep answering there, I think I’m not alone. Many users struggle just like I do with error messages that rarely help find the error. In this article, I will go through an example that illustrates the code error scenario. There are of course other scenarios, such as incorrect settings, software version problems, or network errors, to name a few.

To anticipate what went wrong in the example that follows. I had tried to do mathematical operations with different types of integers in my solidity code. This is of course a noob mistake. The problem, once I found it, was very easy to fix. It just wasn’t easy to spot from the information I got.

What I will describe in the following is on the one hand the flow in the development process and at which point you notice that something is wrong and on the other hand what clues there are that can help you to find out where to start looking for the error.

The Solidity Code

The following code has been reduced to the essentials for debugging purposes. I have systematically switched off all other error sources one after the other. For example, I do not load the data stream key from the configuration file and do not pass it to the constructor during deployment.
There are no error messages under VSCode or Remix and the code compiles without any problems.

The output of the compilation

How I became aware of the error

After completing my contract and compiling it uneventfully (i.e. without error warnings). I set about writing a test for the getEntranceFee() function.

It looks like the following. What you see here is already reduced considerably to exclude additional error sources to better isolate the error. For this, I created a separate file so that I can test it separately via brownie test [path]. This way I don't ruin the work I had put into writing the test until the error occurred.

The error message
I will now show the complete error message so that it becomes clear how much of the output text does not help in solving the problem. There are 72 lines in the error message. After this, I will go into what of it helps find the error.

In case you are interested, you can have a look at the full terminal output under this gist here:

https://gist.github.com/n4n0b1t3/0e80e65faff3c6e7141c3b5fb3d5e4b1

Helpful error output

And these 5 lines (out of 74) help to better isolate the error.

In the first part, you can see from which line in the Python code something went wrong.

In the last line, you can see that there was a VirtualMachineError and that this caused a “revert”.

(As always, completely useless is the following text: “INFO: Could not find files for the given pattern(s).”)

What other typical error sources are there in the test code and how can we exclude them as errors?

Why should you do this? The more complex the code becomes, and the more files and libraries have to interact, the harder it is to find a bug. There is also the possibility that several errors occur in combination. A systematic procedure to temporarily resolve these dependencies is therefore helpful to be able to narrow down the source(s) of the error employing an exclusion procedure.

A possible source of error:
A key value is read from the configuration file and is not set or not set correctly, for example in the wrong network.

Exclusion:
For simplicity, the key can simply be written directly into the code.

A possible source of error:
A function returns a wrong value or no value at all. In this case getEntranceFee().

Exclusion:
Simply set the return value statically.

Possible error source:
A change to the solidity code was made but not compiled.

Exclusion:
Compile before testing.

Back to the error message

In the error message, there is a section “FAILURES”. Here the code breaks at the point where lottery.getEntranceFee() is called. So the deployment seems to have worked. The function does not return any value at all.

Furthermore, there is a line
E brownie.exceptions.VirtualMachineError: revert
This indicates that the transaction was reverted.

Here at the latest, one should take a closer look at the Solidity contract. The fact that the above-mentioned error message does not refer to positions in the Solidity code is due to the fact that Python is already working with a compiled file and there is no reference to our .sol file.

some tooling after all

The first thing I did now is to compile the solidity code again. Only as a test. Again, no problems. Now I opened a remix window in the browser and copied the code into it and compiled it. Again no error messages. Connected the wallet, started Injected Web, copied the chainlink address for ETH/USD for Rinkeby, and deployed the contract with the data feed address I just copied. Still no error message. If I now click on the getEntranceFee button in Remix, I get “call to LotterySimple.getEntranceFee errored: execution reverted” in the terminal.

You have to realize here how many other sources of error I have already ruled out. The rest, however, means thinking.

The Solidity smart contract

Here is the function where the error is.

We have been able to narrow the search down to 5 lines of code.

Notices

  • If you look closely at the data types, there are very different forms here: uint8, uint256, int256, and the constant TICKET_PRICE_USD, which is created at the beginning of the contract with uint16.
    uint16 public TICKET_PRICE_USD;
  • Using different data types without typecasting in mathematical operations is always critical. Especially when types with small memory areas are thrown together with types that have a very large memory requirement. Namely, here uint16, which can never be larger than 65.535, and a uint256 which can be an insanely large number.

Try to fix the problem:
In this line of code
uint256 ticketPriceUsdWeiEquivalent = TICKET_PRICE_USD * 10**18;

The test then ran successfully. Of course, this is not the final solution, this is easily achieved by typecasting as follows.
uint256 ticketPriceUsdWeiEquivalent = uint256(TICKET_PRICE_USD) * 10**18;

Summary

This relatively long post has the following key points.

  1. The error messages we get in our development process are sometimes subtle, sometimes misleading, and often not very helpful.
  2. The current phase of development, for example, writing tests or deployment scripts, may reveal errors that are caused somewhere else upstream.
  3. A systematic approach is very helpful in finding bugs. One of the most important tools is to progressively isolate a bug by gradually eliminating the sources of the bug.
  4. It makes sense to create copies of the files to be processed for this process and also to create separate tests for them.
  5. Solidity is not always able to detect errors in the code before, during, or after compilation. Conversely, this means that a Solidity file without error messages is not necessarily error-free.

--

--

No responses yet