Tenderly App — a Swiss Pocketknife for the Web3 developer

Tenderly is an Ethereum Developer Platform with deep functionality across debugging, testing, monitoring, alerting, providing infrastructure and more, aiming for developers as a whole as well as researchers and forensic investigators. In this article, we will focus on the platform’s key functionality by demonstrating the configuration process of Tenderly for use in your development workflow.

We will also be covering an example of an attack that could have been prevented if the affected Web3 company had used a tool such as Tenderly.

An introduction to Tenderly’s fundamental functions

You can use (almost) all Tenderly functionalities for free, especially if you don’t need the production-level transaction throughput. First of all, sign in or create an account, and if you want to use the full power of the platform, they offer 14 days of free trial.

After completing the sign-in you will be redirected to the dashboard where on the left side you will see a sidebar with all available features. We can start by importing either a contract or a wallet into your project.

There are several options:

  • Import Verified — this imports the contract from verified sources like etherscan, bscscan, polyscan, etc. Tenderly currently supports these networks: Mainnet, Kovan, Ropsten, Rinkeby, Görli, Polygon, xDai, POA, BSC, RSK, Avalanche, Optimism, Fantom (complete list including test networks here).
  • Browser upload — this allows you to either upload a file from your PC or use JSON metadata. Read more about it here.
  • The other two are the CLI options, and you can read about them all in more detail here.

Tenderly now also enables you to import your (or other public) wallets as well as smart contracts by clicking on “Wallets” in the left navigation bar.

After we have imported the contract and got the data, let’s explore the Transactions feature, where we can see the real-time on-chain data.

You can filter the data according to your needs by using the filter transaction option and configuring which kind of data you want to see in your transaction dashboard. This is a very useful feature for anyone who wants to find a particular chain execution.

Using data analysis functionality

Worth mentioning — Tenderly also provides an advanced technique of data analysis. I will explore the transaction below to demonstrate its functionality:

0x3fe07095be12caa9babd0fa426d8dcfd0853140ab8840030b30d28baff6c60ae

After copying the transaction hash into Tenderly you will land on a page with all history types: the transaction id, network status, block number, timestamp, etc.

The “stack traces”, located in the third info block from the top, shows what function is called when and what piece of code is used in the execution of a specific smart contract.

Find out the source of this code by clicking on the “view source” button.

Clicking on the Events section will show you what events are executed on the contract. You can also filter them by event name or by contract. Read more about the transaction execution overview here.

Using the Tenderly debugger section

Now let’s take a look at the debugger. On the left side, there is an execution section in which we see all of the contracts and function variables used during the transaction process. On the bottom there is a stack trace and on the right is the full source code of the function or contract used in the execution.

In this section, we see how much gas was used in the execution , which is helpful in gas optimization when creating and testing a new contract.

A more detailed overview of how to use the Tenderly debugger can be found here.

Simulator section: how-to

The next feature is the Simulator, which is basically a kind of compiler where you can re-execute a transaction that has already been completed in the past while changing any parameters you want to observe the (expected) outcome.

You have several options to run a simulation:

  • Perform a simulation using a contract that you are monitoring in Tenderly.
  • Perform a simulation using a contract that you are not monitoring in Tenderly and which is publicly verified.

If you select a contract that you are monitoring in Tenderly select the function that you would like to execute:

  • Input the function parameters that are required by the smart contract
  • Then input the transaction parameters to indicate the block and transaction information that you would like to simulate.
  • Leave block number and tx index blank if you would like to use the current block, or choose a specific historic block and transaction index within it.
  • Then press simulate transaction and review your results.

If you would like to run a simulation on a contract that you are not monitoring in Tenderly use the Custom Contract option. To execute this simulation, we need the hex data from the transaction we want to simulate.

Click on “New Simulation”, select the contract and Tenderly will automatically load the ABI. On the right side you will see the block address, tx index and input fields. After running the simulation, a new window will open in which you will see the status of your execution.

We can add a custom value by turning on/off the button above the input fields. In my test case the execution failed. Tenderly clearly shows the exact line of code that lead to tx failure.

If you want to change the smart contract source code, click on the re-simulate button. Then, click on the edit smart contract button and it will open the source code window. You will see which line is causing the problem, and you can now change anything you want within the code before testing (simulating) it again. Don’t forget to click “apply changes” after editing.

If you would like to execute complex simulations which include multiple (and/or chained) transactions, check out how to use Tenderly Forks.

Analytics section: deep dive in

Let’s dive into analytics. In the analytics section we see a lot of graphs. Each graph has a different purpose.

The first is Transactions Over Time. This graph shows how many transactions happened in an hour/day/weekend/month/custom time period.

Second is Events Emitted Over Time — with it we can check how many events happened during a selected time period. Top caller shows which user performed the majority of calls for this contract. Daily and weekly active users give us an overview of how many users are actively using our smart contract or one of our functions.

These reports help you see trends in your smart contracts. For example, view how transactions, users and other important metrics change and grow over time. If you wish to view data for all contracts in your project, you can upgrade your account to the Tenderly Developer or Tenderly Pro plan for organizations.

Alerting: All the key info, timely served

