Pentesting Web3: Setting Up a Smart Contract Testing Environment

11 min read

November 3, 2024

Web3 Exploitation Fundamentals: Navigating Security in Decentralized Systems
Pentesting Web3: Setting Up a Smart Contract Testing Environment

Table of contents

Introduction to Web3 and Blockchain Security

As the internet moves toward a new era known as Web3, we’re seeing a shift from centralized platforms—where companies control data and infrastructure—to a decentralized model that places more power in the hands of users. This new internet, built on blockchain technology, introduces exciting possibilities with decentralized applications (dApps) and smart contracts that operate independently, without any single authority.

But with this shift comes a unique set of security challenges. Unlike traditional systems where security issues can often be quickly patched, Web3's reliance on blockchain's immutable structure makes fixing vulnerabilities after deployment much more complex. As a result, understanding and securing Web3 applications is more critical than ever.

This article series will walk you through the fundamentals of Web3 and blockchain technology, covering everything from key terminology to hands-on smart contract deployment. Along the way, we’ll explore common vulnerabilities and dive into tools and techniques to keep decentralized systems safe. By the end, you’ll not only understand the power of Web3 but also be equipped with the skills to protect it.

Let’s start by exploring the essential concepts behind Web3, blockchain, and smart contracts and discover what makes security such a top priority in this new era of the internet.

How Does Blockchain Work?

To really get Web3, it helps to understand how blockchain—the technology behind it—all fits together. At its core, blockchain is like a super-secure, digital ledger for tracking transactions and assets, but with a twist. Instead of being stored on a single, centralized server controlled by one authority, blockchain is spread out across a network of computers, where each one (called a “node”) holds a complete copy of the ledger. This decentralized structure is what gives Web3 its strength and independence.

Here’s how it works: every time there’s a new entry, it’s stored in a "block" that includes recent transactions, a timestamp, and a unique identifier, or “hash.” Each new block links to the one before it by referencing that previous block's hash, creating an unbreakable “chain” of data. Thanks to this chaining, if anyone tries to alter information in a block, it will change the hash, which breaks the chain and alerts the network. This setup makes blockchain incredibly hard to tamper with.

One of blockchain’s standout features is its immutability. Once something is recorded on the blockchain, changing it is almost impossible. This quality is perfect for Web3 applications that prioritize security, transparency, and trust. But, there’s a downside: in traditional databases, you can update records to fix errors. On the blockchain, mistakes or code vulnerabilities stick around, which can be a headache to correct.

What is a Smart Contract?

Smart contracts are at the heart of Web3 and form the backbone of most decentralized apps, or dApps. Put simply, a smart contract is a self-running program on the blockchain that automatically enforces a set of rules between different parties. Think of it as a digital contract that doesn’t need middlemen—once the conditions set in the contract are met, it just takes action on its own.

Traditional contracts usually require trust in a third party, like a lawyer or a bank. But with smart contracts, there’s no need for that; they’re “trustless,” meaning they rely purely on blockchain’s code and consensus rules. For example, in a crowdfunding campaign, a smart contract could automatically release funds to the project creator if they reach their goal by a specific date. If not, the money gets returned to the backers—no middleman required.

Smart contracts are typically written in programming languages like Solidity (a popular choice for Ethereum-based contracts) and live on the blockchain, making them transparent and easy to verify. However, once a smart contract is deployed, its code is set in stone. This immutability is both an advantage and a drawback: while it ensures transparency and security, it also means any bugs or vulnerabilities can’t be easily fixed.

Since smart contracts often manage valuable assets, security is key. If there’s a flaw in the code, attackers could exploit it, potentially leading to big financial losses for users. One well-known example of this is the 2016 DAO Hack, where millions were lost due to a vulnerability in the contract’s code. This is why secure coding practices and thorough testing are essential when developing smart contracts.

Key Blockchain Terminology

Before diving into smart contract deployment and interaction, it's important to familiarize yourself with some foundational blockchain terms that haven’t been covered yet but are essential for understanding the ecosystem. These concepts will frequently come up when working with smart contracts, exploring blockchain structures, and analyzing transactions. Here’s a quick guide to these essential terms:

Account

Think of an account as your “profile” on the blockchain, which has its own unique address (a long hexadecimal code) and balance. There are two main types of accounts, and each has a specific role:

  1. Externally Owned Account (EOA):
    This type of account is managed by a private key (think of it like a password) that you control through a wallet, such as MetaMask. EOAs are able to send transactions, including those that interact with smart contracts. Essentially, this is the “personal” account you use to control assets and initiate actions on the blockchain.
  2. Contract Account:
    A contract account is created when a smart contract is deployed on the blockchain. Unlike EOAs, contract accounts don’t have private keys and can’t initiate actions on their own. Instead, they react to transactions initiated by EOAs or other contracts. This account type represents the smart contracts themselves.

