Console.log Alternative for Brownie
Smart contract development is still a new field, and the development tools are still not up to par with more traditional forms of web development. However, frameworks such as Hardhat, Foundry, Brownie and Truffle provide some decent options for troubleshooting bugs in Solidity.
One of the most useful tools for debugging Solidity code is the console.log feature of Hardhat. The console.log feature lets us log arbitrary information to the console when testing our Solidity contracts. Unfortunately, console.log is not available in all Solidity development frameworks yet including Brownie. At the time of writing, there is a GitHub issue open for this, but it has not yet been worked on.
We’re going to demonstrate an easy alternative to using console.log when debugging code with Brownie. We’ll be emitting events with the messages we would like to log to the console. It will be easiest to show this using a simple example. The completed code for the example can be seen on GitHub, but we’ll go over it step by step here.
Let’s consider the contract below.
// SPDX-License-Identifier: MIT pragma solidity 0.8.14; contract MyContract { uint public total; constructor(){ total = 0; } function addSix() public { addOne(); addTwo(); addThree(); } function addOne() private{ total += 1; } function addTwo() private{ total += 1; } function addThree() private{ total += 3; } }
The contract has a publicly accessible method which will add six to the total state variable. The method calls three private methods addOne, addTwo, and addThree. One of these methods has a bug in it. Let’s confirm this by writing a test with Brownie:
from brownie import accounts, MyContract def test_addSix(): myContract = accounts[0].deploy(MyContract) initialTotal = myContract.total() assert initialTotal == 0 myContract.addSix() assert myContract.total() == 6
When we run this test using the brownie test command, it will fail with a message like AssertionError: assert 5 == 6. Instead of adding 6 to the total, our method added 5. We’d like to find out which of our three private methods is causing the problem. If we were using Hardhat, we could do this easily using console.log as follows:
import "hardhat/console.sol"; contract MyContract { uint public total; constructor(){ total = 0; } function addSix() public { //log the value of total after each method runs console.log(total); addOne(); console.log(total); addTwo(); console.log(total); addThree(); console.log(total); } function addOne() private{ total += 1; } function addTwo() private{ total += 1; } function addThree() private{ total += 3; } }
However, since we’re using Brownie and the console.log feature is not available yet, we have to use a different strategy. We can do this by declaring a Log event which has a message parameter and a data parameter. We can then print the Log events in the Brownie test from the transaction object.
Let’s start with declaring and emitting the Log event in our contract:
// SPDX-License-Identifier: MIT pragma solidity 0.8.14; contract MyContract { //declare log event event Log(string message, uint data); uint public total; constructor(){ total = 0; } function addSix() public { //emit a Log event after each method emit Log("before changes", total); addOne(); emit Log("added one", total); addTwo(); emit Log("added two", total); addThree(); emit Log("added three", total); } function addOne() private{ total += 1; } function addTwo() private{ total += 1; } function addThree() private{ total += 3; } }
We also have to make an adjustment to our test to log the events to the console:
from brownie import accounts, MyContract def test_addSix(): myContract = accounts[0].deploy(MyContract) initialTotal = myContract.total() assert initialTotal == 0 # store the transaction object in the variable tx tx = myContract.addSix() # print the events to the console print(tx.events) assert myContract.total() == 6
Now when we run the test with the brownie test command, we will see something like this in the console output: {‘Log’: [OrderedDict([(‘message’, ‘before changes’), (‘data’, 0)]), OrderedDict([(‘message’, ‘added one’), (‘data’, 1)]), OrderedDict([(‘message’, ‘added two’), (‘data’, 2)]), OrderedDict([(‘message’, ‘added three’), (‘data’, 5)])]}
Let’s format that message to make it easier to read:
{'Log': [OrderedDict([ ('message', 'before changes'), ('data', 0) ]), OrderedDict([ ('message', 'added one'), ('data', 1) ]), OrderedDict([ ('message', 'added two'), ('data',2) ]), OrderedDict([ ('message', 'added three'), ('data',5) ]) ] }
The above console output helps us to see that addTwo is the problematic method. We can see that after the addTwo method has run, the total state variable has increased from 1 to 2. The expected behaviour would have been to increase from 1 to 3. Upon closer inspection of the addTwo method, we can see that it is adding 1 to the total instead of 2!
function addTwo() private{ total += 1; // Adding 1 instead of 2 }
Although this example was a trivial case, we can probably imagine a case where the methods addOne, addTwo and addThree have a much more complicated implementation. In these cases, this technique can be quite helpful. Once we’ve finished troubleshooting and fixing the bug, we should remove the Log event from the contract.