Skip to the content.

Module 05 — Tools & Frameworks Deep Dive

Difficulty: Intermediate

Mastering your tools is as important as understanding vulnerabilities. This module provides hands-on instructions for every major Web3 security tool — installation, configuration, real-world usage examples, and expert tips.


5.1 Foundry (Forge, Cast, Anvil)

Foundry is the industry-standard framework for smart contract development and security testing. It’s written in Rust, fast, and designed for serious auditors.

Setup

1
2
3
4
5
6
7
8
9
10
# Install
curl -L https://foundry.paradigm.xyz | bash
foundryup

# Initialize a new project
forge init my-audit && cd my-audit

# Install dependencies
forge install OpenZeppelin/openzeppelin-contracts
forge install foundry-rs/forge-std

Forge — Testing & Fuzzing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Run all tests
forge test

# Run tests with verbosity (see traces on failure)
forge test -vvvv

# Run a specific test
forge test --mt test_reentrancy -vvvv

# Fuzz testing (runs random inputs)
forge test --mt testFuzz_ --fuzz-runs 10000

# Invariant testing
forge test --mt invariant_ --fuzz-runs 5000

# Fork testing (test against mainnet state)
forge test --fork-url https://eth-mainnet.g.alchemy.com/v2/KEY --fork-block-number 18500000

# Coverage
forge coverage --report summary
forge coverage --report lcov  # For VS Code Coverage Gutters

Foundry Cheatcodes for Pentesters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Essential cheatcodes for exploit PoCs
vm.prank(address)              // Sets msg.sender for the next call
vm.startPrank(address)         // Sets msg.sender for all subsequent calls
vm.deal(address, uint256)      // Sets ETH balance of an address
vm.warp(uint256)               // Sets block.timestamp
vm.roll(uint256)               // Sets block.number
vm.store(address, bytes32, bytes32)  // Write directly to storage slot
vm.load(address, bytes32)      // Read storage slot
vm.expectRevert()              // Next call should revert
vm.expectEmit()                // Assert event emission
vm.createFork(url)             // Create a fork
vm.selectFork(forkId)          // Switch between forks
vm.makePersistent(address)     // Address persists across fork switches
vm.label(address, string)      // Label address in traces for readability
vm.sign(uint256, bytes32)      // Sign with a private key
vm.addr(uint256)               // Derive address from private key
vm.envUint(string)             // Read environment variable

Cast — Live Chain Interaction

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Read contract state
cast call 0xContract "balanceOf(address)(uint256)" 0xUser --rpc-url $ETH_RPC

# Send a transaction
cast send 0xContract "transfer(address,uint256)" 0xTo 1000000 \
  --private-key $PK --rpc-url $ETH_RPC

# Decode calldata
cast calldata-decode "swap(uint256,uint256,address,bytes)" 0x022c0d9f...

# Look up function selector
cast 4byte 0xa9059cbb

# Get storage slot
cast storage 0xContract 0 --rpc-url $ETH_RPC

# Get contract code
cast code 0xContract --rpc-url $ETH_RPC

# Compute storage slot for mapping
cast index address 0xKey 3  # mapping at slot 3, key = 0xKey

# Trace a transaction
cast run 0xTxHash --rpc-url $ETH_RPC

# Estimate gas for a call
cast estimate 0xContract "transfer(address,uint256)" 0xTo 100 --rpc-url $ETH_RPC

# Convert units
cast to-wei 1.5 ether           # 1500000000000000000
cast from-wei 1000000000000000000  # 1.0
cast --to-base 255 hex            # 0xff

Anvil — Local Chain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Start local chain
anvil

# Fork mainnet at a specific block
anvil --fork-url $ETH_RPC --fork-block-number 18500000

# Start with specific accounts
anvil --accounts 10 --balance 10000

# Mine blocks on demand (no auto-mining)
anvil --no-mining

# Impersonate any address (useful for testing admin functions)
cast rpc anvil_impersonateAccount 0xWhaleAddress
cast send 0xContract "adminFunction()" --from 0xWhaleAddress --unlocked

