Smart Contracts

Chapter 7
Development Tools, Testing and Debugging

Diploma in Blockchain
Smart Contracts   •   Diploma

Development Tools, Testing and Debugging

Introduction

Smart-contract development differs from ordinary application development because deployed code is usually immutable, transactions are publicly observable, and defects can cause irreversible financial loss. A professional workflow must therefore address the complete development life cycle: writing source code, compiling it, testing expected and unexpected behaviour, deploying to a controlled network, debugging failures, verifying source code, and maintaining reproducible changes through version control and continuous integration (CI).

This chapter presents a practical workflow using contemporary Ethereum-compatible tools such as Solidity, Hardhat, Foundry, local blockchain nodes, testnets, and block-explorer verification services. The principles also apply to other EVM-compatible networks.

Learning Objectives

By the end of this chapter, a learner should be able to:

  1. Explain the roles of compilers, development frameworks, local networks, wallets, RPC endpoints and block explorers.
  2. Compile, deploy and verify a contract using a controlled workflow.
  3. Design unit, integration, scenario and property-based tests.
  4. Diagnose common compilation, transaction, assertion and gas-related failures.
  5. Apply version control, secret management and CI practices to smart-contract projects.
  6. Evaluate the limitations of automated tests and the need for security review.

1. The Smart-Contract Development Toolchain

A toolchain is a coordinated set of programs that transforms source code into tested and deployable blockchain software.

ComponentMain function
Solidity compiler (solc)Converts Solidity source into EVM bytecode and an ABI
ABIDescribes callable functions, parameters, events and errors
Development frameworkOrganises compilation, testing, deployment and scripts
Local nodeRuns a private blockchain for fast, repeatable experiments
Wallet or signerAuthorises transactions with a private key
RPC endpointCommunication interface between tools and a blockchain
TestnetPublic network for realistic deployment rehearsal
Block explorerDisplays transactions and may host verified source code
Version-control systemRecords source changes and supports collaboration
CI serviceAutomatically runs checks when code changes

The compiler produces two especially important artifacts. Bytecode is the machine-readable program deployed to the EVM. The ABI (Application Binary Interface) is a structured description that allows clients and tests to encode function calls and decode results. The compiler may also produce source maps, metadata and debugging information.

A compilation failure is different from a transaction failure. Compilation occurs before a transaction is sent. A transaction may then fail because a contract reverts, the supplied data is invalid, the caller lacks permission, or the transaction runs out of gas.

2. Frameworks and Project Structure

Hardhat and Foundry are widely used EVM development frameworks. Hardhat is JavaScript- and TypeScript-friendly and integrates naturally with npm-based projects. Foundry is a fast Rust-based toolkit whose principal commands include forge, cast and anvil; tests are commonly written in Solidity. Both support local networks, scripting and automated tests. The choice should depend on team skills, reproducibility requirements and ecosystem integration rather than fashion.

A maintainable project normally separates concerns:

project/
  contracts/       Solidity source
  test/            automated tests
  scripts/         deployment and operational scripts
  ignition/        or deployments/ deployment modules and records
  docs/            design and operational documentation
  .env.example     names of required variables, never real secrets
  README.md        setup and usage instructions

Compiler configuration should pin a compatible Solidity version and enable appropriate optimisation settings. A compiler version is part of the build identity: changing it can change bytecode, gas use or generated metadata. Production builds should be reproducible, meaning that another authorised developer can obtain the same source, compiler and configuration and reproduce the expected artifacts.

A typical Hardhat workflow is conceptually:

npm install
npx hardhat compile
npx hardhat test
npx hardhat node
npx hardhat run scripts/deploy.ts --network localhost

The precise commands vary by framework and version. Documentation should state the versions used rather than assuming that every command remains unchanged.

3. Local Networks, Testnets and Mainnet

A local network is a private blockchain running on a developer’s machine or in CI. It provides rapid block production, test accounts and controlled state. Local nodes such as Hardhat Network or Anvil are useful for deterministic tests and for inspecting failures without spending real funds.

A testnet is a publicly accessible network intended for development and rehearsal. It exposes applications to realistic RPC behaviour, block timing, wallet configuration and explorer interfaces. However, testnet assets have no intended monetary value and testnets may be reset, congested or discontinued.

Deployment should progress through environments:

unit tests → local integration tests → testnet rehearsal → independent review → production deployment

Environment configuration must identify at least the network, RPC URL, deployer address, chain ID and contract addresses. A chain ID helps prevent signing a transaction for the wrong network. Private keys should not be committed to Git, embedded in source code or placed in unprotected CI logs. Use hardware wallets, encrypted secret stores or CI secret variables for operational keys.

4. Compiling and Deploying Contracts

Consider a minimal ownership contract:

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

contract Counter {
    uint256 public count;
    address public immutable owner;

    event Incremented(address indexed caller, uint256 newCount);

    error NotOwner();

    constructor(uint256 initialCount) {
        owner = msg.sender;
        count = initialCount;
    }

    function increment() external {
        count += 1;
        emit Incremented(msg.sender, count);
    }

    function reset(uint256 newCount) external {
        if (msg.sender != owner) revert NotOwner();
        count = newCount;
    }
}

Compilation checks syntax, type rules, imports and compiler constraints. It does not prove that the design is secure or economically correct. The compiler’s warnings should be reviewed rather than automatically ignored.

A deployment script should make assumptions explicit, accept constructor arguments, record the resulting address and preferably wait for transaction confirmation. A robust script should also prevent deployment to an unintended network and record the chain ID, compiler version, commit identifier and deployment transaction hash.

Deployment has two related outputs: a transaction that creates the contract and the resulting contract address. The address alone is insufficient for auditability. A deployment record should include constructor arguments and artifact information so that the deployed bytecode can later be compared with the intended build.

5. Testing as an Evidence Strategy

Testing is not simply the search for failures. It creates evidence that specified properties hold under selected conditions. No finite test suite proves that a contract is correct for every possible transaction, but a well-designed suite increases confidence and exposes assumptions.

5.1 Unit tests

A unit test isolates a function or small contract behaviour. For Counter, unit tests should check:

  • the constructor stores the initial value;
  • increment increases the value by exactly one;
  • the correct event is emitted;
  • reset succeeds for the owner;
  • reset reverts for another account.

Tests should verify both state and externally visible effects. Merely checking that a transaction does not revert is often insufficient.

A test assertion can be expressed as:

expected post-state = actual post-state

For a counter, if the initial state is c₀ and one successful increment occurs, the expected state is:

c₁ = c₀ + 1

Here c₀ and c₁ are non-negative integer counter values. The relationship applies only when the increment transaction completes successfully; a reverted transaction should not change the state.

5.2 Integration tests

Integration tests exercise several components together, such as a token, a marketplace and a payment token. They examine ABI encoding, contract-to-contract calls, event propagation, permissions and state transitions across transactions. Integration tests are slower and more complex than unit tests, so they should be reserved for interactions that cannot be meaningfully tested in isolation.

5.3 Scenario tests

A scenario test describes a realistic sequence:

  1. deploy a token and marketplace;
  2. mint an item to a seller;
  3. approve the marketplace;
  4. list the item;
  5. purchase it as a buyer;
  6. verify ownership, payment and emitted events.

Scenario tests are valuable because smart-contract defects often occur at transitions between individually correct operations. They should include alternative paths such as cancellation, insufficient payment and repeated purchase attempts.

5.4 Negative and access-control tests

A mature suite gives deliberate attention to failure. Examples include:

  • a non-owner calls an administrative function;
  • a user supplies zero or excessive amounts;
  • a withdrawal is attempted twice;
  • a stale price or expired deadline is used;
  • an invalid token address is supplied;
  • a function is called after a pause.

The expected failure should be specific where possible. Custom errors are generally cheaper and more informative than generic string messages, and tests can check the error selector and arguments.

5.5 Property-based and fuzz testing

Example-based tests select particular inputs. Property-based testing instead states an invariant that should hold across a broad range of generated inputs. A useful property for a balance-preserving transfer is:

B′sender = Bsender − a, B′receiver = Breceiver + a

where a is a valid non-negative transfer amount, B denotes balances before the transaction, and B′ denotes balances after it. The property assumes the sender has at least a units, the receiver is valid, and no fee or rebasing rule applies. If fees exist, the property must be modified to account for the fee rather than incorrectly treating the token as fee-free.

A broader conservation property for a fee-free token is:

∑ B′ᵢ = ∑ Bᵢ

where the sum is over all token holders and the supply is unchanged. This property does not apply to minting, burning or fee mechanisms unless those effects are explicitly included.

Fuzzing is powerful but not magic. Poor input constraints can generate mostly irrelevant reverts, while overly narrow constraints can hide defects. Each invariant should be accompanied by a statement of its assumptions.

5.6 Time, balances and chain state

Tests often need controlled time, account balances and block progression. Local frameworks provide helpers for advancing time, mining blocks and impersonating accounts. Such powers are testing facilities, not ordinary capabilities available to arbitrary contracts. A test that passes only because it uses unrestricted impersonation may not represent production behaviour.

6. Debugging Failed Contracts

Debugging begins by classifying the failure:

FailureTypical causeUseful evidence
Compilation errorSyntax, type or import problemCompiler message and source location
Deployment failureConstructor revert, bad arguments or insufficient fundsDeployment trace and receipt
Call revertInvalid state, permission or inputRevert data, custom error, call trace
Out-of-gas failureExcessive computation or an unbounded loopGas report and trace
Wrong stateIncorrect assumption or orderingBefore/after storage assertions
Wrong networkIncorrect RPC or chain IDNetwork configuration and receipt
Verification failureMismatched compiler or constructor settingsArtifact metadata and explorer response

The transaction receipt indicates whether a transaction succeeded, its gas usage, logs and block inclusion. A receipt does not by itself explain a revert. A trace records internal calls and can reveal which contract call failed. Debuggers may display source-level execution when compilation metadata and source maps are available.

A disciplined debugging sequence is:

  1. Reproduce the failure with the smallest reliable test.
  2. Record network, block, caller, input values and contract addresses.
  3. Determine whether failure occurred during compilation, estimation, execution or assertion.
  4. Inspect revert data and internal call traces.
  5. Compare expected and actual state before and after the transaction.
  6. Fix the cause, add a regression test and rerun the complete suite.

Gas estimation is useful but not a guarantee. A transaction may estimate successfully and still fail if state changes before mining. Conversely, setting an arbitrarily large gas limit does not fix an infinite loop or a logical revert. Unbounded iteration over user-controlled storage is both a correctness and availability risk.

Events are valuable diagnostic evidence, but they are not storage. Off-chain systems should not assume that an event was emitted unless the transaction succeeded, and contracts should not use events as their sole source of authoritative state.

7. Verification and Reproducible Deployment

Source verification allows users to compare published source and compiler settings with the bytecode at a deployed address. Verification improves transparency but is not a security audit. Verified source can still contain flawed business logic, unsafe economic assumptions or privileged administrative powers.

Successful verification normally requires:

  • the exact Solidity compiler version;
  • optimisation enabled or disabled as deployed;
  • optimisation runs, if applicable;
  • identical source files and import paths;
  • matching metadata settings;
  • correct constructor arguments;
  • the correct chain and contract address.

A deployment manifest should be stored with the commit identifier and artifact hash. This creates an auditable link between reviewed source, generated bytecode and the address used by applications.

8. Version Control and Continuous Integration

Git provides the basic control mechanism for collaborative development. Useful practices include small focused commits, protected main branches, pull-request review, meaningful commit messages and tags for releases. Generated artifacts may be excluded or stored according to the team’s reproducibility policy; the important requirement is that the build can regenerate them deterministically.

A CI pipeline for a smart-contract repository commonly performs the following checks:

checkout → install locked dependencies → compile
         → unit tests → integration/fuzz tests
         → formatting and linting → static analysis
         → gas or size checks → artifact publication

CI should use a clean environment and fail when tests, formatting or compilation fail. Dependency versions should be locked, and the pipeline should not expose private keys. Testnet deployment may occur in a separate manually approved job, using a restricted deployer and explicit chain validation.

Static-analysis tools can identify patterns such as reentrancy risks, dangerous external calls, uninitialised variables or compiler warnings. They produce useful signals, not conclusive proof. Coverage reports are similarly informative but limited: high line coverage does not demonstrate that assertions are meaningful or that economic scenarios are correct.

9. Applied Case: Illustrative Escrow Deployment

Background

Assume a small organisation commissions an escrow contract in which a buyer deposits funds for a seller. The contract permits the buyer to release funds after delivery, while an arbiter can resolve disputes.

