Fatskills
Practice. Master. Repeat.
Study Guide: Blockchain and Web3 Development: Ethereum and Smart Contracts Testing and Debugging Smart Contracts Unit Tests Consolelog Tenderly
Source: https://www.fatskills.com/cryptocurrency-bitcoin-blockchain-and-more/chapter/blockchain-and-web3-development-blockchain-and-web3-development-ethereum-and-smart-contracts-testing-and-debugging-smart-contracts-unit-tests-consolelog-tenderly

Blockchain and Web3 Development: Ethereum and Smart Contracts Testing and Debugging Smart Contracts Unit Tests Consolelog Tenderly

By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.

⏱️ ~7 min read

What This Is

Testing and debugging smart contracts is the practice of automatically verifying that every function in a Solidity program behaves exactly as intended before you lock it on‑chain. Because blockchain code is immutable and every mistake costs real gas (and sometimes user funds), a solid test suite—combined with live‑debug tools like console.log and Tenderly—lets you catch bugs, edge‑cases, and security flaws early.

Real‑world example: Imagine a Uniswap V3 swap contract. If the price‑oracle logic is off by just one basis point, traders could lose thousands of dollars in a single transaction. Unit tests that simulate swaps across many price ranges, plus a Tenderly “simulation” of the swap on a forked mainnet, guarantee the contract will settle correctly when it goes live.


Key Terms & Code Snippets

  • Unit Test (Hardhat/Foundry): A small, isolated test that calls a single contract function with controlled inputs and asserts the output.
    ```js // test/MyToken.test.js const { expect } = require("chai"); const { ethers } = require("hardhat");

describe("MyToken", function () {
it("mints the correct amount", async () => {
const [owner] = await ethers.getSigners();
const MyToken = await ethers.getContractFactory("MyToken");
const token = await MyToken.deploy();
await token.mint(owner.address, 1000);
expect(await token.balanceOf(owner.address)).to.equal(1000);
}); }); ```


  • console.log (Hardhat/Foundry): A Solidity‑level logger that prints values to the Hardhat console during a test run. Great for quick sanity checks without writing a full test.
    ```solidity // contracts/Example.sol pragma solidity ^0.8.20; import "hardhat/console.sol";

function add(uint256 a, uint256 b) external pure returns (uint256) {
uint256 sum = a + b;
console.log("add(): a=%s b=%s sum=%s", a, b, sum);
return sum; } ```


  • Tenderly Simulation: A cloud service that replays a transaction on a forked state, showing gas usage, internal calls, and state changes. You can upload a Hardhat test transaction and see a visual trace.

  • assert vs require: assert is for internal invariants (should never fail); require validates user‑provided data and reverts with a custom error message. Using assert for user checks wastes gas and can trigger a panic error.

  • Fuzz Testing (Foundry): Automatically generates random inputs to a function and checks that invariants hold (e.g., balances never go negative).
    ```solidity // test/Fuzz.t.sol contract FuzzTest is Test {
    MyToken token;
    function setUp() public { token = new MyToken(); }

    function testTransferFuzz(uint256 amount, address from, address to) public {
    vm.assume(amount < type(uint256).max / 2);
    token.mint(from, amount);
    token.transfer(to, amount);
    assertEq(token.balanceOf(to), amount); } } ```

  • Coverage Report: A metric (usually % of lines/branches) that tells you how much of your contract code is exercised by tests. Hardhat’s hardhat-coverage plugin produces an HTML report.

  • evm_snapshot / evm_revert: Hardhat/Ethers methods that snapshot the blockchain state before a test and revert after, keeping each test isolated and fast.
    js await network.provider.send("evm_snapshot", []); // …run test actions… await network.provider.send("evm_revert", [snapshotId]);

  • delegatecall Pitfall: When a proxy contract forwards calls via delegatecall, the storage layout of the implementation must exactly match the proxy. Tests must deploy the proxy and implementation together and assert that state variables line up.

  • Gas‑Profiler (hardhat-gas-reporter): Shows per‑function gas cost in your test output, helping you spot expensive loops or unnecessary storage writes.

  • Static Analyzer (slither): Not a test but a pre‑run scanner that flags reentrancy, unchecked low‑level calls, and other vulnerabilities. Run it on your repo before you write tests to catch low‑hanging fruit.


Step‑by‑Step / Process Flow

  1. Write the contract in VS Code (or Remix) and commit to a Git repo.
  2. Initialize a Hardhat project: npm init -y && npm install --save-dev hardhat @nomicfoundation/hardhat-chai-matchers ethers. Run npx hardhatCreate a basic sample project.
  3. Add testing utilities: npm install --save-dev hardhat-ethers hardhat-console hardhat-gas-reporter. Enable console.log by adding import "hardhat/console.sol"; in Solidity files you want to debug.
  4. Write unit tests in test/ (JS/TS) or test/ (Foundry). Use beforeEach to await ethers.getContractFactory and deploy.
  5. Run the suite: npx hardhat test. Watch the console for any console.log output; fix failing assertions.
  6. Generate a coverage report: npx hardhat coverage. Open coverage/index.html and verify > 90 % coverage.
  7. Upload a transaction to Tenderly (optional but recommended): copy the raw tx from Hardhat’s output, paste into Tenderly’s “Simulate Transaction” UI, and inspect the call trace. Iterate until the simulation matches expected state changes.