5.2 Hardhat

Setup & Plugin Ecosystem

1
2
3
4
5
6
7
8
# Initialize
npx hardhat init

# Key plugins for security
npm install @nomicfoundation/hardhat-toolbox
npm install hardhat-gas-reporter
npm install solidity-coverage
npm install @openzeppelin/hardhat-upgrades

Mainnet Forking

1
2
3
4
5
6
7
8
9
10
11
// hardhat.config.js
module.exports = {
  networks: {
    hardhat: {
      forking: {
        url: "https://eth-mainnet.g.alchemy.com/v2/KEY",
        blockNumber: 18500000, // Pin to specific block for reproducibility
      },
    },
  },
};

Console.log Debugging

1
2
3
4
5
6
7
8
// Import in Solidity — only works in Hardhat tests
import "hardhat/console.sol";

function complexLogic(uint256 x) internal {
    console.log("Value of x:", x);
    console.log("Sender:", msg.sender);
    console.log("Balance:", address(this).balance);
}
1
2
3
4
5
6
7
8
# Run tests with gas reporter
REPORT_GAS=true npx hardhat test

# Run coverage
npx hardhat coverage

# Compile with optimizer details
npx hardhat compile --force

5.3 Slither — Static Analysis

Installation & Basic Usage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Install
pip3 install slither-analyzer

# Run all detectors
slither .

# Run specific detectors
slither . --detect reentrancy-eth,unchecked-lowlevel,arbitrary-send-eth

# Export to JSON for processing
slither . --json slither-output.json

# Print contract summary
slither . --print human-summary

# Print function summary (call graph)
slither . --print function-summary

# Print inheritance graph
slither . --print inheritance-graph

# Print storage layout
slither . --print variable-order

Key Detectors for Auditors

Detector Severity Description
reentrancy-eth High Reentrancy with ETH transfer
reentrancy-no-eth Medium Reentrancy without ETH
arbitrary-send-eth High Functions sending ETH to arbitrary address
controlled-delegatecall High Delegatecall to user-controlled address
suicidal High Functions allowing self-destruct
unprotected-upgrade High Upgradeable proxy without access control
unchecked-lowlevel Medium Unchecked low-level call return value
unchecked-transfer Medium Unchecked ERC20 transfer return value
uninitialized-storage High Uninitialized storage variables
timestamp Low Dangerous use of block.timestamp
weak-prng High Weak randomness source

Writing Custom Detectors

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# custom_detector.py
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification
from slither.core.cfg.node import NodeType

class MyCustomDetector(AbstractDetector):
    ARGUMENT = "my-custom-detector"
    HELP = "Detect custom vulnerability pattern"
    IMPACT = DetectorClassification.HIGH
    CONFIDENCE = DetectorClassification.HIGH

    WIKI = "Custom detector for XYZ vulnerability"

    def _detect(self):
        results = []
        for contract in self.compilation_unit.contracts_derived:
            for function in contract.functions:
                # Check for specific pattern
                for node in function.nodes:
                    if self._check_vulnerability(node):
                        info = [function, " has vulnerability pattern\n"]
                        results.append(self.generate_result(info))
        return results

Slither Triage Workflow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1. Run full analysis
slither . 2>&1 | tee slither-full.txt

# 2. Count findings by detector
slither . --json output.json
cat output.json | jq '.results.detectors | group_by(.check) | map({check: .[0].check, count: length})'

# 3. Focus on high-impact findings
slither . --detect reentrancy-eth,arbitrary-send-eth,controlled-delegatecall,suicidal

# 4. Review each finding — classify as:
#   - True positive → write up
#   - False positive → document reason
#   - Informational → note for completeness

5.4 Mythril — Symbolic Execution

Installation & Usage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Install via pip
pip3 install mythril

# Analyze a contract
myth analyze contracts/Vault.sol --solc-json mythril.config.json