Problem

An initial implementation passes the happy-path test but fails when the buyer opens a dispute after the seller has requested release. A second problem appears when the release function is called twice: the first call pays the seller, while the second call attempts to pay again.

Analysis

The project team models explicit states: Created, Funded, Released, Refunded and Disputed. Each public function defines permitted starting states and the resulting state. The payment operation is checked against the current state, and the state is updated before an external call where appropriate. Tests cover buyer, seller and arbiter identities; repeated calls; zero-value deposits; dispute deadlines; and failed recipient transfers.

A scenario test then executes:

  1. buyer funds the escrow;
  2. seller requests release;
  3. buyer opens a dispute;
  4. arbiter refunds the buyer;
  5. seller attempts release again.

The final operation must revert, and the balance must remain unchanged. A fuzz test generates sequences of valid and invalid actions and checks that funds cannot be released and refunded in the same completed escrow.

Outcome

The team adds a state-machine model, a one-time settlement flag and regression tests for every discovered failure. The contract is deployed first to a local node, then to a testnet with verified source. The production deployment is approved only after an independent review of the state transitions, external calls, access control and emergency procedures.

Learning Points

The case demonstrates that a happy-path unit test is inadequate for a stateful financial system. Scenario modelling, negative testing, invariants, trace-based debugging and controlled deployment provide complementary evidence. Verification improves transparency, while neither verification nor testing eliminates the need for review and operational governance.

10. Limitations, Governance and Contemporary Practice

Smart-contract testing cannot fully model every blockchain condition. Network reorganisation, changing gas prices, oracle manipulation, token non-standard behaviour and interaction with unknown contracts may remain outside a local test environment. Testnet success is therefore evidence of deployment readiness, not proof of production safety.

Governance must define who controls upgrade keys, pause functions, treasury withdrawals and administrative roles. Privileged roles should be documented, minimised and, where appropriate, protected by multisignature wallets and timelocks. Users should receive clear information about upgradeability and emergency powers.

AI-assisted coding and test generation can accelerate boilerplate creation and suggest edge cases. They can also produce insecure patterns, hallucinated APIs or tests that merely reproduce the implementation’s assumptions. Generated code must be compiled, reviewed, tested and subjected to security analysis by accountable humans. An AI tool must never receive production private keys or confidential audit material without explicit organisational approval.

Best-Practice Checklist

  • Pin compiler and dependency versions.
  • Treat warnings as review items.
  • Use local networks for fast deterministic feedback.
  • Test successful, failing and adversarial paths.
  • State assumptions for every invariant.
  • Use fuzzing alongside example-based tests.
  • Record chain ID, deployment address and constructor arguments.
  • Never commit private keys or secrets.
  • Verify deployed source using exact build settings.
  • Require CI checks and peer review before release.
  • Add a regression test for every discovered defect.
  • Treat verification, coverage and static analysis as evidence, not guarantees.
  • Document administrative powers and emergency procedures.

Chapter Summary

Professional smart-contract development combines compilation, controlled deployment, testing, debugging, verification and disciplined collaboration. Hardhat and Foundry provide complementary frameworks for organising these activities, while local networks offer fast feedback and testnets provide a realistic rehearsal environment. Unit tests check focused behaviour; integration and scenario tests examine interactions and state transitions; property-based tests evaluate invariants across generated inputs. Debugging relies on receipts, revert data, traces, state comparison and minimal reproductions. Reproducible builds, source verification, Git and CI connect reviewed source to deployed bytecode. Finally, testing and tooling reduce risk but do not replace security review, governance, careful key management or responsible operational decisions.

Key Terms

Compiler: Software that translates Solidity into EVM bytecode and related artifacts.

ABI: Application Binary Interface describing contract calls, events and errors.

Bytecode: EVM instructions deployed or executed on a blockchain.

Local network: Private blockchain environment used for development and testing.

Testnet: Public blockchain used to rehearse deployments without intended real-value assets.

RPC: Remote Procedure Call interface used to communicate with a blockchain node.

Signer: Account or wallet capable of authorising transactions.

Unit test: Test of a focused function or component.

Integration test: Test of interactions among multiple components.

Scenario test: Test representing a realistic sequence of operations.

Property-based testing: Testing based on general properties over generated inputs.