Transaction

Transactions are how accounts interact with each other and with the blockchain. A transaction is any action that changes the state of the blockchain—whether it’s transferring cryptocurrency or running a function in a smart contract. Every transaction has three main parts:

  1. Sender: The account initiating the transaction.
  2. Recipient: The account receiving it.
  3. Value: The amount of cryptocurrency (if any) being sent.

Each transaction also includes a gas price and gas limit, which cover the cost of computation needed to process the transaction. After being verified by validators, a transaction is permanently recorded on the blockchain.

Validator

Validators are critical for keeping the blockchain accurate and secure. They verify each transaction against network rules, ensuring it’s legitimate before adding it to the blockchain. Validators are compensated with gas fees for their work. The selection process for validators varies by consensus mechanism (e.g., Proof of Stake or Proof of Work), but their core function remains the same: maintaining blockchain integrity by validating transactions.

Gas

Gas is a fee users pay for blockchain transactions, compensating validators for the computational work involved. Each transaction requires a certain amount of gas, calculated as gas used × gas price:

  • Gas Price: The amount of Ether (ETH) a user is willing to pay per unit of gas, usually measured in gwei (1 gwei = 0.000000001 ETH). A higher gas price can speed up transaction processing, as validators prioritize these transactions.
  • Gas Limit: The maximum gas a user is willing to pay for a transaction. More complex transactions (e.g., smart contract interactions) need higher limits. If the limit is too low, the transaction fails, but any gas consumed up to that point is still charged.

Gas Fees

Gas fees are the total cost a user pays to execute a transaction on the blockchain. Fees are deducted from the user’s balance and given to the validator. During peak network activity, gas fees can increase as users compete to have their transactions processed faster.

Nonce

A nonce is a unique number associated with each account’s transactions. It helps keep transactions in the correct order and prevents double-spending (sending the same transaction twice). Each time an account sends a new transaction, its nonce increments, ensuring the transaction sequence remains accurate.

Token

A token represents a digital asset on the blockchain, commonly following standards like ERC-20 for fungible tokens (identical units, like currency) and ERC-721 for non-fungible tokens (NFTs) (unique items, like digital collectibles). Tokens serve various roles within decentralized apps (dApps), such as representing assets, utilities, or value, and can be transferred or traded.

Wallet

A wallet is an app that helps users manage accounts, store private keys, and interact with the blockchain. Popular wallets like MetaMask allow users to send transactions, store tokens, and connect to dApps. Importantly, wallets don’t actually store assets; they store private keys, which control access to blockchain assets. Keeping private keys secure is crucial, as they’re the only way to access your account.

Explorer

A blockchain explorer (such as Etherscan for Ethereum) is a tool for viewing all public data on the blockchain, including transactions, blocks, accounts, and smart contracts. Explorers provide transparency, enabling users to verify transactions, monitor account activity, and even review smart contract code.

How It All Connects

Let’s go through an example that ties together the different concepts we’ve covered to understand them more clearly. Imagine Alex wants to use a decentralized finance (DeFi) app to earn interest by lending cryptocurrency.

To start, Alex needs an account on the blockchain, which works like their profile and has a unique address tied to a private key. This account is managed through a wallet app like MetaMask, where the private key is securely stored. Think of this private key as a super-secure password that only Alex should have. With the wallet, Alex can directly interact with the blockchain from their computer or phone. To join the DeFi lending pool, Alex initiates a transaction to transfer some cryptocurrency to the DeFi application’s smart contract.

This transaction doesn’t just move funds from Alex’s account; it actually communicates with the smart contract—a self-executing piece of code that runs on the blockchain. In this case, the contract manages the lending pool, accepting deposits, calculating interest, and keeping track of who has contributed funds. When Alex submits this transaction, their wallet app “signs” it using their private key, making sure that the transaction is authentic and can’t be altered by anyone else.

Once signed, the transaction is broadcast to the blockchain network, where it awaits approval from validators. Validators are crucial players on the blockchain who confirm and record transactions. They ensure that each transaction follows the rules and, in exchange, they’re paid a fee, known as gas. Alex sets both a gas price (how much they’re willing to pay per unit of gas) and a gas limit (the max amount they’ll pay to process this transaction). Setting a higher gas price can speed things up since validators will prioritize transactions with higher fees. After the transaction is verified by the validators, it’s added to a new block on the blockchain—a block that contains this and other transactions. Once the block is completed, it’s linked to the chain of previous blocks, creating a permanent record of Alex’s transaction.

Once that’s done, the DeFi app’s smart contract confirms Alex’s deposit by issuing a token back to Alex’s account. This token is an ERC-20 token (a popular standard on Ethereum), and it represents Alex’s share in the lending pool. It’s a bit like a receipt that proves Alex has contributed to the pool and may start earning interest over time, depending on how the smart contract is set up.

