Debugging FAQ - The Questions You Ask When Your Contract is Fucked

Q

My contract reverts with "execution reverted" - how do I find the actual problem?

A

This is the number one reason developers switch to Hardhat. Generic revert messages are useless when you're trying to debug complex DeFi logic at 2am.

Use Hardhat's console.log debugging:

import "hardhat/console.sol";

function problematicFunction(uint256 amount) public {
    console.log("Input amount:", amount);
    console.log("Contract balance:", address(this).balance);
    console.log("User balance:", msg.sender.balance);
    
    require(amount > 0, "Amount must be positive");
    console.log("Passed require check");
    
    uint256 result = doComplexMath(amount);
    console.log("Complex math result:", result);
    
    require(result < maxAllowed, "Result too large"); // This might be failing
}

Check specific failure points:

  1. Run your tests with npx hardhat test --trace to see all calls
  2. Look at the exact line where execution stops
  3. Hardhat Network gives you source-mapped stack traces

Pro tip: If you're testing against a forked mainnet and the error is unclear, the issue is probably in a contract you don't control. Use hardhat-tracer to see internal calls.

Q

How do I debug transactions that work in tests but fail on mainnet?

A

Gas estimation differences are the biggest culprit. Mainnet has MEV bots, network congestion, and different block conditions than your local Hardhat Network.

Debug with mainnet forking:

// hardhat.config.js - Fork at a specific block
networks: {
  hardhat: {
    forking: {
      url: process.env.MAINNET_RPC_URL,
      blockNumber: 18500000  // Pin to specific block for consistency
    },
    mining: {
      auto: false,          // Manual mining for debugging
      interval: 1000        // Or automatic every 1 second
    }
  }
}

// In your test
await network.provider.send("evm_mine"); // Mine a block manually
await network.provider.send("hardhat_setNextBlockBaseFeePerGas", ["0x1"]); // Control gas

Common mainnet vs testnet differences:

  • Block timestamps: Mainnet blocks are ~12 seconds apart, your tests might assume instant
  • Gas prices: EIP-1559 gas mechanics work differently under load
  • Contract state: Mainnet contracts might be paused or have different parameters
  • MEV protection: Flashloan arbitrage might front-run your transactions

Debugging strategy:

  1. Fork mainnet at the exact block where your tx fails
  2. Enable console.log in relevant contracts using state overrides
  3. Run the exact same transaction parameters
  4. Use --vvvv flag to see all internal calls and storage operations
Q

My tests are slow as fuck - how do I speed them up?

A

Hardhat 3's EDR runtime should have fixed most speed issues. If you're still on Hardhat 2, upgrade immediately. If you're already on Hardhat 3 and it's slow, you're doing something wrong.

Speed optimization checklist:

// 1. Use test fixtures to avoid redeploying contracts
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");

async function deployTokenFixture() {
  const [owner, addr1] = await ethers.getSigners();
  const Token = await ethers.getContractFactory("Token");
  const token = await Token.deploy();
  return { token, owner, addr1 };
}

describe("Token tests", function() {
  it("should transfer tokens", async function() {
    const { token, owner, addr1 } = await loadFixture(deployTokenFixture);
    // Test here - contract deployed once, shared across tests
  });
});

Performance killers:

  • Redeploying contracts in every test - Use fixtures instead
  • Complex mainnet forks - Fork at a specific block, don't use latest
  • Too many console.log statements - Remove them from production test runs
  • Large contract suites - Break into multiple test files

Memory usage fixes:

// 2. Set explicit gas limits to avoid estimation
await contract.method({ gasLimit: 500000 });

// 3. Use snapshots for test isolation instead of redeploys
await network.provider.request({
  method: "evm_snapshot",
  params: [],
});

Real performance numbers: With EDR, test suites that took 10 minutes in Hardhat 2 should run in 2-3 minutes. If they don't, you have configuration issues.

Q

How do I debug gas optimization issues?

A

Use gas reporters that actually give actionable data:

npm install --save-dev hardhat-gas-reporter
// hardhat.config.js
gasReporter: {
  enabled: process.env.REPORT_GAS ? true : false,
  currency: "USD",
  gasPrice: 20,      // Current mainnet gas price
  coinmarketcap: process.env.CMC_API_KEY,
  showTimeSpent: true,
  showMethodSig: true,
  maxMethodDiff: 10,  // Highlight functions that changed >10 gas
}

