Tracing smart contract transaction calls, and events locally with hardhat-tracer.

Prajwal More
QuillHash
Published in
5 min readMay 5, 2022

--

Photo by Mitchell Luo on Unsplash

Have you ever noticed a need of tracing smart contract transactions while testing or learning smart contract protocols or forks?

hardhat-tracer can help you.

Find this tool here: https://github.com/zemse/hardhat-tracer

From the Readme file, it's pretty clear how to use the tool. still, I would like to show a walkthrough of this tool using some simple contract examples.

For this example, we will see the trace flag but it also provides other flags like fulltrace and hash to trace.

These are the contracts that we are using:

  1. Simple ERC20 contract which mints the given token supply:
// SPDX-License-Identifier: MITpragma solidity 0.8.4;import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract ERC20Mock is ERC20, Ownable {constructor(
string memory name
string memory symbol,
uint256 supply)
public ERC20(name, symbol) {
_mint(msg.sender, supply);
}
}

2. OtherContract ( a contract that calls EventEmitter contract to emit event):

// SPDX-License-Identifier: MITpragma solidity 0.8.4;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";import "./EventEmitter.sol";
contract OtherContract{address emitter;function takeTokenAndCallOtherFunction(address _tokenAddr) public {IERC20(_tokenAddr).transferFrom(msg.sender, address(this), 5 ether);emitTokensTaken(msg.sender);}function setEmitterAddress(address _emitter) external {emitter=_emitter;}function emitTokensTaken(address _from) private {EventEmitter(emitter).emitTokenFrom(_from);}}

EventEmitter (which actually emits the event )

pragma solidity 0.8.4;
contract EventEmitter{event TokensFrom(address From);function emitTokenFrom(address _from) public {emit TokensFrom(_from);}}

A quick working flow :

Caller approves OtherContract to spend 5 tokens.

Caller sets EventEmitter contract address with setEmitterAddress function present in OtherContract contract.

Caller calls OtherContract contract’s takeTokenAndCallOtherFunction function which then calls emitTokensTaken function.

emitTokensTaken function calls emitTokenFrom function situated in EventEmitter contract

emitTokenFrom function simply emits the TokensFrom event.

Install and set hardhat-tracer :

Install it with npm i hardhat-tracer

Add it to hardhat.config.js: require(“hardhat-tracer”);

Let's write a simple test case for our smart contracts:

In the following test you can see four function calls:

it("test",async () => {// Approve OtherContract.console.log("------------------------------------ approve tokens ------------------------------------");await token1Instance.approve(OtherContractInstance.address,ethers.utils.parseEther("5"));console.log("----------------------------------------------------------------------------------------\n");console.log("----------------------------------- set emitter addr -----------------------------------");await OtherContractInstance.setEmitterAddress(EventEmitterInstance.address);console.log("----------------------------------------------------------------------------------------\n");console.log("-------------------------- call takeTokenAndCallOtherFunction --------------------------")await OtherContractInstance.takeTokenAndCallOtherFunction(token1Instance.address);console.log("----------------------------------------------------------------------------------------\n")console.log("---------------------- failed takeTokenAndCallOtherFunction call -----------------------")await expect(OtherContractInstance.takeTokenAndCallOtherFunction(token1Instance.address)).to.be.revertedWith("ERC20: insufficient allowance");console.log("----------------------------------------------------------------------------------------\n")});
  1. The first call approves the OtherContract address to spend 5 Tokens.
  2. The second call sets the EventEmitter contract address in OtherContract using setEmitterAddress
  3. The third call calls the takeTokenAndCallOtherFunction with the token address for which we have given approval.
  4. And fourth is for failed takeTokenAndCallOtherFunction call where there are no approved tokens remaining for OtherContract to Spend.

Let's trace these calls with hardhat-tracer:

now just run the test file with test command + trace flag:

eg:     > npx hardhat test test/token-trace.js --trace

You can see the transaction trace in the terminal:

Test screenshot 1

hardhat-tracer allows you to set Address name tags to identify addresses. you can add them before tests calls or after contract instance creations to set address like this:

hre.tracer.nameTags[OtherContractInstance.address] = "OtherContract";hre.tracer.nameTags[EventEmitterInstance.address] = "EventEmitter";hre.tracer.nameTags[token1Instance.address] = "TKN1";hre.tracer.nameTags[alice.address] = "Alice";

Once these tags are added then you can see direct names/tags for addresses in the trace:

traces when address tags added

In the above screenshot, you can see traces for contract creation with constructor arguments and ownership getting transferred from zero address to msg.sender.

Coming to the first test-case call, the call for approving tokens :

The second call is OtherContract.setEmitterAddress() to set the emitter address.

The third is an interesting call, this call basically calls another function which then calls again the function in Emitter contract. The screenshot below shows traces for OtherContract.takeTokenAndCallOtherFunction() function which calls transferFrom() function, which then sets approval to zero (approval was 5 tokens) and transfers 5 tokens to OtherContract address you can see event traces for Approval and Transfer.

After that it calls emitTokensTaken() function which then calls emitTokenFrom() function in the emitter contract which finally emits the TokensFrom event. You can see this in the screenshot below:

Finally, The last one is for failed takeTokenAndCallOtherFunction() call which will revert. Here you can see takeTokenAndCallOtherFunction() is calling transferFrom() which is failing because of 0 allowance (takeTokenAndCallOtherFunction function needs allowance of at least 5 token). And then you can see a revert message for insufficient allowance.

The above example was quite small and doesn't have complex external contract calls but while going through complex code like big project forks and even with a forked network that contains many calls the tracing is very useful to understand what's going on on the code level.

About QuillAudits
Leading smart contract audit firm committed to secure Blockchain projects with cutting-edge Web3 security solutions.
It is an auditing platform that rigorously analyzes and verifies smart contracts to check for security vulnerabilities through effective manual review with static and dynamic analysis tools, gas analysers as well as simulators. Moreover, the audit process also includes extensive unit testing as well as structural analysis.
We conduct both
smart contract audits and penetration tests to find potential
security vulnerabilities which might harm the platform’s integrity.
For further discussion and queries on the same topic, join the discussion on
Telegram group of QuillHash —
https://t.me/quillhash

To be up to date with our work, Join Our Community:-

Telegram | Twitter | Facebook | LinkedIn

--

--