Debugging Hardhat Smart Contract Project with Tenderly

Published on: March 20, 20258 minutes

Author: QuillAudits Team

Even with the best tools, auditing a smart contract on Hardhat can feel like navigating a maze in the dark, especially when dealing with nested calls and complex logic. Are you struggling to track variable values and control flow during your audits or development?

Imagine pinpointing issues with precision, sharing testnet progress seamlessly with your team, and eliminating the guesswork. In this article, we'll guide you through integrating Tenderly with your Hardhat project, focusing on practical steps and crucial insights beyond the official documentation.

Before that let's check why it’s needed:

From a security point of view, while auditing protocols, you will encounter multiple scenarios where keeping track of various variable values in the control flow is not feasible. For example, there can be multiple nested calls with values getting updated in each call frame. In many large projects, it’s impractical to track everything mentally, and direct debugging is also not possible if you’re using a Hardhat project.

While we have explored some local options, this would be more user friendly option. with possibility of working with multiple teams by sharing the TestNet progress.
 

Project Overview

The project consists of three main simplified smart contracts:

  1. Token.sol: An ERC20 token used for staking.
  2. Staking.sol: A contract that allows users to stake tokens and manage their staked amounts.
  3. Rewards.sol: A contract with reward logic.

Really short explanation of the contract functionality / entry points for mentioned contracts:

carbon.webp

carbon (1).webp


Function-wise Functionality for Staking Contract (Staking.sol)

1. constructor(MyToken _token, Rewards _rewards)

Initializes the staking contract with the MyToken and Rewards contract addresses.
 

2. function stake(uint256 amount) public nonReentrant

Allows users to stake a specified amount of MyToken tokens, updating their staked amount and the total staked amount.
 

3. function unstake(uint256 amount) public nonReentrant

Allows users to unstake a specified amount of MyToken tokens, updating their staked amount, the total staked amount, and claiming rewards.
 

4. function getStakedAmount(address staker) public view returns (uint256)

Returns the staked amount of a specified user.
 

5. function getTotalStaked() public view returns (uint256)

Returns the total amount of tokens staked in the contract.


Function-wise Functionality for Rewards Contract (Rewards.sol)

1. function initialize(address _staking) external

Initializes the rewards contract with the address of the staking contract.
 

2. function calculateRewards(address staker) public view returns (uint256)

Calculates the rewards for a specified user based on their staked amount.
 

3. function claimRewards(address staker) external

Claims the rewards for a specified user, ensuring only the staking contract can call this function.

 

From now onwards, you can follow the official documentation, but there’s something additional I’d like to share that you might not find in the docs.


Setting up Tenderly 

1. Install the Tenderly CLI

brew tap tenderly/tenderly
brew install tenderly
tenderly login


Once logged in, you can check tenderly whoami to confirm.
 

2. Install tenderly-hardhat plugin

npm install --save-dev @tenderly/hardhat-tenderly
 

3. Import the plugin to your hardhat.config.js/ts file like 
const tenderly = require("@tenderly/hardhat-tenderly"); the way of importing will vary based if its .ts or .js file.
 

4. Generally if you are using @tenderly/hardhat-tenderly version that is upto 2.4.0 you need to call the tdly.setup() and set the automaticVerifications option to true like this: tenderly.setup({ automaticVerifications: true }); 

But if your are using the version that is greater than this then you can set the TENDERLY_AUTOMATIC_VERIFICATION environment variable to true for example with .env file. Otherwise you might get warning like this.

0_LHG62DCeJAz4Vo-l.webp

5. Adding tenderly configuration in hardhat configuration:
Where you need to create a virtual TestNet using Tenderly dashboard and add a network with a Tenderly virtual TestNet URL and Tenderly configuration object:

carbon (2).webp


Deploying and Debugging with tenderly:

Writing the deployment script. How script works is not explained as it is assumed that reader knows it already.

carbon (3).webp

As you can see the script contains code to deploy contracts and to call the functions. and based on the smart contract logic it expects the output.

But additionally you can see when calling any functions on the smart contract, the function is actually called on native Contract and not on contract name variable/instance. That's because once you configure hardhat to use Tenderly the types of these contracts are changed from BaseContract type to TdlyContract . you can check this by printing the contract instance that BaseContract is in the nativeContract . Generally it is directly available when the tenderly is not configured. But here, directly calling the function won't work with tenderly setup.

0_i73CzkaKpgt0tjFJ.webp

 

Additionally it is helpful to use waitForDeployment() on contracts after deploying (as can be seen in the script above) as it will verify the contracts in tenderly TestNets.

Since the automatic contract verification is set to true, you can just run the deployment script with the command like: where the virtualMainnet is the network name in the hardhat config file:

npx hardhat run scripts/deploy.ts --network virtualMainnet

Open tenderly TestNet dashboard and debug the transaction by clicking on the transaction hash.

0_jjskwQTYOGqVTMD1.webp

and here we go debugging:

0_L6Oo92LYpYi77qd1.webp

Conclusion:

While Tenderly offers a wealth of features, we focused on addressing the specific challenges and 'quirks' encountered during script development, particularly the crucial aspect of accessing the base contract object for function calls. These insights, often less emphasized in official documentation, are essential for a smooth debugging experience. 

By mastering these nuances, you'll be well-equipped to navigate complex smart contract logic and enhance your development workflow, ultimately leading to more effective smart contract auditing. Remember, effective debugging is not just about finding errors; it's about gaining a deeper understanding of your code's behavior and building more robust, secure smart contracts.

Loading...
Loading...
Loading...
Telegram