Advanced gas debugging with hardhat-tracer:

## See exactly which operations consume gas
npx hardhat test --vvv --opcodes SSTORE,SLOAD

Gas optimization red flags:

// BAD: Reading storage in loops
for (uint i = 0; i < expensiveArray.length; i++) {
    console.log("Gas killer:", expensiveArray[i]);
}

// GOOD: Cache length and use memory
uint256 length = expensiveArray.length;
for (uint i = 0; i < length; i++) {
    console.log("Optimized:", expensiveArray[i]);
}

Profile before optimizing:

  1. Run REPORT_GAS=true npx hardhat test to get baseline
  2. Identify the most expensive functions
  3. Use hardhat-tracer to see which opcodes cost the most
  4. Optimize only the expensive paths - don't waste time on functions called once
Q

How do I test time-dependent contract behavior?

A

Time manipulation is essential for testing vesting schedules, lockups, and DeFi protocols with time-based logic.

const { time } = require("@nomicfoundation/hardhat-network-helpers");

describe("Time-dependent behavior", function() {
  it("should vest tokens over time", async function() {
    // Fast forward 30 days
    await time.increase(30 * 24 * 60 * 60);
    
    // Or jump to specific timestamp
    const futureTime = (await time.latest()) + 86400; // +1 day
    await time.increaseTo(futureTime);
    
    // Test vesting logic
    const vestedAmount = await vestingContract.getVestedAmount();
    expect(vestedAmount).to.be.gt(0);
  });
  
  it("should handle block number dependencies", async function() {
    // Mine specific number of blocks
    await mine(100);
    
    // Or mine to specific block
    await mineUpTo(await getBlockNumber() + 50);
  });
});

Testing compound interest and decay:

// Test yearly compound interest
const oneYear = 365 * 24 * 60 * 60;
await time.increase(oneYear);
await stakingContract.compound();

const expectedReturn = principal.mul(108).div(100); // 8% APY
expect(await stakingContract.balanceOf(user)).to.be.closeTo(expectedReturn, precision);

Time-based gotchas:

  • Block timestamps vs block numbers: Know which your contract uses
  • Leap years: If testing multi-year periods, account for them
  • Time zones: Blockchain time is always UTC
  • Precision: Use closeTo for calculations involving time decay

Console.log Debugging: The Feature That Changed Everything

Before Hardhat's console.log support, debugging Solidity was like trying to fix a car engine while blindfolded. You'd add require statements with different revert messages just to figure out where your function was failing. It was barbaric.

The Breakthrough: Actual Logging in Solidity

Console.log in Solidity works exactly like you'd expect from any modern programming language. Import the library, add your logs, run your tests, see the output. Revolutionary stuff for 2025.

import "hardhat/console.sol";

contract DebuggingExample {
    mapping(address => uint256) public balances;
    
    function transfer(address to, uint256 amount) public {
        console.log("=== Transfer Debug Info ===");
        console.log("From:", msg.sender);
        console.log("To:", to);
        console.log("Amount requested:", amount);
        console.log("Sender balance:", balances[msg.sender]);
        console.log("Contract ETH balance:", address(this).balance);
        
        require(balances[msg.sender] >= amount, "Insufficient balance");
        console.log("✓ Balance check passed");
        
        balances[msg.sender] -= amount;
        console.log("Sender new balance:", balances[msg.sender]);
        
        balances[to] += amount;
        console.log("Recipient new balance:", balances[to]);
        console.log("=== Transfer Complete ===");
    }
}

Advanced Console.log Techniques

Conditional logging for complex debugging scenarios:

function complexCalculation(uint256 input) public returns (uint256) {
    if (input > 1000000) {
        console.log("🚨 Large input detected:", input);
        console.log("Gas remaining:", gasleft());
    }
    
    uint256 result = (input * 150) / 100;
    
    // Log intermediate steps for specific ranges
    if (input >= 500 && input <= 1500) {
        console.log("Mid-range calculation - input:", input, "result:", result);
    }
    
    return result;
}