# Analyze deployed contract
myth analyze --address 0xContract --rpc infura-mainnet

# Set execution depth
myth analyze contracts/Vault.sol --execution-timeout 300 --max-depth 50

# Check specific SWC
myth analyze contracts/Vault.sol --swc-filter 107,104,101

Mythril Configuration

1
2
3
4
5
6
// mythril.config.json
{
  "remappings": [
    "@openzeppelin/=lib/openzeppelin-contracts/"
  ]
}

Limitations


5.5 Echidna — Property-Based Fuzzing

Installation

1
2
3
4
# Download binary
wget https://github.com/crytic/echidna/releases/download/v2.2.3/echidna-2.2.3-linux.tar.gz
tar -xzf echidna-2.2.3-linux.tar.gz
sudo mv echidna /usr/local/bin/

Writing Echidna Properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
contract EchidnaTest {
    Token token;

    constructor() {
        token = new Token();
        token.mint(address(this), 1000e18);
    }

    // Echidna calls random functions, then checks all echidna_ properties
    // If any property returns false, Echidna reports a bug

    function echidna_total_supply_consistent() public view returns (bool) {
        return token.totalSupply() >= token.balanceOf(address(this));
    }

    function echidna_no_free_tokens() public view returns (bool) {
        return token.balanceOf(address(this)) <= 1000e18;
    }

    // Echidna will try to make these return false by calling
    // transfer(), approve(), burn(), etc. with random inputs
}

Configuration

1
2
3
4
5
6
7
8
# echidna.config.yaml
testMode: "assertion"    # or "property" or "optimization"
testLimit: 50000         # Number of test runs
seqLen: 100              # Max sequence length
shrinkLimit: 5000        # Shrink attempts for counterexample
deployer: "0x10000"
sender: ["0x10000", "0x20000", "0x30000"]
corpusDir: "echidna-corpus"
1
2
3
4
5
# Run Echidna
echidna . --contract EchidnaTest --config echidna.config.yaml

# With corpus management (saves interesting inputs)
echidna . --contract EchidnaTest --config echidna.config.yaml --corpus-dir corpus/

5.6 Certora Prover — Formal Verification

Basics

Certora uses formal verification to prove that properties hold for ALL possible inputs, not just random ones.

Writing CVL Specs

// spec.cvl — Certora Verification Language
methods {
    function totalSupply() external returns (uint256) envfree;
    function balanceOf(address) external returns (uint256) envfree;
    function transfer(address, uint256) external returns (bool);
}

// Rule: Transfer preserves total supply
rule transferPreservesTotalSupply(address to, uint256 amount) {
    env e;
    uint256 supplyBefore = totalSupply();

    transfer(e, to, amount);

    uint256 supplyAfter = totalSupply();
    assert supplyAfter == supplyBefore, "Total supply changed after transfer!";
}

// Invariant: No user has more than total supply
invariant noUserExceedsTotalSupply(address user)
    balanceOf(user) <= totalSupply();
1
2
3
4
# Run Certora
certoraRun contracts/Token.sol --verify Token:specs/token.cvl \
  --solc solc-0.8.20 \
  --msg "Token transfer verification"

5.7 Manticore — Symbolic Execution

1
2
3
4
5
6
7
8
# Install
pip3 install manticore[native]

# Analyze a contract
manticore contracts/Vault.sol --contract Vault --thorough

# Explore all paths
manticore --contract Vault --workspace manticore-output

Manticore explores all execution paths symbolically. It’s slower than Mythril but can go deeper on complex contracts.


5.8 Bytecode Analysis Tools

Heimdall-rs

1
2
3
4
5
6
7
8
9
10
11
# Install
cargo install heimdall

# Decompile a contract
heimdall decompile 0xContractAddress --rpc-url $ETH_RPC

# Decode function selectors
heimdall decode 0xContractAddress --rpc-url $ETH_RPC

# Disassemble
heimdall disassemble 0xContractAddress --rpc-url $ETH_RPC

EtherVM / Panoramix / Dedaub

