Debugging with Truffle CLI

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.

--ADVERTISEMENT--

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!