Debugging with Truffle CLI

Mislav Javor
Mislav Javor
Share

Debuggers have been crucial software development tools for over thirty years.

A modern debugger enables us to:

  • run the code line-by-line
  • set breakpoints in the code
  • put conditions on the breakpoints
  • evaluate expressions during runtime.

Most modern debuggers are also highly integrated into development environments of languages they are serving. They enable setting breakpoints by clicking on line numbers, evaluating expressions by hovering over variables, writing conditional breakpoints in the code comments … and so on.

So what is the state of Solidity smart contract debugging and debuggers?

Solidity Debugger

As with most blockchain things, we’re still in the infancy stage. The basic debuggers are available (and advancing at a rapid pace), but no editor integrations are here yet, and the debugger heavily relies on the framework of choice.

In this article, we’ll be exploring the Solidity debugger bundled with the Truffle Suite.

Getting Started

First, we’ll need to install all the required tools. Luckily for us, the Truffle framework is very well integrated, so we’ll just need to install it.

First, install Node.js and NPM. After you’ve installed Node, you can verify that it’s installed by checking the version of the tool like this:

➜  ~ node -v
v10.2.1
➜  ~ npm -v
5.6.0

If your Node is up and running, let’s install the Truffle framework. This is made simple enough by the usage of npm, so just run this:

npm install -g truffle

You can check whether the install was successful by checking the version:

truffle version
Truffle v4.1.11 (core: 4.1.11)
Solidity v0.4.24 (solc-js)

Setting Up the Project

Now that you have Truffle all set up, let’s create a new (empty) Truffle project. Open your terminal, position yourself into a desired directory and run truffle init. The output should be similar to this:

truffle init
Downloading...
Unpacking...
Setting up...
Unbox successful. Sweet!

Commands:

  Compile:        truffle compile
  Migrate:        truffle migrate
  Test contracts: truffle test

After you’ve done this, you should have a contract structure similar to this:

.
├── contracts
│   └── Migrations.sol
├── migrations
│   └── 1_initial_migration.js
├── test
├── truffle-config.js
└── truffle.js

Now open the truffle.js file and put the following data into it:

module.exports = {
  networks: {
      development: {
          port: 9545,
          host: "127.0.0.1",
          network_id: "*"
      }
  }
};

Save the file and run truffle develop. You should get an output similar to this:

truffle develop
Truffle Develop started at http://127.0.0.1:9545/

Accounts:
(0) 0x627306090abab3a6e1400e9345bc60c78a8bef57
(1) 0xf17f52151ebef6c7334fad080c5704d77216b732
(2) 0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef
(3) 0x821aea9a577a9b44299b9c15c88cf3087f3b5544
(4) 0x0d1d4e623d10f9fba5db95830f7d3839406c6af2
(5) 0x2932b7a2355d6fecc4b5c0b6bd44cc31df247a2e
(6) 0x2191ef87e392377ec08e7c08eb105ef5448eced5
(7) 0x0f4f2ac550a1b4e2280d04c21cea7ebd822934b5
(8) 0x6330a553fc93768f612722bb8c2ec78ac90b3bbc
(9) 0x5aeda56215b167893e80b4fe645ba6d5bab767de

Private Keys:
(0) c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3
(1) ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f
(2) 0dbbe8e4ae425a6d2687f1a7e3ba17bc98c673636790f1b8ad91193c05875ef1
(3) c88b703fb08cbea894b6aeff5a544fb92e78a18e19814cd85da83b71f772aa6c
(4) 388c684f0ba1ef5017716adb5d21a053ea8e90277d0868337519f97bede61418
(5) 659cbb0e2411a44db63778987b1e22153c086a95eb6b18bdf89de078917abc63
(6) 82d052c865f5763aad42add438569276c00d3d88a2d062d36b2bae914d58b8c8
(7) aa3680d5d48a8283413f7a108367c7299ca73f553735860a87b08f39395618b7
(8) 0f62d96d6675f32685bbdb8ac13cda7c23436f63efbb9d07700d8669ff12b7c4
(9) 8d5366123cb560bb606379f90a0bfd4769eecc0557f1b362dcae9012b548b1e5

Mnemonic: candy maple cake sugar pudding cream honey rich smooth crumble sweet treat

⚠️  Important ⚠️  : This mnemonic was created for you by Truffle. It is not secure.
Ensure you do not use it on production blockchains, or else you risk losing funds.