When to Use Bytecode Analysis

Scenario Recommended Tool
Quick function signature lookup 4byte.directory, cast 4byte
Full decompilation of unverified contract Dedaub, Heimdall-rs
Opcode-level debugging Tenderly debugger, cast run
Compiler bug verification Compare source → expected bytecode → actual bytecode

5.9 Tenderly

Transaction Simulation

1
2
3
4
5
6
# Tenderly CLI
tenderly export --project myproject 0xTxHash

# Simulate a transaction without broadcasting
tenderly simulate --from 0xSender --to 0xContract \
  --input 0xCalldata --value 0 --block 18500000

Fork Testing via API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Create a Tenderly fork via API
const response = await fetch("https://api.tenderly.co/api/v1/account/USERNAME/project/PROJECT/fork", {
    method: "POST",
    headers: {
        "X-Access-Key": TENDERLY_API_KEY,
        "Content-Type": "application/json"
    },
    body: JSON.stringify({
        network_id: "1",
        block_number: 18500000
    })
});
const { simulation_fork } = await response.json();
// Use simulation_fork.rpc_url for testing

5.10 Phalcon / BlockSec

Phalcon is the go-to tool for analyzing past exploit transactions.

Usage Workflow

  1. Go to phalcon.blocksec.com
  2. Enter the exploit transaction hash
  3. View:
    • Invocation flow: Every internal call in tree format
    • Balance changes: Which addresses gained/lost tokens
    • Fund flow: Visual diagram of token movements
  4. Use this to reverse-engineer the exploit for study or PoC recreation

5.11 Scripting with Web3.py / Ethers.js / Viem

Ethers.js Example — Reading Contract State

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const { ethers } = require("ethers");

const provider = new ethers.JsonRpcProvider(process.env.ETH_RPC);

async function readState() {
    // Read storage directly
    const slot0 = await provider.getStorage("0xContract", 0);
    console.log("Slot 0:", slot0);

    // Call a function
    const contract = new ethers.Contract("0xContract", ABI, provider);
    const owner = await contract.owner();
    console.log("Owner:", owner);

    // Get transaction receipt
    const receipt = await provider.getTransactionReceipt("0xTxHash");
    console.log("Gas used:", receipt.gasUsed.toString());
}

Web3.py Example — Monitoring Mempool

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from web3 import Web3

w3 = Web3(Web3.WebsocketProvider("wss://eth-mainnet.g.alchemy.com/v2/KEY"))

def handle_pending(tx_hash):
    tx = w3.eth.get_transaction(tx_hash)
    if tx and tx['to'] == '0xTargetContract':
        print(f"Pending tx to target: {tx_hash.hex()}")
        print(f"  From: {tx['from']}")
        print(f"  Value: {tx['value']}")
        print(f"  Input: {tx['input'][:10]}")  # Function selector

# Subscribe to pending transactions
pending_filter = w3.eth.filter('pending')
for tx_hash in pending_filter.get_new_entries():
    handle_pending(tx_hash)

Tool Selection Decision Matrix

Task Primary Tool Alternative
Writing exploit PoCs Foundry (fork tests) Hardhat
Static analysis (first pass) Slither Aderyn
Fuzzing (property-based) Echidna / Foundry fuzz Medusa
Symbolic execution Halmos (Foundry-native) Mythril, Manticore
Formal verification Certora Halmos (lighter)
Bytecode decompilation Dedaub / Heimdall EtherVM
Live chain interaction Cast (Foundry) ethers.js, web3.py
Transaction debugging Tenderly Cast run
Exploit analysis Phalcon Tenderly
Local chain forking Anvil (Foundry) Hardhat

Key Takeaway: Foundry is the single most important tool in your arsenal. If you master forge, cast, and anvil, you can handle 90% of pentesting tasks. Add Slither for automated detection and Echidna for deeper fuzzing. The remaining tools fill specific niches — learn them as needed for specific engagements.


*← Previous: Audit Methodology Next: Exploit Development →*