Another important feature is alerting. You can find it in the monitoring section. This is the type of feature we use to get real-time alerts when a certain event triggers, a certain code is executed, and certain conditions are met. It also provides information on what exactly happened and why the code failed or is not working correctly.

Click on alerting and you will see the dashboard, then click on the setup. You can configure alerts in a way it will react on, for example, specific function call, event emit, ERC20 token transfer, state change, etc.

You can monitor the following types of events:

  • Successful transactions
  • Failed transactions
  • Specific function calls
  • Events emitted
  • Event parameters match a specific condition
  • ERC20 token transfers
  • Whitelisted callers
  • Blacklisted callers
  • ETH balances that fall below a threshold
  • When a specific transaction occurs
  • Variable state changes
  • View function values change
  • And more…

Let’s set up an alert for a successful transaction. After clicking, it took us to the next step where we can see several options.

I’m gonna use the third (project) one. It will send me an alert when any transaction in this project is executed successfully. Click on it and set the channel(s) where your alert will be delivered. By default Tenderly sends alerts to the associated (used in sign-up) email. You also can use different apps like telegram, discord, slack and more. In the example, I have chosen the email method.

After pressing “save” you will receive an alert when an event meets your saved criteria parameters.

The casual work of an investigator in Tenderly: an inside view

Let’s take an example of a known and recent hack to show how it could have been prevented if the project had been using Tenderly for monitoring changes in smart contracts. Last year, the Cover Protocol got hacked because of the inability of code to update the cache in storage. Developer team could have added an alerting feature on important functions like deposit/withdrawal and state changes to prevent an attack.

Let’s manually analyze the hack. First, import the smart contract “blacksmith.sol” in which the vulnerability was found. If you know the exact name of the affected protocol or vulnerable contract then you can search it on Tenderly’s “contract input feature”.

The best way is to search the project’s code on their github repository because sometimes it is unable to verify if the contract belongs to the project.

After these actions you can easily use Tenderly to import the correct smart contract.

Now, after we have imported the contract let’s analyze the hack itself. We know that the bug existed in the deposit function’s cache so we will simulate that function. As we can see on the screenshot below, after execution it failed with the “Blacksmith: pool does not exist” reason.

Open the debugger section. On line 118 we see Pool memory pool = pools[_lpToken]; in deposit function where data is cached and later updated in the line 125 we see _claimCoverRewards(pool, miner);

Contract used the cache data because the data itself stayed in memory; that’s why later the contract calculated the function using that data. Cache was not updated which resulted in this hack:

miner.rewardWriteoff = miner.amount.mul(pool.accRewardsPerToken).div(CAL_MULTIPLIER);

miner.bonusWriteoff = miner.amount.mul(bonusToken.accBonusPerToken).div(CAL_MULTIPLIER);

function deposit(address _lpToken, uint256 _amount) external override {

require(block.timestamp >= START_TIME , "Blacksmith: not started");

require(_amount > 0, "Blacksmith: amount is 0");

Pool memory pool = pools[_lpToken];

require(pool.lastUpdatedAt > 0, "Blacksmith: pool does not exists");

require(IERC20(_lpToken).balanceOf(msg.sender) >= _amount, "Blacksmith: insufficient balance");

updatePool(_lpToken);



Miner storage miner = miners[_lpToken][msg.sender];

BonusToken memory bonusToken = bonusTokens[_lpToken];

_claimCoverRewards(pool, miner);

_claimBonus(bonusToken, miner);



miner.amount = miner.amount.add(_amount);

// update writeoff to match current acc rewards/bonus per token

miner.rewardWriteoff = miner.amount.mul(pool.accRewardsPerToken).div(CAL_MULTIPLIER);

miner.bonusWriteoff = miner.amount.mul(bonusToken.accBonusPerToken).div(CAL_MULTIPLIER);



IERC20(_lpToken).safeTransferFrom(msg.sender, address(this), _amount);

emit Deposit(msg.sender, _lpToken, _amount);


}

1) A new pool was approved for liquidity mining merely hours before the hack. This pool is perfectly normal but since it was new, the blacksmith contract didn’t have any LP token of this pool.

2) The attacker deposited some tokens of this pool into the Blacksmith contract.

3) The Blacksmith contract keeps track of rewards on a per token basis. If a lot of tokens are locked, the per token reward will be small. If very few tokens are locked, the per token reward will be large. The relevant variable is called accRewardsPerToken and is calculated as totalPoolRewards / totalTokenBalance.

4) The attacker then withdrew almost all of the LP tokens from the Blacksmith contract, reducing the totalTokenBalance amount to almost zero.

5) The attacker then deposited some tokens of this pool again into the Blacksmith contract. This is where the bug showed its true colors. Since the totalTokenBalance was reduced a lot in the previous transaction, the newly calculated accRewardsPerToken shot up. The contract uses rewardWriteoff to keep the effect of accRewardsPerToken in check. However, due to the bug, the old (small) value of accRewardsPerToken was used when calculating the rewardWriteoff value. Due to this, the large value of accRewardsPerToken remained unchecked.

6) The attacker then withdrew their rewards. Since there was a large, unchecked value in accRewardsPerToken, the total reward paid out of the system got inflated and the contract ended up minting 40,796,131,214,802,500,000 COVER tokens.

Investigator & Researcher | Twitter: https://twitter.com/officer_cia