Skip to content

Debugging Smart Contracts with Truffle

In this lab you will learn two things:

  1. Debugging Smart Contracts while you develop
  2. Debugging Smart Contract after they are deployed.

Those are two very different things. While you develop, you might iterate over the code. If you come from JavaScript, you probably know it, something isn't working. Next best thing is just put a console.log everywhere to figure out why something isn't working as expected. If you come from more sophisticated languages, you might start a step-by-step debugger with breakpoints.

But what are your options in Solidity?

Debugging During Smart Contract Development

While developing, especially complex contracts, you sometimes run into unexpected states. Let's simulate this:

Lets say you want to extend the ERC721 contract to return a Token URI based on the Token-ID. The Token ID is numeric, and as you remember, we set the first token to "spacebear_1.json". So it could just take the token id and concat a few strings, right?

Let's try it with this contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract Spacebear is ERC721, Ownable {
    using Counters for Counters.Counter;

    Counters.Counter private _tokenIdCounter;

    constructor() ERC721("Spacebear", "SBR") {}

    function _baseURI() internal pure override returns (string memory) {
        return "https://ethereum-blockchain-developer.com/2022-06-nft-truffle-hardhat-foundry/nftdata/";
    }

    function safeMint(address to) public onlyOwner {
        uint256 tokenId = _tokenIdCounter.current();
        _tokenIdCounter.increment();
        _safeMint(to, tokenId);
    }

    function buyToken() public payable {
        uint256 tokenId = _tokenIdCounter.current();
        require(msg.value == tokenId * 0.1 ether, "Not enough funds sent");

        _tokenIdCounter.increment();
        _safeMint(msg.sender, tokenId);
    }

    // The following functions are overrides required by Solidity.

    function _burn(uint256 tokenId) internal override(ERC721) {
        super._burn(tokenId);
    }

    function tokenURI(uint256 tokenId)
        public
        pure
        override(ERC721)
        returns (string memory)
    {
        return string(abi.encodePacked(_baseURI(),"_",tokenId+1,".json"));
    }
}

It has gotten considerably smaller, because now we don't need to give it a token URI anymore on safeMint. But it also has a buyToken function, which get 0.1 ether for the first token, 0.2 ether for the second token, etc. But does it really?

Let's give it a try...

Start ganache in one terminal

ganache

and connect to it with truffle from the other one:

truffle console --network ganache

then deploy the contract and create a new instance:

migrate

then try the contract:

const spacebearInstance = await Spacebear.deployed();
await spacebearInstance.buyToken({value: web3.utils.toWei("0.1","ether")});

And it throws an error!! WHY?!

Error in Truffle Smart Contract
Error in Truffle Smart Contract

If there was just a method to debug this... Turns out there is.

By using console.log, very similar from the console.log from JavaScript...

Close the truffle console and install the right package:

npm install @ganache/console.log

Then change the source code to:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

import "@ganache/console.log/console.sol";


contract Spacebear is ERC721, Ownable {
    using Counters for Counters.Counter;

    Counters.Counter private _tokenIdCounter;

    constructor() ERC721("Spacebear", "SBR") {}

    function _baseURI() internal pure override returns (string memory) {
        return "https://ethereum-blockchain-developer.com/2022-06-nft-truffle-hardhat-foundry/nftdata/";
    }

    function safeMint(address to) public onlyOwner {
        uint256 tokenId = _tokenIdCounter.current();
        _tokenIdCounter.increment();
        _safeMint(to, tokenId);
    }

    function buyToken() public payable {
        uint256 tokenId = _tokenIdCounter.current();
        console.log("got here", tokenId, msg.value);
        require(msg.value == tokenId * 0.1 ether, "Not enough funds sent");

        _tokenIdCounter.increment();
        _safeMint(msg.sender, tokenId);
    }

    // The following functions are overrides required by Solidity.

    function _burn(uint256 tokenId) internal override(ERC721) {
        super._burn(tokenId);
    }

    function tokenURI(uint256 tokenId)
        public
        pure
        override(ERC721)
        returns (string memory)
    {
        return string(abi.encodePacked(_baseURI(),"_",tokenId+1,".json"));
    }
}

then re-open the console and redeploy the contract:

truffle console --network ganache
migrate