This started an instance of the Truffle development blockchain backed by ganache-cli (former TestRPC).

Writing and Deploying the Contract

In the contracts directory, make a file called Storage.sol. In that file, put the following code:

pragma solidity ^0.4.23;


contract Storage {

    uint[] private _numberStorage;

    event AddedNewNumber(uint position);

    function addNumber(uint newNumber) public returns (uint) {
        _numberStorage.push(newNumber);

        uint numberPosition = _numberStorage.length;

        emit AddedNewNumber(numberPosition);
        return numberPosition;
    }

    function getNumber(uint position) public constant returns (uint) {
        return _numberStorage[position];
    }

}

After you’re done with this, your file structure should look like this:

├── contracts
│   ├── Migrations.sol
│   └── Storage.sol
├── migrations
│   └── 1_initial_migration.js
├── test
├── truffle-config.js
└── truffle.js

In the migrations directory, make a new file called 2_deploy_migrations.js and put the following code into it:

var Storage = artifacts.require("./Storage.sol");

module.exports = function(deployer) {

    deployer.deploy(Storage);

}

This code defines how Truffle will migrate our project to the blockchain.

Now open a new tab in the terminal (leaving the truffle develop running) and run truffle migrate. This will compile and migrate your contracts to the development blockchain. You should get an output like this:

Using network 'development'.

Running migration: 1_initial_migration.js
  Deploying Migrations...
  ... 0x819678a9812313714a27b52c30f065544a331ec5c79ec6c251bc97cd09398d08
  Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0
Saving successful migration to network...
  ... 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956
Saving artifacts...

Now write truffle console. This will open up an interactive console for you to test out your contracts. In the console do the following:

~> Storage.deployed().then((i) => { iStorage = i }) // Store the contract instance into the iStorage variable

~> iStorage.AddedNewNumber({}).watch((err, res) => { console.log("NUMBER POSITION: " + res.args.position.toNumber()) }) // Subscribe to the added new number event

Filter {
  requestManager:
   RequestManager {
     provider: Provider { provider: [HttpProvider] },
     polls: {},
     timeout: null },
  options:
   { topics:
      [ '0x197006a61de03a2f3b4de7f4c4fab6e30ebedef7c1a42d716b2140f184c718b7' ],
     from: undefined,
     to: undefined,
     address: '0xdda6327139485221633a1fcd65f4ac932e60a2e1',
     fromBlock: undefined,
     toBlock: undefined },
  implementation:
   { newFilter:
      { [Function: send] request: [Function: bound ], call: [Function: newFilterCall] },
     uninstallFilter:
      { [Function: send] request: [Function: bound ], call: 'eth_uninstallFilter' },
     getLogs:
      { [Function: send] request: [Function: bound ], call: 'eth_getFilterLogs' },
     poll:
      { [Function: send] request: [Function: bound ], call: 'eth_getFilterChanges' } },
  filterId: null,
  callbacks: [ [Function] ],
  getLogsCallbacks: [],
  pollFilters: [],
  formatter: [Function: bound ] }

~> iStorage.addNumber(13) // Add a new number

{ tx:
   '0xad3f82a6a6cec39dff802f2f16e73bbbc8eff3b68c2ac4da4c371a4c84345a4f',
  receipt:
   { transactionHash:
      '0xad3f82a6a6cec39dff802f2f16e73bbbc8eff3b68c2ac4da4c371a4c84345a4f',
     transactionIndex: 0,
     blockHash:
      '0x464bc0075036cf95484dec165f0248fb0a7db929d14068a312076be14d43d1fe',
     blockNumber: 5,
     gasUsed: 63362,
     cumulativeGasUsed: 63362,
     contractAddress: null,
     logs: [ [Object] ],
     status: '0x01',
     logsBloom:
      '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000004000000000000000000000000000000000000000000000000000000000000000000010000000000000' },
  logs:
   [ { logIndex: 0,
       transactionIndex: 0,
       transactionHash:
        '0xad3f82a6a6cec39dff802f2f16e73bbbc8eff3b68c2ac4da4c371a4c84345a4f',
       blockHash:
        '0x464bc0075036cf95484dec165f0248fb0a7db929d14068a312076be14d43d1fe',
       blockNumber: 5,
       address: '0x345ca3e014aaf5dca488057592ee47305d9b3e10',
       type: 'mined',
       event: 'AddedNewNumber',
       args: [Object] } ] }