At any time, Alex can use a blockchain explorer like Etherscan to track their transaction, check their token balance, or review details about the DeFi smart contract. Blockchain explorers provide transparency into all this activity, letting users see what’s happening across the network.

Deploying Your Smart Contract Locally with Ganache UI

After understanding all the basic concepts, we’re ready to set up a local blockchain environment, deploy a smart contract, and start interacting with it. You can use any code editor you prefer—VS Code is a popular choice, but any editor will work just fine. In the upcoming chapters, we’ll dive into common vulnerabilities and explore strategies to secure your contracts effectively.

To get started, open your code editor and create a new project folder—let’s call it "Ether." In your terminal, navigate to the project directory and initialize a Hardhat project with the following commands:

npm install --save-dev hardhat
npx hardhat init
Hardhat CLI

Next, we’ll create the smart contract. Inside the contracts folder, delete any sample contracts (like Greeter.sol) and create a new file called SimpleStorage.sol. Open this file and add the following Solidity code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SimpleStorage {
    uint256 private storedData;

    function set(uint256 x) public {
        storedData = x;
    }

    function get() public view returns (uint256) {
        return storedData;
    }
}

Our SimpleStorage contract is straightforward—it has a variable called storedData and two functions. The set function allows anyone to store a value in storedData, while the get function retrieves it. Once the contract is written, compile it by running:

Compiling the SmartContract

Hardhat will compile your contract and store the output files in the artifacts folder.

Now, let’s set up Ganache. Open Ganache UI, select Quickstart Ethereum, and it will create a local blockchain with 10 pre-funded accounts. The default RPC server for Ganache is http://127.0.0.1:7545. In Ganache, you’ll see a list of accounts—click the key icon next to one to reveal its private key and copy it.

Accessing the private key of the account

Then, open hardhat.config.js and add the Ganache network configuration:

require("@nomicfoundation/hardhat-toolbox");

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.27",
  networks: {
    ganache: {
      url: "http://127.0.0.1:7545", // Ganache's default RPC server address
      accounts: [
        "0xd6d215c98c4fd42ddb7b1a8ab89275bc284412267ef3f300cb61ef6fcb0d4d4e",
      ], // Replace with a private key from Ganache UI
    },
  },
};


Replace "0xYourPrivateKeyHere" with the actual private key from Ganache, including the 0x prefix.

With everything configured, it’s time to deploy! In the scripts folder, create a new file called deploy.js and add the following code:

const hre = require("hardhat");

async function main() {
  const SimpleStorage = await hre.ethers.getContractFactory("SimpleStorage");
  const simpleStorage = await SimpleStorage.deploy();
  await simpleStorage.waitForDeployment();

  console.log("SimpleStorage deployed to:", simpleStorage.target);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

This script uses Hardhat’s ethers library to deploy the SimpleStorage contract and logs the contract’s address to the console. To deploy to the Ganache network, run:

npx hardhat run scripts/deploy.js --network ganache

Once you run the command, you should see the contract’s address in the terminal. To double-check, open Ganache UI and look under the Transactions tab for the latest transaction. You’ll see the contract creation with the contract address included.

Deploying the contract
Contract deployed in Ganache

To interact with the contract, open the Hardhat console and specify Ganache as the network:

npx hardhat console --network ganache

In the console, you can attach to the deployed contract and call its functions. Start by attaching to the contract:

const SimpleStorage = await ethers.getContractFactory("SimpleStorage");
const simpleStorage = await SimpleStorage.attach("YOUR_CONTRACT_ADDRESS"); // Replace with the actual address

To set a value, use the set function as follows:

await simpleStorage.set(42);

Then, retrieve the stored value with the get function:

const value = await simpleStorage.get();
console.log("Stored Value:", value.toString());

Each interaction with the contract will appear as a transaction in Ganache UI, where you can view details about gas usage, account balances, and transaction status.

Using hardhat console to interact with the netwrok
Successful transaction

Conclusions

In this first chapter, we’ve covered the basics of Web3 concepts and set up a local environment for deploying smart contracts. Understanding these fundamentals is essential as we begin exploring how to identify and analyze vulnerabilities within decentralized applications.

In upcoming chapters, we’ll examine real-world vulnerabilities in smart contracts, diving into how they can be exploited and how to defend against these attacks. From reentrancy issues to insecure token implementations, each section will break down specific attack vectors and mitigation strategies, equipping you with the skills to secure Web3 projects effectively.

Thank you for joining me on this journey into Web3 security! Stay tuned as we delve deeper into practical examples, uncover common weaknesses, and develop a robust toolkit for securing the decentralized web.

Resources

Chapters

Exploiting Predictable Randomness in Ethereum Smart Contracts

Next chapter