Variable Inspection: Going Deeper with the Truffle Solidity Debugger

Variable inspection: Going deeper with the Truffle Solidity debugger

The integrated Solidity debugger in Truffle is a powerful tool for inspecting your contracts.

When initially released, the debugger had the ability to to step through Solidity code, but that was it.

But development has continued apace, and new functionality has been added to make your contract inspection more powerful. Specifically, you now have the ability to perform variable inspection on your contracts. With this, you can know exactly the state of your variables at every given point in the instruction list, giving you a much greater ability to truly inhabit the current state of your contracts, and making debugging a breeze.

In this tutorial, we're going to take a look at a simple contract and inspect it using the Truffle Solidity debugger. We'll investigate three scenarios:

A working contract

A working contract with unexpected output

A broken contract

A basic smart contract

The Fibonacci sequence is an integer sequence where each successive number in the sequence is the sum of the two previous numbers.

With the first two numbers set to 1, you can determine every number in the sequence through iteration.

The Fibonacci sequence is related to the "golden ratio", which is found in certain areas of nature, such as governing the the arrangement of leaves on branches and petals on flowers.

First, we see the standard pragma declaration, which states that the contract s compatible with any 0.4.x version of Solidity newer than 0.4.22.

The contract name is called Fibonacci.

We're defining an array of integers called fibseries. This will house our Fibonacci series. Note that the variable declaration is happening outside of any function, and therefore the array will be saved in storage (instead of memory), provoking a transaction to occur when the contract is run.

The function is called generateFib and takes a single argument, which is the number of integers in the sequence to generate.

The next two commands add an element each to the array via the .push() method. As we know our fibseries array is defined in storage, but size isn't known until runtime, the .push() method is used to add entries to (the end of) our array. This starts the sequence with the number 1 twice.

The for loop iterates through the rest of the array (as determined by the integer n) filling each entry with the appropriate value.

Inside the migrations/ directory of your project, create a file called 2_deploy_contracts.js and populate it with the following content:

This command uses JavaScript promises. Specifically, the command says that given a deployed version of the Fibonacci contract, run an instance of that contract, and then run the generateFib function from that contract, passing it the argument 10.

Run the above command. A transaction will be created on the blockchain because our array that holds the generated Fibonacci sequence is in storage. Because of this, the output of the console will be the transaction details, which will look similar to this:

The important part of this output for us is the transaction ID (listed as the value of tx:). Because even though we've run our function, we don't actually know what happened. Did it work correctly? Did something unexpected happen? We'll need the Truffle Solidity debugger to find out.

Note: Your transaction ID will be different from the above.

Debugging a transaction

You can debug a transaction in the Truffle console by typing debug <transactionID>. We'll now do just that.

Type debug and then the value of tx: found in your transaction details. For example:

The debugger names the address of the contract in question that is related to the transaction, as well as the name of that contract. In our case, we're only dealing with a single contract, though if our transaction dealt with multiple contracts, all addresses would be shown.

A full list of commands for the debugger is shown. Many of these mirror other code debuggers. We'll use a few of these throughout the tutorial, but if you ever want to see the list again, type h.

The debugger starts at the first instruction of the transaction, and shows you the relevant code for that instruction, highlighted with carets (^^^).

Type n and <Enter> to step next. This will move to the next instruction:

6:
7: // n = how many in the series to return
8: function generateFib(uint n) public {
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
debug(development:0xf47f01da...)>

We've seen in the previous tutorial on debugging how you can step through the instructions to debug your contract. But here, we have an additional concern, which is that we don't actually know the outcome of our function call; we want to know what a variable is set to. You can view the state of all known variables by pressing v:

fibseries: []
debug(development:0xf47f01da...)>

Here we see one empty array. The arry fibseries will have the n numbers in the Fibonacci sequence.

Press Enter to repeat the last command, and step next to the next instruction:

This instruction moves on to the initial seeding of the fibseries variable.

Press v to see the current state of the variables:

i: 0
n: 10
fibseries: []

Now we see three variables, our fibseries array and two others:

i is an index variable, used to determine the location of the next number in the sequence

n is the integer we passed to the function (10 in this case) indicating the number of entries in our Fibonacci sequence

Here we see that n has been passed the value from the contract, while i hasn't received its value from the for loop yet, since we haven't gotten to that part of the function.

Press Enter to step next to the next instruction:

9:
10: // set 1st and 2nd entries
11: fibseries.push(1);
^

It's rather tedious to constantly press v after every state change. Thankfully, you can set certain variables to be "watched", so that they will display after every movement in the debugger. The syntax to watch a variable is +:<variable>. So let's watch the variables we care about (i and fibseries) with the following two commands:

+:i
+:fibseries

After each command, the current value of the variable will display. But once done, we'll get a persistent display of the variables and their values.

Because the debugger steps through each instruction one at a time, it's going to take a long time to see results if we don't pick
up our pace. Luckily, the debugger can "step over", which steps over the current line, moving to the next line, as long as it's at the same function depth. This will allow us to make progress much more quickly. So type o to step over the current instruction set. The output will be:

Now that we have two entries in our sequence, we've moved into our for loop.

Continue pressing Enter and watching the variable output. You should notice that when i gets to 10, the for loop ends (as the loop terminates at i < n). The final array is:

[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

It can be verified that this is the correct first ten entries in the Fibonacci sequence.

Debugging unexpected output

Our contract as we have created it is working as expected. That's great, but we can't always get so lucky. Sometimes a contract appears to work fine (no errors are thrown) but the output is not what you'd expect. Let's examine that here.

It's amazing what switching a plus sign for a minus sign can do. Instead of the Fibonacci sequence:

F[1] = 1
F[2] = 1
F[n] = F[n-1] + F[n-2]

Let's switch the plus for a minus sign:

F[1] = 1
F[2] = 1
F[n] = F[n-1] - F[n-2]

(I don't know if this series has a name, so we'll just call it the "Trufflenacci" sequence.)

Let's edit our contract and see what happens.

Open the Fibonacci.sol file. Edit the definition of our fibseries[] array to be of type uint8[].

uint8[] fibseries;

Note: A uint8 is an unsigned integer with 8 bits, with a maximum value of 2^8 = 255. Compare this to a uint, which is a 256 bit number with a maximum value of 2^256.

Edit the for loop so that the terms are subtracted instead of added:

fibseries.push(fibseries[i-1] - fibseries[i-2]);

Save this file.

Back on the console, recompile the contract.

compile --all

Note: The --all flag with force recompile all of the contracts.

Remigrate the contract to the blockchain.

migrate --reset

We can now run the same command as before, which will generate a new transaction:

Something seems off here. Obviously, if you were expecting the Fibonacci series, by this point you would realize that you had veered far off course.

But the jump from 0 to 255 should be suspicious too. And in fact, what we're seeing is a buffer underflow, a situation where we have an unsigned integer, which can hold only positive values, being set to less than zero. Specifically, that fourth value is defined as the difference between the previous two elements, so 0 - 1 = -1. Our type is uint8[] which means that each integer in the array is defined by 8 bits, so its maximum value is 2^8 - 1 = 255. Subtract one from zero, and the value "wraps around" to its maximum value.

Assuming this was the series we wanted (and that our minus sign was correct), the way to change this is to change the definition of our fibseries array from uint8[] to int8[]. That would make values "signed integers" and able to accept negative values, giving you an array that would look like this instead:

[ 1, 1, 0, -1 ]

Debugging errors

Sometimes, contracts have errors in them. They may compile just fine, but when you go to use them, problems arise.

This is one of the reasons to use Ganache over a public testnet: you can easily redeploy a contract without any penalty in time or ether.

So we're going to introduce a small error, a misnumbering in our for loop that will cause it to have an invalid index.

Note that the contract is failing at the point where it tries to determine the value of fibseries[i-2] But in our debugger, we know that the current value of i is 1, so i-2 is going to be -1 (or actually 2^256 - 1, due to the buffer underflow issue we talked about above, and with i defined as a uint). Since either one of those values are invalid (you can't have a negative index, and the value at index 2^256 - 1 is clearly not defined), the contract halts with an error.