Debugging storage layout issues:

contract StorageDebug {
    uint256 private slot0;
    address private slot1; 
    mapping(address => uint256) private slot2;
    
    function debugStorage(address user) public view {
        console.log("=== Storage Debug ===");
        console.log("slot0 value:", slot0);
        console.log("slot1 value:", slot1);
        console.log("User balance in slot2:", slot2[user]);
        
        // Debug packed storage
        uint128 packed1 = 12345;
        uint128 packed2 = 67890;
        console.log("Packed values - first:", packed1, "second:", packed2);
    }
}

Performance Impact Reality Check

Does console.log affect gas usage? In Hardhat Network, no. The console.log calls are stripped out when compiling for actual deployment. But there's a catch - they do slow down test execution slightly because they're processed during runtime. See the Hardhat debugging guide for performance considerations.

Best practices for production debugging:

  1. Leave debugging code in during development - comment out before mainnet deployment
  2. Use conditional compilation for extensive logging with Solidity preprocessor directives:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// Only import console in development
import "hardhat/console.sol";

contract MyContract {
    function sensitiveOperation(uint256 amount) public {
        // Wrap debug logs in compilation flags
        console.log("Debug: processing amount", amount);
        
        // Your actual logic here
        require(amount > 0, "Amount must be positive");
    }
}

Hardhat Logo

The EDR Runtime Debugging Advantage

Hardhat's EDR (Ethereum Development Runtime) takes debugging beyond just console.log. When contracts fail, you get actual stack traces pointing to the exact Solidity line, not just EVM opcodes.

Before EDR (the dark ages):

Error: Transaction reverted without a reason string
    at Contract.someFunction (Contract.sol)
    at processTicksAndRejections (node:internal/process/task_queues:95:5)

After EDR (enlightenment):

Error: VM Exception while processing transaction: revert Insufficient balance
    at MyContract.transfer (contracts/MyContract.sol:45:9)
    at HardhatNode._mineBlockWithPendingTxs (hardhat/src/internal/hardhat-network/provider/node.ts:1773:23)
    
Contract call stack:
    MyContract.transfer(to=0x1234..., amount=1000)
        ERC20.transferFrom(from=0x5678..., to=0x1234..., amount=1000)
            require(balanceOf[from] >= amount) <- FAILURE POINT

The stack trace includes your variable values and shows the exact require statement that failed. This alone saves hours of debugging compared to the cryptic EVM errors you get with other tools like Remix or older Hardhat versions.

Mainnet Forking: Debug Against Real State

The most powerful debugging technique: fork mainnet and debug against real deployed contracts with real state.

// hardhat.config.js - Fork mainnet for debugging
networks: {
  hardhat: {
    forking: {
      url: "https://mainnet.infura.io/v3/YOUR_INFURA_KEY",
      blockNumber: 18500000  // Pin to specific block for reproducible debugging
    }
  }
}

Debug real protocol interactions:

describe("Debug Uniswap interaction", function() {
  it("should debug failed swap", async function() {
    // Fork mainnet and impersonate a whale account
    await network.provider.request({
      method: "hardhat_impersonateAccount",
      params: ["0x8ba1f109551bD432803012645Hac136c"]  // Vitalik's address
    });
    
    const vitalik = await ethers.getSigner("0x8ba1f109551bD432803012645Hac136c");
    
    // Debug the exact swap that's failing
    const uniswapRouter = await ethers.getContractAt(
      "IUniswapV2Router02", 
      "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"
    );
    
    // This will show console.logs from Uniswap's contracts if they have them
    await uniswapRouter.connect(vitalik).swapExactETHForTokens(
      0,
      ["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "0xdAC17F958D2ee523a2206206994597C13D831ec7"],
      vitalik.address,
      Math.floor(Date.now() / 1000) + 300,
      { value: ethers.utils.parseEther("1") }
    );
  });
});

State override debugging - inject console.log into existing contracts:

tracer: {
  stateOverrides: {
    "0xA0b86a33E6C6a4f6e7e8c3f0Ea8D0DAb7f4C1234": {  // Target contract
      bytecode: "MyDebuggingContract"  // Your version with console.logs
    }
  }
}

This lets you add debugging to contracts you don't own - essential when your DeFi protocol interacts with Uniswap, Compound, Aave, etc.

Gas Profiling and Transaction Tracing

Beyond console.log: Use hardhat-tracer to see every internal call, event, and storage operation:

## See everything that happens in a transaction
npx hardhat test --vvvv

## Focus on specific opcodes (storage operations)
npx hardhat test --vvv --opcodes SSTORE,SLOAD

## Trace mainnet transactions
npx hardhat trace --hash 0x1234abcd --rpc "https://mainnet.infura.io/v3/YOUR_KEY_HERE"

Real output from hardhat-tracer shows you the exact gas costs and call hierarchy:

├─ MyContract.complexFunction() [Gas: 85,432]
│  ├─ ERC20.approve(spender=0x1234..., amount=1000) [Gas: 22,567]
│  │  └─ emit Approval(owner=0x5678..., spender=0x1234..., value=1000)
│  ├─ UniswapRouter.swapExactTokensForTokens(...) [Gas: 45,234]
│  │  ├─ UniswapPair.getReserves() [Gas: 2,345] 
│  │  │  └─ return (112340000000000000000, 445670000000000000000, 1640995200)
│  │  └─ WETH.transfer(to=0x9abc..., amount=234500000000000000) [Gas: 23,456]
│  └─ console.log("Swap completed, received:", 234500000000000000)

This level of visibility into contract execution doesn't exist in any other Ethereum development environment. You can see exactly which function calls consume the most gas, what events are emitted, and where your transaction might be failing.

The Reality of Professional Smart Contract Debugging

Professional DeFi protocols like Uniswap, Aave, and Compound all use Hardhat's debugging features extensively. When you're dealing with millions of dollars in TVL, "transaction reverted" isn't acceptable - you need to know exactly what went wrong.

The combination of console.log, stack traces, mainnet forking, and transaction tracing makes Hardhat the only development environment where you can actually debug complex smart contract interactions effectively. Everything else is guesswork.

Advanced Testing FAQ - The Hard Questions About Testing Smart Contracts

Q

How do I test upgradeable contracts without breaking everything?

A

OpenZeppelin upgrades add complexity that'll make you question your life choices. The storage layout restrictions alone have killed more projects than reentrancy attacks.

Test the full upgrade cycle:

const { upgrades } = require("@openzeppelin/hardhat-upgrades");

describe("Contract Upgrades", function() {
  it("should upgrade without breaking storage", async function() {
    // Deploy V1
    const ContractV1 = await ethers.getContractFactory("MyContractV1");
    const proxy = await upgrades.deployProxy(ContractV1, [1000], {
      initializer: 'initialize'
    });
    
    // Set some state in V1
    await proxy.setValue(42);
    await proxy.setUser("0x1234567890123456789012345678901234567890", 100);
    
    // Upgrade to V2
    const ContractV2 = await ethers.getContractFactory("MyContractV2");
    const upgraded = await upgrades.upgradeProxy(proxy.address, ContractV2);
    
    // Critical: Test that V1 state is preserved
    expect(await upgraded.getValue()).to.equal(42);
    expect(await upgraded.getUserBalance("0x1234567890123456789012345678901234567890")).to.equal(100);
    
    // Test new V2 functionality
    await upgraded.newV2Function();
    expect(await upgraded.getNewV2Value()).to.equal(expected);
  });
});

Storage layout testing - this catches upgrades that'll brick your contract:

// V1 Contract
contract MyContractV1 {
    uint256 public value;           // Slot 0
    mapping(address => uint256) public users;  // Slot 1
    address public owner;           // Slot 2
}

// V2 Contract - WRONG, breaks storage layout
contract MyContractV2 {
    uint256 public newValue;        // Slot 0 - OVERWRITES old value!
    uint256 public value;           // Slot 1 - Now in wrong slot
    mapping(address => uint256) public users;  // Slot 2 - Wrong slot!
    address public owner;           // Slot 3 - Wrong slot!
}

// V2 Contract - CORRECT, preserves storage layout  
contract MyContractV2 {
    uint256 public value;           // Slot 0 - Same as V1
    mapping(address => uint256) public users;  // Slot 1 - Same as V1
    address public owner;           // Slot 2 - Same as V1
    uint256 public newValue;        // Slot 3 - New addition
}

Test upgrade failures:

it("should reject invalid upgrades", async function() {
  const ContractV1 = await ethers.getContractFactory("MyContractV1");
  const proxy = await upgrades.deployProxy(ContractV1, [1000]);
  
  const BadContractV2 = await ethers.getContractFactory("BadContractV2");
  
  // This should fail storage layout validation
  await expect(
    upgrades.upgradeProxy(proxy.address, BadContractV2)
  ).to.be.revertedWith(/New storage layout is incompatible/);
});
Q

How do I test complex DeFi interactions with multiple protocols?

A

Multi-protocol testing is where you find out if your integration actually works or just works in isolation. Most DeFi exploits happen at protocol boundaries.

Fork mainnet and test against real protocols:

// Test your protocol's interaction with Uniswap, Aave, Compound simultaneously
describe("Multi-protocol integration", function() {
  let uniswapRouter, aavePool, compoundCEth;
  let user;

  beforeEach(async function() {
    // Fork mainnet at known good block
    await network.provider.request({
      method: "hardhat_reset",
      params: [{
        forking: {
          jsonRpcUrl: process.env.MAINNET_RPC_URL,
          blockNumber: 18500000
        }
      }]
    });

    // Get contracts at their mainnet addresses
    uniswapRouter = await ethers.getContractAt("IUniswapV2Router02", "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D");
    aavePool = await ethers.getContractAt("IPool", "0x87870Bce3F85c7CD9b8DF5F2b0b7e5c0b3c8e7f5");
    compoundCEth = await ethers.getContractAt("ICEth", "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5");
    
    // Impersonate a whale for testing
    await network.provider.request({
      method: "hardhat_impersonateAccount",
      params: ["0x8ba1f109551bD432803012645Hac136c"]
    });
    user = await ethers.getSigner("0x8ba1f109551bD432803012645Hac136c");
  });

  it("should handle complex arbitrage scenario", async function() {
    // 1. Borrow from Aave
    const borrowAmount = ethers.utils.parseEther("100");
    await aavePool.connect(user).borrow(WETH_ADDRESS, borrowAmount, 2, 0, user.address);
    
    console.log("Borrowed from Aave:", ethers.utils.formatEther(borrowAmount));
    
    // 2. Swap on Uniswap
    const swapTx = await uniswapRouter.connect(user).swapExactETHForTokens(
      0,
      [WETH_ADDRESS, USDC_ADDRESS],
      user.address,
      Math.floor(Date.now() / 1000) + 300,
      { value: borrowAmount }
    );
    
    const receipt = await swapTx.wait();
    console.log("Uniswap swap gas used:", receipt.gasUsed.toString());
    
    // 3. Lend to Compound
    const usdcBalance = await usdc.balanceOf(user.address);
    await compoundCUsdc.connect(user).mint(usdcBalance);
    
    // 4. Check if profitable (this is where most strategies fail)
    const compoundBalance = await compoundCUsdc.balanceOf(user.address);
    const aaveDebt = await aavePool.getUserAccountData(user.address);
    
    console.log("Compound cUSDC balance:", compoundBalance.toString());
    console.log("Aave debt:", aaveDebt.totalDebtETH.toString());
    
    // Test should verify arbitrage profitability
    expect(compoundBalance).to.be.gt(expectedMinimumProfit);
  });
});

Test failure scenarios that happen in production:

it("should handle Uniswap slippage protection", async function() {
  // Set unrealistic slippage protection
  const highMinAmountOut = ethers.utils.parseEther("1000"); // Expecting too much
  
  await expect(
    uniswapRouter.swapExactETHForTokens(
      highMinAmountOut,  // This will fail due to slippage
      [WETH_ADDRESS, USDC_ADDRESS],
      user.address,
      Math.floor(Date.now() / 1000) + 300,
      { value: ethers.utils.parseEther("1") }
    )
  ).to.be.revertedWith("UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT");
});
Q

How do I test front-running and MEV scenarios?

A

MEV testing is critical for any protocol that'll run on mainnet. Your perfect arbitrage strategy means nothing if MEV bots can front-run every transaction.

Simulate front-running:

describe("MEV resistance", function() {
  it("should resist front-running attacks", async function() {
    const victim = await ethers.getSigner(0);
    const attacker = await ethers.getSigner(1);
    
    // Victim's transaction - profitable arbitrage opportunity
    const victimTx = await myContract.populateTransaction.profitableArbitrage(
      ethers.utils.parseEther("10")
    );
    
    // Attacker sees mempool and tries to front-run with higher gas
    const attackerTx = await myContract.connect(attacker).populateTransaction.profitableArbitrage(
      ethers.utils.parseEther("10")
    );
    attackerTx.gasPrice = ethers.utils.parseUnits("50", "gwei"); // Higher gas price
    
    // Simulate both transactions in same block (attacker first due to higher gas)
    await network.provider.send("evm_setAutomine", [false]);
    
    // Submit attacker transaction first (higher gas price)
    await attacker.sendTransaction(attackerTx);
    // Submit victim transaction second  
    await victim.sendTransaction(victimTx);
    
    // Mine block - attacker's tx should execute first
    await network.provider.send("evm_mine");
    
    // Victim's transaction should fail or be unprofitable
    const victimBalance = await myToken.balanceOf(victim.address);
    const attackerBalance = await myToken.balanceOf(attacker.address);
    
    expect(attackerBalance).to.be.gt(victimBalance);
    console.log("Attacker profit:", ethers.utils.formatEther(attackerBalance));
  });
});

Test sandwich attacks:

it("should handle sandwich attack scenarios", async function() {
  // User wants to buy tokens
  const userBuyAmount = ethers.utils.parseEther("5");
  
  // MEV bot front-runs with large buy (pumps price)
  const frontRunTx = await uniswapRouter.swapExactETHForTokens(
    0,
    [WETH_ADDRESS, TARGET_TOKEN],
    mevBot.address,
    deadline,
    { value: ethers.utils.parseEther("50") } // Large buy
  );
  
  // User's transaction executes at higher price
  const userTx = await uniswapRouter.swapExactETHForTokens(
    0,
    [WETH_ADDRESS, TARGET_TOKEN], 
    user.address,
    deadline,
    { value: userBuyAmount }
  );
  
  // MEV bot back-runs with sell (dumps price, keeps profit)
  const backRunTx = await uniswapRouter.swapExactTokensForETH(
    await targetToken.balanceOf(mevBot.address),
    0,
    [TARGET_TOKEN, WETH_ADDRESS],
    mevBot.address,
    deadline
  );
  
  // Check MEV bot profitability
  const mevProfit = await ethers.provider.getBalance(mevBot.address);
  console.log("MEV bot sandwich profit:", ethers.utils.formatEther(mevProfit));
  
  // User should have received less tokens than expected
  const userTokens = await targetToken.balanceOf(user.address);
  expect(userTokens).to.be.lt(expectedTokensWithoutMEV);
});
Q

How do I test contract behavior under extreme conditions?

A

Stress testing finds edge cases that break contracts under unusual market conditions - like UST depeg, LUNA collapse, or FTX liquidations.

Test extreme market movements:

describe("Extreme conditions testing", function() {
  it("should handle 90% price drops", async function() {
    // Set up normal market conditions
    await mockPriceOracle.setPrice(WETH_ADDRESS, ethers.utils.parseEther("2000")); // $2000 ETH
    
    // User deposits collateral
    await collateralVault.deposit(ethers.utils.parseEther("10"), { value: ethers.utils.parseEther("10") });
    
    // User borrows against collateral
    await lendingPool.borrow(USDC_ADDRESS, ethers.utils.parseUnits("15000", 6)); // Borrow $15k against $20k collateral
    
    // Simulate extreme price crash - 90% drop
    await mockPriceOracle.setPrice(WETH_ADDRESS, ethers.utils.parseEther("200")); // ETH crashes to $200
    
    // This should trigger liquidation
    const isLiquidatable = await lendingPool.isLiquidatable(user.address);
    expect(isLiquidatable).to.be.true;
    
    // Test liquidation mechanism works
    await lendingPool.connect(liquidator).liquidate(
      user.address,
      USDC_ADDRESS,
      ethers.utils.parseUnits("5000", 6) // Liquidate $5k of debt
    );
    
    // User should lose collateral but reduce debt
    const remainingDebt = await lendingPool.getDebt(user.address);
    expect(remainingDebt).to.be.lt(ethers.utils.parseUnits("10000", 6));
  });
  
  it("should handle zero liquidity scenarios", async function() {
    // Drain all liquidity from pool
    const totalLiquidity = await liquidityPool.totalLiquidity();
    await liquidityPool.connect(whale).withdraw(totalLiquidity);
    
    // Operations should fail gracefully or revert with clear messages
    await expect(
      liquidityPool.connect(user).withdraw(ethers.utils.parseEther("1"))
    ).to.be.revertedWith("Insufficient liquidity");
  });
});

Test integer overflow/underflow edge cases:

it("should handle max uint256 values", async function() {
  const maxUint256 = ethers.constants.MaxUint256;
  
  // Test arithmetic near maximum values
  await expect(
    mathContract.add(maxUint256, 1)
  ).to.be.revertedWith("Math: addition overflow");
  
  // Test safe math functions handle edge cases
  const safeResult = await mathContract.safeAdd(maxUint256.sub(1), 1);
  expect(safeResult).to.equal(maxUint256);
});

Memory and gas limit testing:

it("should handle large array operations", async function() {
  // Test contract behavior with large datasets
  const largeArray = [];
  for (let i = 0; i < 1000; i++) {
    largeArray.push(ethers.utils.parseEther(i.toString()));
  }
  
  // This might hit gas limits or cause memory issues
  const tx = await arrayProcessor.processLargeArray(largeArray);
  const receipt = await tx.wait();
  
  console.log("Gas used for 1000 items:", receipt.gasUsed.toString());
  expect(receipt.gasUsed).to.be.lt(8000000); // Should stay under block gas limit
});

Debugging Tools Comparison: What Actually Works

Feature

Hardhat 3

Foundry

Remix

Truffle

Brownie

Console.log in Solidity

✅ Built-in

✅ Native

✅ Built-in

❌ None

❌ None

Stack Traces with Source Maps

✅ Excellent

✅ Good

⚠️ Basic

⚠️ Limited

❌ Poor

Mainnet Forking

✅ Full support

✅ Anvil

❌ None

⚠️ Via Ganache

✅ Built-in

Gas Profiling

✅ Plugin-based

✅ Built-in

⚠️ Basic

⚠️ Limited

⚠️ Basic

Transaction Tracing

✅ hardhat-tracer

✅ forge trace

❌ None

❌ None

⚠️ Basic

Time Manipulation

✅ Network helpers

✅ vm.warp

❌ None

⚠️ Limited

✅ chain.sleep

State Overrides

✅ Full support

✅ vm.* cheats

❌ None

❌ None

❌ None

Memory Usage (Large Projects)

✅ EDR optimized

✅ Rust efficient

❌ Browser limited

❌ High memory

⚠️ Moderate

Test Performance

✅ 2-10x faster

✅ Fastest

❌ Slow

❌ Very slow

⚠️ Moderate

JavaScript/TypeScript Support

✅ Native

⚠️ Limited

❌ None

✅ Native

✅ Python only

Essential Debugging & Testing Resources

Related Tools & Recommendations

compare
Similar content

Hardhat vs Foundry: Best Smart Contract Frameworks for Devs

Compare Hardhat vs Foundry, Truffle, and Brownie to pick the best smart contract framework. Learn which tools are actively supported and essential for modern bl

Hardhat
/compare/hardhat/foundry/truffle/brownie/framework-selection-guide
100%
tool
Similar content

Hardhat Ethereum Development: Debug, Test & Deploy Smart Contracts

Smart contract development finally got good - debugging, testing, and deployment tools that actually work

Hardhat
/tool/hardhat/overview
80%
tool
Similar content

Foundry Debugging - Fix Common Errors That Break Your Deploy

Debug failed transactions, decode cryptic error messages, and fix the stupid mistakes that waste hours

Foundry
/tool/foundry/debugging-production-errors
72%
tool
Similar content

Foundry: Fast Ethereum Dev Tools Overview - Solidity First

Write tests in Solidity, not JavaScript. Deploy contracts without npm dependency hell.

Foundry
/tool/foundry/overview
61%
tool
Similar content

Debugging Broken Truffle Projects: Emergency Fix Guide

Debugging Broken Truffle Projects - Emergency Guide

Truffle Suite
/tool/truffle/debugging-broken-projects
56%
tool
Similar content

Fix Uniswap v4 Hook Integration Issues - Debug Guide

When your hooks break at 3am and you need fixes that actually work

Uniswap v4
/tool/uniswap-v4/hook-troubleshooting
55%
tool
Similar content

Chainlink: The Industry-Standard Blockchain Oracle Network

Currently securing $89 billion across DeFi protocols because when your smart contracts need real-world data, you don't fuck around with unreliable oracles

Chainlink
/tool/chainlink/overview
52%
tool
Similar content

Arbitrum Production Debugging: Fix Gas & WASM Errors in Live Dapps

Real debugging for developers who've been burned by production failures

Arbitrum SDK
/tool/arbitrum-development-tools/production-debugging-guide
43%
compare
Recommended

Web3.js is Dead, Now Pick Your Poison: Ethers vs Wagmi vs Viem

Web3.js got sunset in March 2025, and now you're stuck choosing between three libraries that all suck for different reasons

Web3.js
/compare/web3js/ethersjs/wagmi/viem/developer-ecosystem-reality-check
42%
tool
Similar content

Hardhat Production Deployment: Secure Mainnet Strategies

Master Hardhat production deployment for Ethereum mainnet. Learn secure strategies, overcome common challenges, and implement robust operations to avoid costly

Hardhat
/tool/hardhat/production-deployment
37%
tool
Similar content

Hardhat 3 Migration Guide: Speed Up Tests & Secure Your .env

Your Hardhat 2 tests are embarrassingly slow and your .env files are a security nightmare. Here's how to fix both problems without destroying your codebase.

Hardhat
/tool/hardhat/hardhat3-migration-guide
36%
tool
Similar content

Hemi Network Bitcoin Integration: Debugging Smart Contract Issues

What actually breaks when you try to build Bitcoin-aware smart contracts

Hemi Network
/tool/hemi/debugging-bitcoin-integration
35%
tool
Similar content

Ethereum Layer 2 Development: EIP-4844, Gas Fees & Security

Because mainnet fees will bankrupt your users and your sanity

Ethereum
/tool/ethereum/layer-2-development
34%
howto
Similar content

Deploy Smart Contracts on Optimism: Complete Guide & Gas Savings

Stop paying $200 to deploy hello world contracts. Here's how to use Optimism like a normal person.

/howto/deploy-smart-contracts-optimism/complete-deployment-guide
33%
tool
Similar content

Stacks Blockchain: Bitcoin Smart Contracts & Development Guide

Bitcoin L2 for smart contracts that actually inherits Bitcoin security - works way better since the October 2024 upgrade.

Stacks Blockchain
/tool/stacks/overview
31%
howto
Similar content

Arbitrum Layer 2 dApp Development: Complete Production Guide

Stop Burning Money on Gas Fees - Deploy Smart Contracts for Pennies Instead of Dollars

Arbitrum
/howto/develop-arbitrum-layer-2/complete-development-guide
30%
tool
Similar content

Ethereum Overview: The Least Broken Crypto Platform Guide

Where your money goes to die slightly slower than other blockchains

Ethereum
/tool/ethereum/overview
29%
alternatives
Similar content

Hardhat Migration Guide: Ditch Slow Tests & Find Alternatives

Tests taking 5 minutes when they should take 30 seconds? Yeah, I've been there.

Hardhat
/alternatives/hardhat/migration-difficulty-guide
28%
tool
Similar content

Slither: Smart Contract Static Analysis for Bug Detection

Built by Trail of Bits, the team that's seen every possible way contracts can get rekt

Slither
/tool/slither/overview
28%
tool
Similar content

Anchor Framework Production Deployment: Debugging & Real-World Failures

The failures, the costs, and the late-night debugging sessions nobody talks about in the tutorials

Anchor Framework
/tool/anchor/production-deployment
28%

Recommendations combine user behavior, content similarity, research intelligence, and SEO optimization