Invariant: Condition that should remain true under specified assumptions.

Fuzz testing: Automated testing using many generated or varied inputs.

Revert: EVM execution failure that undoes state changes in the transaction.

Transaction receipt: Record of transaction inclusion, status, gas and logs.

Trace: Detailed record of internal calls and execution steps.

Source verification: Matching published source and build settings to deployed bytecode.

Continuous integration: Automated validation of changes in a shared repository.

Regression test: Test that prevents a previously fixed defect from returning.

Multisignature wallet: Wallet requiring approval from multiple authorised signers.

Review Questions

Short Answer Questions

  1. What are the distinct purposes of bytecode and an ABI?
  2. Why are local blockchain networks useful during development?
  3. Name three categories of smart-contract tests.
  4. What information should be recorded in a deployment manifest?
  5. Why is source verification not equivalent to a security audit?

Long Answer Questions

  1. Explain a complete workflow from Solidity source code to testnet deployment and source verification.
  2. Compare unit, integration, scenario and property-based testing, including the risks each may fail to detect.
  3. Describe a systematic process for debugging a reverted transaction.
  4. Explain how Git and CI improve the reliability and auditability of smart-contract projects.
  5. Discuss the security and governance implications of private keys, upgrade permissions and emergency controls.

Analytical / Application Questions

  1. Design a test strategy for a token marketplace that supports listing, cancellation, purchase and administrative pausing. Identify positive, negative, integration and invariant tests.
  2. A contract passes all happy-path tests but allows a user to withdraw twice after a dispute resolution. Explain how you would reproduce, diagnose, fix and prevent the defect.
  3. A testnet deployment cannot be verified on a block explorer. Develop a diagnostic checklist covering compiler version, optimisation, source files, constructor arguments, network and deployment artifacts.

Multiple Choice Questions with Answers

Question 1

Which artifact primarily describes how clients encode calls to a contract and decode its results?

A. The ABI

B. The block timestamp

C. The gas limit

D. The private key

Correct Answer: A. The ABI

Question 2

What is the principal purpose of a local blockchain network?

A. To guarantee production security

B. To provide a fast and controlled development environment

C. To replace source-code review

D. To create legally binding digital currency

Correct Answer: B. To provide a fast and controlled development environment

Question 3

Which test is most directly concerned with interactions among several contracts?

A. Unit test

B. Integration test

C. Formatting test

D. Compilation test

Correct Answer: B. Integration test

Question 4

A property-based test is primarily designed to test what?

A. Only one manually selected input

B. A general property across generated inputs

C. The visual design of a block explorer

D. The strength of a wallet password

Correct Answer: B. A general property across generated inputs

Question 5

Under the stated fee-free transfer assumptions, a valid transfer of amount a should produce which result?

A. The sender increases by a and the receiver decreases by a

B. Both balances increase by a

C. The sender decreases by a and the receiver increases by a

D. Both balances remain unchanged

Correct Answer: C. The sender decreases by a and the receiver increases by a

Question 6

Which item is most useful for identifying the internal contract call where execution failed?

A. A source-code comment

B. An execution trace

C. A repository README alone

D. A wallet address alone

Correct Answer: B. An execution trace

Question 7

Which practice best protects deployment credentials in CI?

A. Commit the private key to the repository

B. Print the private key in the build log

C. Store credentials in a protected secret-management system

D. Place the key in a public test file

Correct Answer: C. Store credentials in a protected secret-management system

Question 8

What does source verification primarily establish?

A. That the contract is economically profitable

B. That published source and build settings match deployed bytecode

C. That every possible input has been tested

D. That administrative powers do not exist

Correct Answer: B. That published source and build settings match deployed bytecode

Question 9

Which sequence best represents a disciplined deployment progression?

A. Mainnet first, then local testing

B. Local testing, testnet rehearsal, review, then production

C. Source verification, then compilation

D. CI secrets, then public publication

Correct Answer: B. Local testing, testnet rehearsal, review, then production

Question 10

Why should a regression test be added after fixing a discovered defect?

A. To prevent the same defect from silently returning

B. To increase the private-key balance

C. To bypass compiler checks

D. To remove all negative tests

Correct Answer: A. To prevent the same defect from silently returning

Smart Contracts
Chapter 7 of 14