Debugging a smart contract
Note: This tutorial requires Truffle version 4.0 or newer.
A smart contract in Ethereum is just code. Unlike the "paper" contracts that you find elsewhere, this contract needs to make sense in a very precise manner.
(And that's a good thing. Imagine how much clearer real-world contracts would be if they needed to "compile"?)
If our contracts are not coded correctly, our transactions may fail, which can result in the loss of ether (in the form of gas), not to mention wasted time and effort.
Luckily, Truffle (as of version 4) has a built in debugger for stepping through your code. So when something goes wrong, you can find out exactly what it was, and fix it promptly.
In this tutorial, we will migrate a basic contract to a test blockchain, introduce some errors into it, and solve each one through the use of the built-in Truffle debugger.
A basic smart contract
One of the most basic, non-trivial, types of smart contract is a simple storage contract. (This example was adapted from the Solidity documentation.)
pragma solidity ^0.4.17;
contract SimpleStorage {
uint myVariable;
function set(uint x) public {
myVariable = x;
}
function get() constant public returns (uint) {
return myVariable;
}
}
This contract does two things:
- Allows you to set a variable (
myVariable
) to a particular integer value - Allows you to query that variable to get the selected value
This isn't a very interesting contract, but that's not the point here. We want to see what happens when things go wrong.
First, let's set up our environment.
Deploying the basic smart contract
Create a new directory where we will house our contract locally:
mkdir simple-storage cd simple-storage
Create a bare Truffle project:
truffle init
This will create directories such as
contracts/
andmigrations/
, and populate them with files we will use when we deploy our contract to the blockchain.Inside the
contracts/
directory, create a file calledStore.sol
with the following content:pragma solidity ^0.4.17; contract SimpleStorage { uint myVariable; function set(uint x) public { myVariable = x; } function get() constant public returns (uint) { return myVariable; } }
This is the contract that we will be debugging. While the full details of this file are beyond the scope of this tutorial, note that there is a contract named
SimpleStorage
that contains a numeric variablemyVariable
and two functions:set()
andget()
. The first function stores a value in that variable and the second queries that value.Inside the
migrations/
directory, create a file called2_deploy_contracts.js
and populate it with the following content:var SimpleStorage = artifacts.require("SimpleStorage"); module.exports = function(deployer) { deployer.deploy(SimpleStorage); };
This file is the directive that allows us to deploy the
SimpleStorage
contract to the blockchain.On the terminal, compile the smart contract:
truffle compile
Open a second terminal and run
truffle develop
to start a development blockchain built directly into Truffle that we can use to test our contract:truffle develop
The console will display a prompt
truffle(develop)>
. From here, unless otherwise specified, all commands will be typed on this prompt.With the develop console up and running, we can now deploy our contracts to the blockchain by running our migrations:
migrate
The response should look something like below, though the specific IDs will differ:
Running migration: 1_initial_migration.js Replacing Migrations... ... 0xe4f911d95904c808a81f28de1e70a377968608348b627a66efa60077a900fb4c Migrations: 0x3ed10fd31b3fbb2c262e6ab074dd3c684b8aa06b Saving successful migration to network... ... 0x429a40ee574664a48753a33ea0c103fc78c5ca7750961d567d518ff7a31eefda Saving artifacts... Running migration: 2_deploy_contracts.js Replacing SimpleStorage... ... 0x6783341ba67d5c0415daa647513771f14cb8a3103cc5c15dab61e86a7ab0cfd2 SimpleStorage: 0x377bbcae5327695b32a1784e0e13bedc8e078c9c Saving successful migration to network... ... 0x6e25158c01a403d33079db641cb4d46b6245fd2e9196093d9e5984e45d64a866 Saving artifacts...
Interacting with the basic smart contract
The smart contract is now deployed to a test network via truffle develop
, which launches a console against Ganache, a local development blockchain built right into Truffle.
We next want to interact with the smart contract to see how it works when working correctly. We'll interact using the truffle develop
console.
Note: If you're wondering why we didn't need to mine to get the transaction to be secured, the Truffle Develop console already takes care of that for us. If using a different network, you'll need to make sure you mine to get the transaction on the blockchain.
In the console where
truffle develop
is running, run the following command:SimpleStorage.deployed().then(function(instance){return instance.get.call();}).then(function(value){return value.toNumber()});
This command looks at the SimpleStorage contract, and then calls the
get()
function as defined inside it. It then returns the output, which is usually rendered as a string, and converts it to a number:0
This shows us that our variable,
myVariable
, is set to0
, even though we haven't set this variable to any value (yet). This is because variables with integer types are automatically populated with the value of zero in Solidity, unlike other languages where it might beNULL
orundefined
.Now let's run a transaction on our contract. We'll do this by running the
set()
function, where we can set our variable value to some other integer. Run the following command:SimpleStorage.deployed().then(function(instance){return instance.set(4);});
This sets the variable to
4
. The output shows some information about the transaction, including the transaction ID (hash), transaction receipt, and any event logs that were triggered during the course of the transaction:{ tx: '0x7f799ad56584199db36bd617b77cc1d825ff18714e80da9d2d5a0a9fff5b4d42', receipt: { transactionHash: '0x7f799ad56584199db36bd617b77cc1d825ff18714e80da9d2d5a0a9fff5b4d42', transactionIndex: 0, blockHash: '0x60adbf0523622dc1be52c627f37644ce0a343c8e7c8955b34c5a592da7d7c651', blockNumber: 5, gasUsed: 41577, cumulativeGasUsed: 41577, contractAddress: null, logs: [] }, logs: [] }
Most important to us is the transaction ID (listed here both as
tx
and astransactionHash
). We'll need to copy that value when we start to debug.Note: Your transaction IDs will likely be different from what is listed here.
To verify that the variable has changed values, run the
get()
function again:SimpleStorage.deployed().then(function(instance){return instance.get.call();}).then(function(value){return value.toNumber()});
The output should look like this:
4
Debugging errors
The above shows how the contract should work. Now, we will introduce some small errors to the contract and redeploy it. We will see how the issues present itself, and also use Truffle's built-in debug feature to fix the issues.
We will look at the following issues:
- An infinite loop
- Invalid error check
- No error, but a function isn't operating as desired
Issue #1: An infinite loop
On the Ethereum blockchain, transactions cannot be set to run forever.
A transaction can run up until its gas limit is reached. Once that happens, the transaction will error out, and an "out of gas" error will be returned.
Since gas is priced in ether, this could have real-world financial implications. So fixing an out-of-gas error is critical.
Introducing the error
An infinite loop is easy to create.
Open
Store.sol
from thecontracts/
directory in a text editor.Replace the
set()
function with the following:function set(uint x) public { while(true) { myVariable = x; } }
This function will never terminate, because of the
while(true)
conditional.
Testing the contract
The Truffle Develop console has the ability to migrate updated contracts without the need to exit and restart the console. And since the migrate
command can compile and migrate in one step, we can reset our contract on the blockchain in one step.
In the Truffle Develop console, update the contract:
migrate --reset
You will see both the compiler output and the migration output.
In order to facilitate error hunting, we will open a second console with logging. This will allow us to, for example, see transaction IDs when a transaction fails. In another terminal window, run the following command:
truffle develop --log
Leave that window for now and return to the first console.
Now we are ready to run that transaction. Run the
set()
command from above.SimpleStorage.deployed().then(function(instance){return instance.set(4);});
An error will display:
Error: VM Exception while processing transaction: out of gas
Moreover, in the console with the logs, you will see more information:
develop:testrpc eth_sendTransaction +0ms develop:testrpc +1s develop:testrpc Transaction: 0xe493340792ab92b95ac40e43dca6bc88fba7fd67191989d59ca30f79320e883f +2ms develop:testrpc Gas usage: 4712388 +11ms develop:testrpc Block Number: 6 +15ms develop:testrpc Runtime Error: out of gas +0ms develop:testrpc +16ms
With our failure and our transaction ID, we can now debug the transaction.
Debugging the issue
Truffle contains a built-in debugger. The command to launch this is debug <Transaction ID>
from the Truffle Develop console, or truffle debug <Transaction ID>
from the terminal. Let's launch this now.
In the Truffle Develop console, copy the transaction ID from the logs console and paste it as the argument in the
debug
command:debug 0xe493340792ab92b95ac40e43dca6bc88fba7fd67191989d59ca30f79320e883f
Note: Again, your transaction ID will be different from what is listed here.
You will see the following output:
Gathering transaction data...
Addresses affected:
0x377bbcae5327695b32a1784e0e13bedc8e078c9c - SimpleStorage
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
Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c:
1: pragma solidity ^0.4.17;
2:
3: contract SimpleStorage {
^^^^^^^^^^^^^^^^^^^^^^^
debug(develop:0xe4933407...)>
This is an interactive console. You can use the commands listed to interact with the code in different ways.
The most common way to interact with the code is to "step next", which steps through the code one instruction at a time. Do this by pressing
Enter
orn
:The output is as follows:
Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c: 4: uint myVariable; 5: 6: function set(uint x) public { ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Notice that the program has moved to the next instruction, located on line 6. (The carets point to the exact part of the instruction taking place.)
Press
Enter
again to step to the next instruction:Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c: 5: 6: function set(uint x) public { 7: while(true) { ^^^^^^^^^^^^
Keep pressing Enter:
Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c: 5: 6: function set(uint x) public { 7: while(true) { ^^^^ debug(develop:0xe4933407...)> Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c: 5: 6: function set(uint x) public { 7: while(true) { ^^^^^^^^^^^^ debug(develop:0xe4933407...)> Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c: 6: function set(uint x) public { 7: while(true) { 8: myVariable = x; ^ debug(develop:0xe4933407...)> Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c: 6: function set(uint x) public { 7: while(true) { 8: myVariable = x; ^^^^^^^^^^ debug(develop:0xe4933407...)> Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c: 6: function set(uint x) public { 7: while(true) { 8: myVariable = x; ^^^^^^^^^^^^^^ debug(develop:0xe4933407...)> Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c: 5: 6: function set(uint x) public { 7: while(true) { ^^^^^^^^^^^^
Notice that the steps eventually repeat. In fact, pressing
Enter
over and over will repeat those transactions forever (or at least until the transaction runs out of gas). This tells you where the problem is.Type
q
to exit the debugger.
Issue #2: An invalid error check
Smart contracts can use statements like assert()
to ensure that certain conditions are met. These can conflict with the state of the contract in ways that are irreconcilable.
Here we will introduce such a condition, and then see how the debugger can find it.
Introducing the error
Open
Store.sol
again.Replace the
set()
function with the following:function set(uint x) public { assert(x == 0); myVariable = x; }
This is the same as the original version, but with an
assert()
function added, testing to make sure thatx == 0
. This will be fine until we set that value to something else, and then we'll have a problem.
Testing the contract
Just as before, we'll reset the contract on the blockchain.
In the Truffle Develop console, reset the contract on the blockchain to its initially deployed state:
migrate --reset
Now we are ready to test the new transaction. Run the same command as above:
SimpleStorage.deployed().then(function(instance){return instance.set(4);});
You will see an error:
Error: VM Exception while processing transaction: invalid opcode
This means that we have a problem on our hands.
In the log window, note the transaction ID with that error.
Debugging the issue
Copy the transaction ID and use it as an argument to the
debug
command:debug 0xe493340792ab92b95ac40e43dca6bc88fba7fd67191989d59ca30f79320e883f
Note: Again, your transaction ID will be different from what is listed here.
Now we are back in the debugger:
Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c:
1: pragma solidity ^0.4.17;
2:
3: contract SimpleStorage {
^^^^^^^^^^^^^^^^^^^^^^^
debug(develop:0xe4933407...)>
Press
Enter
a few times to step through the code. Eventually, the debugger will halt with an error message:Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c: 5: 6: function set(uint x) public { 7: assert(x == 0); ^^^^^^^^^^^^^^ debug(develop:0x7e060037...)> Transaction halted with a RUNTIME ERROR. This is likely due to an intentional halting expression, like assert(), require() or revert(). It can also be due to out-of-gas exceptions. Please inspect your transaction parameters and contract code to determine the meaning of this error.
It is this last event that is triggering the error. You can see that it is the
assert()
that is to blame.
Issue #3: A function isn't operating as desired
Sometimes, an error isn't a true error, in that it doesn't cause a problem at runtime, but instead is just doing something that you don't intend it to do.
Take for example an event that would run if our variable was odd and another event that would run if our variable was even. If we accidentally swapped this conditional so that the opposite function would run, it wouldn't cause an error; nevertheless, the contract would act unexpectedly.
Once again, we can use the debugger to see where things go wrong.
Introducing the error
Open
Store.sol
again.Replace the
set()
function with the following:event Odd(); event Even(); function set(uint x) public { myVariable = x; if (x % 2 == 0) { Odd(); } else { Even(); } }
This code introduces two dummy events,
Odd()
andEven()
that are trigged based on a conditional that checks whetherx
is divisible by2
.But notice that we have the results flipped. If
x
is divisible by2
, theOdd()
event will run.
Testing the contract
Just as before, we'll reset the contract on the blockchain.
In the Truffle Develop console, update the contract:
migrate --reset
You will see both the compiler output and the migration output.
Now we are ready to test the new transaction. Run the same command as above:
SimpleStorage.deployed().then(function(instance){return instance.set(4);});
Note that there is no error here. The response is given as a transaction ID with details:
{ tx: '0x7f799ad56584199db36bd617b77cc1d825ff18714e80da9d2d5a0a9fff5b4d42', receipt: { transactionHash: '0x7f799ad56584199db36bd617b77cc1d825ff18714e80da9d2d5a0a9fff5b4d42', transactionIndex: 0, blockHash: '0x08d7c35904e4a93298ed5be862227fcf18383fec374759202cf9e513b390956f', blockNumber: 5, gasUsed: 42404, cumulativeGasUsed: 42404, contractAddress: null, logs: [ [Object] ] }, logs: [ { logIndex: 0, transactionIndex: 0, transactionHash: '0x7f799ad56584199db36bd617b77cc1d825ff18714e80da9d2d5a0a9fff5b4d42', blockHash: '0x08d7c35904e4a93298ed5be862227fcf18383fec374759202cf9e513b390956f', blockNumber: 5, address: '0x377bbcae5327695b32a1784e0e13bedc8e078c9c', type: 'mined', event: 'Odd', args: {} } ] }
But notice the logs of the transaction show the event
Odd
. That's wrong, and so our job is to find out why that's being invoked.
Debugging the contract
Copy that transaction ID and use it as an argument with the
debug
command:debug 0x7f799ad56584199db36bd617b77cc1d825ff18714e80da9d2d5a0a9fff5b4d42
Note: Again, your transaction ID will be different from what is listed here.
You will enter the debugger as before.
Press
Enter
multiple times to cycle through the steps. Eventually you will see that the conditional leads to theOdd()
event:Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c: 10: function set(uint x) public { 11: myVariable = x; 12: if (x % 2 == 0) { ^^^^^^^^^^^^^^^^ debug(develop:0x7f799ad5...)> Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c: 11: myVariable = x; 12: if (x % 2 == 0) { 13: Odd(); ^^^^^ debug(develop:0x7f799ad5...)>
The problem is revealed. The conditional is leading to the wrong event.
Conclusion
With the ability to debug your contracts directly within Truffle, you have even more power at your hands to make your smart contracts rock-solid and ready to deploy. Make sure to read more about Truffle Develop console and the debugger in the docs. If you have any questions, please join our Gitter channel and ask there.
Happy debugging!