Debugging Smart Contracts with Truffle¶
In this lab you will learn two things:
- Debugging Smart Contracts while you develop
- 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?!
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:
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:
- Ganache can fork the mainchain
- 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:
Opening the Token will reveal the source code:
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
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).
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:
This has helped me tremendously a few times. I hope it will also help you.
Now off to Hardhat...