const spacebearInstance = await Spacebear.deployed();
await spacebearInstance.buyToken({value: web3.utils.toWei("0.1","ether")});

Still, no console.log. The problem is the estimate gas function, to determine how much gas that function call would take if it was sent. It turns out, it will use up all the gas, because it errors out, so we need to provide a manual gas amount to actually send the transaction and get the console log:

await spacebearInstance.buyToken({value: web3.utils.toWei("0.1","ether"), gas: 300000});

What Ganache shows you then is the console log directly in the log window:

Ganache Truffle Console.log
Ganache Truffle Console.log

Now we see that the tokenId is 0, what a bug. Let's fix it:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

import "@ganache/console.log/console.sol";


contract Spacebear is ERC721, Ownable {
    using Counters for Counters.Counter;

    Counters.Counter private _tokenIdCounter;

    constructor() ERC721("Spacebear", "SBR") {}

    function _baseURI() internal pure override returns (string memory) {
        return "https://ethereum-blockchain-developer.com/2022-06-nft-truffle-hardhat-foundry/nftdata/";
    }

    function safeMint(address to) public onlyOwner {
        uint256 tokenId = _tokenIdCounter.current();
        _tokenIdCounter.increment();
        _safeMint(to, tokenId);
    }

    function buyToken() public payable {
        uint256 tokenId = _tokenIdCounter.current();
        console.log("got here", tokenId, msg.value);
        require(msg.value == (tokenId + 1) * 0.1 ether, "Not enough funds sent");

        _tokenIdCounter.increment();
        _safeMint(msg.sender, tokenId);
    }

    // The following functions are overrides required by Solidity.

    function _burn(uint256 tokenId) internal override(ERC721) {
        super._burn(tokenId);
    }

    function tokenURI(uint256 tokenId)
        public
        pure
        override(ERC721)
        returns (string memory)
    {
        return string(abi.encodePacked(_baseURI(),"_",tokenId+1,".json"));
    }
}

Okay, so now our token is getting more expensive over time 😏

But what about Smart Contracts in the wild.

Turns out there is a trick using truffle debugger and ganache to fork the main chain...

Debugging already deployed Smart Contracts with Forking

There it is, your contract, on Mainnet. But suddenly users are reporting a bug. Something isn't right.

What comes next is usually a mix between panic and a near-death experience, because your users funds are at stake, after all...

But here is Ganache and truffle coming to the rescue.

There are two things that, when combined, really make a difference:

  1. Ganache can fork the mainchain
  2. Truffle can debug smart contracts without even having the source code locally

Little warning

This feature involves a lot of complexity from a lot of different components, it might break, but its my personal go-to swiss-army-knife for already deployed contracts.

Also, it might be restricted the the past 128 blocks due to pruning. Depends where Ganche conntects to, if its an archive node...

Let's run an example. Pick any recent transaction from Etherscan. Just make sure the interaction was with a verified smart contract.

I'll pick this one for our example:

Etherscan Transaction
Etherscan Transaction

Opening the Token will reveal the source code:

Source Code on Etherscan
Source Code on Etherscan

You need to start ganache on one terminal with very specific settings to fork the main-chain. And then start truffle on the other terminal with very specific settings to connect to ganache, which has the debug_traceTransaction endpoint available.

ganache --fork --chain.chainId 1

Ganache Mainchain Forking
Ganache Mainchain Forking

Then you can use truffle to connect to Ganache, which will happily serve the requests. But you can further use etherscan to download the source code automatically. In a second terminal open the Truffle Debugger:

truffle debug <TXHASH> --url ws://localhost:8545 --fetch-external

This will load the Truffle debugger, download the source code from etherscan, compile the source code with the right solidity compiler and then run the code through debug_traceTransaction.

It is possible that truffle hangs and you have to force stop it with ctrl+c (for a few times).

Truffle Debugger for Mainchain Contracts
Truffle Debugger for Mainchain Contracts

Now you can conveniently browse through the code with the truffle debugger.

Hit return a few times to see the actual execution of the code.

Hit h for help and v for the current variables:

View Variables during Debugging
View Variables during Debugging

This has helped me tremendously a few times. I hope it will also help you.

Now off to Hardhat...


Last update: July 27, 2023