Common Mistakes

  • Mistake: Using tx.origin for access control.
    Correction: Always use msg.sender. tx.origin can be spoofed through a malicious contract, letting attackers bypass restrictions.

  • Mistake: Forgetting to reset the blockchain state between tests, causing state bleed‑over.
    Correction: Use await network.provider.send("evm_snapshot") in a beforeEach hook and evm_revert in an afterEach hook, or rely on Hardhat’s automatic fixture reset.

  • Mistake: Writing tests that only cover the “happy path” (e.g., successful transfers) and ignoring failure branches.
    Correction: Add expect(...).to.be.revertedWith("...") for every require/revert path, and use fuzzing to explore edge cases like zero amounts, max uint256, and address zero.

  • Mistake: Assuming a proxy’s storage layout is correct without testing.
    Correction: Deploy the proxy + implementation in a test, call a setter on the implementation via the proxy, then read the value directly from the proxy to confirm the slot mapping.

  • Mistake: Ignoring gas‑report warnings and shipping a contract with an O(n) loop that could hit block‑gas limits.
    Correction: Run hardhat-gas-reporter each test run; refactor any function that exceeds ~50 k gas per iteration into a batched or off‑chain process.


Blockchain Developer Interview / Practical Insights

  1. “Explain the difference between call and delegatecall and when you’d use each.”
    Interviewers look for understanding of context switching: call executes code in the callee’s storage, while delegatecall runs the callee’s code in the caller’s storage—the backbone of upgradeable proxy patterns.

  2. “How would you test that a newly added ERC‑20 permit function (EIP‑2612) works correctly?”
    Expect a demonstration of signing an off‑chain EIP‑712 message, feeding the signature to the contract in a test, and asserting that the allowance is set without a separate approve transaction.

  3. “What are the pros and cons of using Tenderly vs. a local Hardhat fork for debugging complex DeFi interactions?”
    Key points: Tenderly gives a UI, real‑time gas estimates, and persistent transaction history; a local fork is faster, fully scriptable, and can be integrated into CI pipelines.

  4. “Describe how you’d detect a reentrancy bug using unit tests.”
    Look for a test that deploys a malicious attacker contract, calls the vulnerable function, and asserts that the attacker’s balance does not increase beyond the expected amount.


Quick Check Questions

  1. Scenario: A contract uses tx.origin to restrict minting to the contract owner.
    Answer: Dangerous – an attacker can trick a user into calling a malicious contract that then calls the mint function, and tx.origin will still be the user’s address, bypassing the restriction.

  2. Scenario: Your unit test passes, but a Tenderly simulation shows a revert due to “insufficient funds”.
    Answer: The test likely used a mocked balance or a hard‑coded msg.sender with unlimited ether; you need to fund the test address with realistic ether (e.g., await ethers.provider.sendTransaction({to: addr, value: ethers.utils.parseEther("10")})).

  3. Scenario: You added console.log statements to a contract, but they never appear in the Hardhat test output.
    Answer: console.log only works when the contract is compiled with the Hardhat Solidity compiler (hardhat compile) and the test is run with npx hardhat test; ensure you’re not using a different compiler (e.g., Foundry) or a production build.


Last‑Minute Cram Sheet (10 one‑liners)

  1. ⚠️ Reentrancy – always use Checks‑Effects‑Interactions or OpenZeppelin’s ReentrancyGuard.
  2. assert = internal invariant; require = user input validation.
  3. EVM snapshot (evm_snapshot) + revert (evm_revert) keep tests isolated and fast.
  4. Coverage ≥ 90 % is the de‑facto benchmark for production‑ready contracts.
  5. delegatecall shares the caller’s storage; mismatched slot order = corrupted state.
  6. Hardhat gas‑reporter shows gas per function; aim for < 30 k gas for simple ERC‑20 transfers.
  7. Tenderly simulation = “dry‑run” on a forked mainnet; perfect for multi‑step DeFi flows.
  8. Fuzz testing (Foundry) automatically finds edge‑cases; add vm.assume to prune impossible inputs.
  9. Solidity 0.8.x introduced built‑in overflow checks; no need for SafeMath unless you target < 0.8.0.
  10. ERC‑20 vs ERC‑777 – ERC‑777 adds hooks (tokensReceived) and can be re‑entered; test those hooks aggressively.


ADVERTISEMENT