After you’ve run the iStorage.addNumber(...) function, the event we subscribed to should have been fired. If this is the case, the output should contain something like this:

truffle(development)> NUMBER POSITION: 1

Now let’s try fetching the number from the position to which we stored it:

~> iStorage.getNumber(1)
iStorage.getNumber(1)
Error: VM Exception while processing transaction: invalid opcode
    at XMLHttpRequest._onHttpResponseEnd (/usr/local/lib/node_modules/truffle/build/webpack:/~/xhr2/lib/xhr2.js:509:1)
    at XMLHttpRequest._setReadyState (/usr/local/lib/node_modules/truffle/build/webpack:/~/xhr2/lib/xhr2.js:354:1)
    at XMLHttpRequestEventTarget.dispatchEvent (/usr/local/lib/node_modules/truffle/build/webpack:/~/xhr2/lib/xhr2.js:64:1)
    at XMLHttpRequest.request.onreadystatechange (/usr/local/lib/node_modules/truffle/build/webpack:/~/web3/lib/web3/httpprovider.js:128:1)
    at /usr/local/lib/node_modules/truffle/build/webpack:/~/truffle-provider/wrapper.js:134:1
    at /usr/local/lib/node_modules/truffle/build/webpack:/~/web3/lib/web3/requestmanager.js:86:1
    at Object.InvalidResponse (/usr/local/lib/node_modules/truffle/build/webpack:/~/web3/lib/web3/errors.js:38:1)

We get an error! How convenient: it seems we can now use the debugger. It’s almost as if it was planned!

Using the Debugger

In order to debug the transactions, you need to find the transaction hash of the transaction you wish to debug. Copy the transaction hash and, in the terminal, input it in the form:

truffle debug tx_hash

We’ll debug the function which adds the number to the storage. The tx hash will be different for you, but the overall form will be similar to this. And the output should be the same:

truffle debug 0x34d26b8bcf01f23dfbef0de28384623ca3ed3e3e7fe28f1a0968d363cf38765f
Compiling ./contracts/Migrations.sol...
Compiling ./contracts/Storage.sol...

Gathering transaction data...

Addresses affected:
 0x345ca3e014aaf5dca488057592ee47305d9b3e10 - Storage

Commands:
(enter) last command entered (step next)
(o) step over, (i) step into, (u) step out, (n) step next
(;) step instruction, (p) print instruction, (h) print this help, (q) quit
(b) toggle breakpoint, (c) continue until breakpoint
(+) add watch expression (`+:<expr>`), (-) remove watch expression (-:<expr>)
(?) list existing watch expressions
(v) print variables and values, (:) evaluate expression - see `v`


Storage.sol:

2:
3:
4: contract Storage {
   ^^^^^^^^^^^^^^^^^^

debug(development:0x34d26b8b...)>

Here we can see the debug console. It’s much more primitive than the modern debuggers most of the developers are used to, but it has all the required functionality. Let’s explore the functionality.

In the console, run the following series of commands:

debug(development:0x34d26b8b...)> o

Storage.sol:

 8:     event AddedNewNumber(uint position);
 9:
10:     function addNumber(uint newNumber) public returns (uint) {
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

By typing o, we’ve stepped over the function line — meaning we didn’t enter the body of the function we called.

Now we’re positioned at line 10 — or the header of the function for adding a number. Let’s go further. Type o again.

Storage.sol:

 9:
10:     function addNumber(uint newNumber) public returns (uint) {
11:         _numberStorage.push(newNumber);
            ^^^^^^^^^^^^^^

Now we’re pushing the number to the array. Type o again.

Storage.sol:

11:         _numberStorage.push(newNumber);
12:
13:         uint numberPosition = _numberStorage.length;

We’ve pushed the number and are getting the position of the number in the array. Press o again.

Storage.sol:

13:         uint numberPosition = _numberStorage.length;
14:
15:         emit AddedNewNumber(numberPosition);
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

We’re at the line emitting the position. Now instead of pressing o, let’s inspect our variables and try getting the number back from the array. We can use one of two ways. The first, more generic one is just typing v and inspecting all variables available from the function context.

Do it by typing v. The output should be this:

numberPosition: 1
     newNumber: 12
              : 189
_numberStorage: [ 12 ]

The other way to do it is more specific, and we can do it by typing :<variable_name>. Do it by typing :numberPosition. You should get 1 as output.

Now let’s try evaluating an expression. You can do this with the : construct also.

Let’s try getting our number back with the :_numberStorage[numberPosition]:

debug(development:0x34d26b8b...)> :_numberStorage[numberPosition]
undefined

By running it, we get the output undefined. This means that there is no number there.

To all developers, the error should be reasonably self-evident. We’ve tried accessing the first element at the position one. Since in Solidity, as in most languages, the arrays are zero indexed, we need to access it with the number zero.

Press q to exit the debugger and let’s fix the code. In your Storage.sol change this:

uint numberPosition = _numberStorage.length;

to this:

uint numberPosition = _numberStorage.length - 1;

Now run truffle migrate again, and after it’s finished, run truffle console. In the console, run the commands again:

~> Storage.deployed().then((i) => { iStorage = i})
~> iStorage.AddedNewNumber({}).watch((err, res) => { console.log("NUMBER POSITION: " + res.args.position.toNumber()) });
~> iStorage.addNumber(12);
~> output: "NUMBER POSITION: 0"
~> iStorage.getNumber(0)
~> iStorage.getNumber(0).then((res) => { console.log(res.toNumber()) } )
~> output: 12

You’ve successfully deployed and debugged the smart contract!

Frequently Asked Questions (FAQs) about Debugging with Truffle CLI

What is Truffle CLI and why is it important for debugging?

Truffle CLI, or Command Line Interface, is a powerful tool used in the Truffle Suite, a development environment for Ethereum. It provides developers with a console to execute Truffle commands directly, making it easier to compile, migrate, test, and debug smart contracts. Debugging is a crucial part of any development process, and Truffle CLI simplifies this process by providing a set of commands that can be used to identify and fix issues in your smart contracts.

How do I install Truffle CLI?

To install Truffle CLI, you need to have Node.js and npm installed on your system. Once these prerequisites are met, you can install Truffle CLI by running the command ‘npm install -g truffle’ in your terminal. This will install Truffle globally on your system, allowing you to access the Truffle commands from any directory.

How can I use Truffle CLI to debug my smart contracts?

Truffle CLI provides a command ‘truffle debug’ that can be used to debug your smart contracts. This command starts a debugging session and provides you with a prompt where you can enter various commands to step through your code, inspect variables, and understand the execution flow of your contract.

What are some common issues I might encounter while debugging with Truffle CLI?

Some common issues you might encounter while debugging with Truffle CLI include incorrect contract addresses, issues with contract compilation, and problems with the Ethereum network. These issues can usually be resolved by checking your contract addresses, recompiling your contracts, or switching to a different Ethereum network.

How can I resolve issues with contract addresses while debugging with Truffle CLI?

If you encounter issues with contract addresses while debugging with Truffle CLI, you can use the ‘truffle networks’ command to list all the networks your contracts are deployed on, along with their addresses. You can then verify if the addresses match with the ones you are using in your code.

How can I resolve issues with contract compilation while debugging with Truffle CLI?

If you encounter issues with contract compilation while debugging with Truffle CLI, you can use the ‘truffle compile’ command to recompile your contracts. This command will also display any compilation errors, which can help you identify and fix issues in your code.

How can I resolve issues with the Ethereum network while debugging with Truffle CLI?

If you encounter issues with the Ethereum network while debugging with Truffle CLI, you can use the ‘truffle networks –clean’ command to remove all network artifacts. You can then redeploy your contracts on the network.

Can I use Truffle CLI to debug contracts deployed on public networks?

Yes, you can use Truffle CLI to debug contracts deployed on public networks. However, you need to have the transaction hash of the transaction you want to debug. You can then use the ‘truffle debug ‘ command to start a debugging session for that transaction.

Can I use Truffle CLI to debug contracts that are not written in Solidity?

Truffle CLI is primarily designed to debug contracts written in Solidity. However, it also supports contracts written in Vyper, although the debugging experience might not be as smooth as with Solidity contracts.

Can I use Truffle CLI to debug contracts that use libraries or other contracts?

Yes, you can use Truffle CLI to debug contracts that use libraries or other contracts. However, you need to have the source code of the libraries or contracts, and they need to be compiled with the same compiler version as your main contract.