Testing, Debugging and Smart-Contract SecurityIntroductionSmart contracts are programs whose execution changes shared blockchain state and may transfer valuable assets. Once deployed, their code is difficult to alter, and an error can be exploited repeatedly by unknown parties. Testing and security therefore form one engineering discipline rather than two separate activities. Testing asks whether a contract behaves as intended under selected conditions; security analysis asks whether an attacker can cause an unintended outcome, including through conditions that ordinary tests did not anticipate. This chapter presents a professional workflow for testing Solidity contracts with unit, integration, fuzz, invariant and fork tests. It also introduces debugging, static analysis, threat modeling, security review and the principal classes of Ethereum vulnerabilities. Examples use Foundry-style terminology, although the underlying methods apply to Hardhat and other development environments. Learning ObjectivesBy the end of this chapter, a learner should be able to: - distinguish unit, integration, fuzz, invariant and fork testing;
- construct useful positive, negative and boundary test cases;
- interpret traces, revert data and debugging output;
- use static-analysis and dependency-checking tools responsibly;
- model threats using assets, actors, trust boundaries and attack paths;
- identify and mitigate reentrancy, access-control, oracle, front-running, arithmetic, denial-of-service, signature and upgrade risks;
- plan a proportionate code review and security release process.
1. The Security Testing MindsetA contract specification should be expressed as observable properties. For example, a vault specification may state: - A depositor can withdraw no more than the amount credited to that depositor.
- The sum of user balances is consistent with the accounting model.
- Only an authorized role can change the withdrawal limit.
- A failed withdrawal does not permanently lock unrelated users’ funds.
- An emergency pause prevents the specified dangerous operations.
A test is valuable when it distinguishes correct from incorrect behaviour. Merely increasing line coverage is insufficient: a test suite may execute every line while never checking whether the resulting state is safe. Security testing should therefore combine examples, generated inputs, state-machine exploration and adversarial reasoning. A useful test oracle is a rule that determines whether an execution is acceptable. Oracles may be exact outputs, emitted events, revert conditions, balance relationships or invariants. For monetary calculations, avoid relying only on floating-point expectations. Solidity uses integer arithmetic, so expected values should explicitly account for truncation and rounding. 2. Testing Levels and Their Purposes2.1 Unit testsA unit test isolates one contract function or small component. Dependencies are replaced with mocks, controlled addresses or test doubles. Unit tests are fast and precise, making them suitable for arithmetic, access checks, validation and event assertions. A sound unit test normally verifies: - the initial state;
- the successful path;
- each relevant failure path;
- state changes;
- emitted events;
- caller identity and value sent;
- boundary values such as zero, maximum values and exact thresholds.
For example, a withdrawal test should verify both the user’s balance and the contract’s asset balance after execution. It should also verify that an unauthorized caller reverts and that a withdrawal equal to the entire balance succeeds if the specification permits it. 2.2 Integration testsIntegration tests connect multiple contracts or external components. A token, vault, price feed, access manager and router may each work in isolation but fail when their assumptions differ. Integration tests should check interfaces, decimals, allowance flows, callback behaviour, role configuration and failure propagation. A typical sequence is: - deploy token and protocol contracts;
- configure roles and dependencies;
- mint or distribute test assets;
- execute a realistic user workflow;
- inspect balances, events and protocol state;
- repeat the workflow with invalid ordering and hostile callers.
2.3 Fuzz testingFuzz testing generates many input values rather than relying only on manually selected examples. A property-based test might assert that, for every valid amount, a deposit followed by a withdrawal does not create value from nothing. Fuzz tests require carefully defined domains. If an input is unrestricted, most generated values may simply revert and provide little information. Use assumptions sparingly and document them; excessive assumptions can hide bugs by discarding the very inputs that matter. A Foundry-style example is: function testFuzz_DepositWithdraw(uint128 amount) public {
vm.assume(amount > 0);
deal(address(token), alice, amount);
vm.startPrank(alice);
token.approve(address(vault), amount);
vault.deposit(amount);
vault.withdraw(amount);
vm.stopPrank();
assertEq(token.balanceOf(alice), amount);
}The test must also account for fees, rounding and tokens with non-standard transfer behaviour if those are within scope. 2.4 Invariant testingAn invariant is a property that should remain true across every permitted sequence of actions. Invariant testing generates sequences of calls and checks the property after each sequence or at the end of a run. Examples include: - total recorded shares do not exceed the protocol’s share supply;
- a user’s debt is never negative;
- only designated contracts can mint a governance token;
- assets held by a solvency-constrained vault satisfy the accounting relation defined by its design.
Invariants must be precise. “The protocol is solvent” is incomplete unless solvency is defined, including treatment of fees, pending withdrawals, unrealized losses and oracle valuation. A handler contract is often used to constrain actors and generate realistic actions such as deposit, borrow, repay and liquidate. 2.5 Fork testingFork tests execute against a copy of a real network state at a selected block. They are useful for checking interactions with deployed tokens, routers, lending markets, proxies and price feeds. They can reveal decimal mismatches, unsupported token methods, paused dependencies and configuration errors that local mocks conceal. Fork tests are not automatically reproducible: live state can change, and an RPC provider may return incomplete historical data. Pin the chain, block number, compiler settings and dependency versions. Never send transactions or secrets to an untrusted fork environment. 3. A Practical Test ArchitectureA professional repository separates production contracts, unit tests, integration tests, invariant handlers, scripts and deployment configuration. Test fixtures should centralize deployment and common actors, while each test should make its assumptions visible. Use named actors such as admin, alice, bob and attacker, rather than arbitrary addresses. Test both ordinary users and contracts, because msg.sender may be a contract capable of callbacks. Include payable calls with zero, small and large values where relevant. Assertions should cover effects, not only execution. A transaction that does not revert may still transfer funds to the wrong address, emit an incorrect event or leave an authorization role in an unsafe state. Revert tests should check the expected custom error or reason when that distinction is part of the interface. Events are part of the off-chain observability contract. Tests should verify indexed fields, amounts, recipients and event ordering when indexers or governance systems depend on them. 4. Debugging Solidity ProgramsWhen a test fails, first classify the failure: - compilation or type error;
- setup or fixture error;
- unexpected revert;
- incorrect return value;
- incorrect state transition;
- gas exhaustion;
- environmental or fork inconsistency.
A transaction trace is usually more informative than the final revert string. It shows nested calls, caller and callee addresses, function selectors, value transfers, storage operations and revert locations. Trace analysis is especially important for reentrancy and proxy calls, where the failing operation may be several frames below the public function. Useful debugging techniques include: - add temporary assertions at state boundaries;
- emit diagnostic events in a local debugging branch, not in production code;
- decode custom errors from revert data;
- inspect storage slots when proxy or assembly code is involved;
- use a debugger to step through external calls;
- compare a passing and failing trace;
- reduce a fuzz failure to the smallest input and shortest sequence.
A failing fuzz case should be saved as a regression test. Random discovery is valuable only when the defect remains permanently guarded. 5. Static Analysis and Review ToolsStatic-analysis tools inspect code without executing every possible scenario. They can identify reentrancy patterns, dangerous external calls, shadowed variables, unreachable code, unchecked low-level call results and authorization concerns. Examples in the Ethereum ecosystem include Slither, compiler warnings, Mythril-style symbolic analysis, Semgrep rules and dependency scanners. These tools produce findings, not final judgments. A warning may be a false positive, while an unreported business-logic defect may be severe. Each finding should be triaged as: - confirmed vulnerability;
- valid but accepted risk;
- false positive;
- code-quality issue requiring correction.
Record the rationale, affected code, severity, owner and resolution. Compiler warnings should normally be treated as errors unless a documented exception exists. Lock compiler versions, use tested OpenZeppelin dependencies, review transitive dependencies and check for known vulnerabilities. Formal verification can prove specified properties under stated assumptions, but it cannot prove that the specification reflects the business requirement. Audits and verification are complementary to tests, not substitutes for them. 6. Threat Modeling Smart ContractsThreat modeling begins with assets and trust boundaries. Assets may include tokens, shares, governance power, privileged roles, oracle data and upgrade authority. Actors may include ordinary users, malicious contracts, validators or block builders, administrators, relayers and compromised keys. For every externally reachable function, ask: - Who can call it?
- What state and assets can it affect?
- Which assumptions does it make about tokens, oracles or callers?
- Does it perform an external call before internal state is safe?
- Can the transaction be reordered, copied or sandwiched?
- What happens when a dependency reverts, returns false or behaves maliciously?
- Can an administrator or upgrade key violate user expectations?
A threat model should identify preventive controls, detection mechanisms and recovery actions. For example, a timelocked upgrade, multisignature administration, pause mechanism and monitored event stream address different parts of the risk, but none alone guarantees safety. 7. Principal Vulnerability Classes7.1 ReentrancyReentrancy occurs when a contract makes an external call and the callee calls back before the first invocation has completed. The classic failure is updating a balance after sending funds. The attacker repeatedly withdraws against stale accounting. The principal controls are: - checks-effects-interactions: validate conditions, update state, then call externally;
- a reentrancy guard for functions whose call graph requires it;
- pull-payment designs rather than unsolicited pushes;
- minimizing external calls and carefully reviewing callbacks.
A guard is not a universal solution. Cross-function reentrancy, read-only reentrancy and interactions among multiple contracts may remain possible. Tests should use a malicious receiver that re-enters at each relevant callback point. 7.2 Access-control failuresAccess-control defects include missing modifiers, incorrect role administration, unprotected initialization, tx.origin authentication, default-zero administrators and functions that trust user-supplied role identifiers. Authorization should be explicit and tested for every privileged operation. Use least privilege: separate pausing, upgrading, minting and parameter-management roles where practical. Test role grant, revocation, renunciation, unauthorized calls and deployment-time initialization. A multisignature wallet reduces single-key risk but does not correct an incorrectly designed permission model. 7.3 Oracle manipulationProtocols that price collateral, swaps or liquidations depend on data quality. A spot price from a shallow pool can be moved temporarily by a trade, causing an unsafe valuation. Mitigations include time-weighted prices, multiple independent sources, deviation bounds, freshness checks, circuit breakers and conservative liquidation rules. An oracle test should simulate stale data, zero or negative answers where the interface permits them, extreme deviations, sequencer downtime on relevant layer-two systems and feed failure. The protocol must define what happens when the oracle is unavailable; silently using an old value may be dangerous. 7.4 Front-running and transaction orderingPublic pending transactions reveal intent. An adversary may insert transactions before or after a victim, causing slippage, liquidation races or governance manipulation. Mitigations include minimum-output or maximum-input limits, deadlines, commit-reveal schemes, batch auctions, private order flow and designs that do not expose valuable predictable state transitions. A deadline is not a price guarantee, and a private relay is not an absolute security guarantee. Tests should model an adversarial transaction inserted between approval and execution or between two dependent actions. 7.5 Arithmetic and numeric representationSolidity 0.8 and later reverts on ordinary integer overflow and underflow, but explicit unchecked blocks, division truncation, casting and decimal conversion remain important risks. Integer division discards the fractional remainder. If a and b are non-negative integers, Solidity’s integer quotient is ⌊a ÷ b⌋, provided b ≠ 0. A conversion between quantities with decimal scales must preserve units. For example, converting an amount expressed with 6 decimals to 18 decimals multiplies by 1018−6 = 1012, assuming the value is within the destination type’s range. Rounding direction must be selected deliberately: rounding in favour of a borrower, trader or protocol can have different economic consequences. Test zero denominators, maximum integers, narrow casts, fee calculations, exchange-rate rounding and repeated operations. Mathematical correctness does not guarantee economic fairness; a small rounding advantage repeated many times may become material. 7.6 Denial of service and gas exhaustionUnbounded loops over user-controlled arrays can become too expensive to execute. A malicious recipient can also revert in a push-payment loop, blocking payments to everyone. Avoid loops whose upper bound grows without a practical limit. Use pull payments, pagination, bounded batches and withdrawal queues. Gas assumptions can change across network upgrades. Tests should include large but realistic state sizes and hostile recipients. A function that succeeds with three users may become unusable with thirty thousand. 7.7 Signature and replay risksOff-chain signatures require domain separation, correct chain and contract identifiers, nonce management, deadlines and unambiguous typed data. EIP-712-style signing helps users understand structured messages, but implementation must still verify the signer, digest and authorization scope. Replay may occur across chains, contracts, functions or time periods if the signed message lacks an appropriate nonce, chain identifier, verifying contract or expiration. Test altered fields, reused signatures, wrong chain identifiers, expired deadlines, malleable signatures where relevant and signatures submitted by the wrong relayer. 7.8 UpgradeabilityProxy systems separate the user-facing address from implementation logic. Risks include uninitialized implementations, storage-layout collisions, unauthorized upgrades, dangerous delegatecall assumptions and upgrade functions that bypass governance. Storage variables must preserve ordering and compatible types according to the chosen proxy pattern. Upgrade tests should deploy version one, create meaningful state, upgrade to version two, and verify state preservation, access controls, events and expected migrations. Test a malicious or accidental implementation that overwrites a critical slot. A timelock provides transparency and reaction time, but it does not make an unsafe upgrade safe. 8. An Integrated Security Review WorkflowA release candidate should pass a staged process: - Specification review: define assets, actors, trust assumptions, failure behaviour and invariants.
- Unit and negative tests: cover normal, invalid and boundary cases.
- Integration tests: exercise complete workflows and external interfaces.
- Fuzz and invariant testing: explore broad values and action sequences.
- Fork tests: validate real deployments and configuration at pinned blocks.
- Static and dependency analysis: triage findings and document exceptions.
- Manual review: inspect call graphs, authorization, arithmetic, economic incentives and upgrade paths.
- Adversarial review: attempt reentrancy, oracle, ordering, signature and denial-of-service attacks.
- Deployment rehearsal: test initialization, role transfer, pausing, monitoring and rollback procedures.
- Post-deployment monitoring: watch events, balances, oracle health, unusual calls and privileged actions.
Severity should consider exploitability, affected assets, likelihood, detection difficulty and recovery options. A vulnerability that requires a privileged key may still be critical if that key controls billions in assets. 9. Illustrative Case Study: A Vulnerable Lending VaultBackgroundThis illustrative case concerns a lending vault accepting a stablecoin as collateral and allowing users to borrow another token. The vault uses an exchange rate from a single decentralized-exchange pool and has an administrator who can update the price-feed address. ProblemA review identifies three weaknesses. First, liquidation uses the current pool spot price. Second, the administrator can change the feed immediately. Third, repayment signatures contain a user address and amount but no nonce or deadline. AnalysisAn attacker can temporarily trade against the shallow pool, inflate collateral value, borrow more than the collateral is economically worth, and reverse the trade. The immediate feed change creates a centralization and key-compromise risk. A signed repayment can be replayed, potentially authorizing repeated actions. The threat model identifies the pool as a manipulable dependency, the administrator key as a high-impact trust boundary and the signature as a reusable capability. Unit tests alone are unlikely to expose the economic attack because each contract may behave according to its local interface. Approach and solutionThe protocol replaces the spot price with a validated time-weighted or independent oracle design, checks freshness and deviation, and pauses borrowing when data is invalid. Feed changes require a timelocked multisignature process and emit an event. Signed repayments include a domain-separated digest, per-user nonce and deadline; successful use increments the nonce. Integration and fork tests exercise actual token and oracle interfaces. A fuzz test varies collateral, debt, price and rounding boundaries. An invariant checks that borrowing power does not exceed the defined collateral valuation after each generated sequence. An adversarial test manipulates the test pool and confirms that borrowing is rejected or conservatively limited. OutcomeThe revised design reduces manipulation exposure, makes administrative changes observable, and prevents straightforward signature replay. It does not eliminate all risk: oracle governance, liquidity conditions, key compromise and economic assumptions still require monitoring and review. Learning pointsThe case demonstrates why security is systemic. A mathematically correct loan calculation can still be unsafe when its price input is manipulable. A valid signature can still be dangerous when it is replayable. Testing must therefore include dependencies, privilege, economic incentives and time-dependent behaviour. 10. Limitations, Governance and Professional PracticeNo testing campaign proves that a contract is safe in every future state. Unknown compiler behaviour, dependency changes, novel attack techniques, governance decisions and network conditions remain possible. Smart-contract security is also a governance problem: users should know who can pause, upgrade, mint, change parameters or access emergency funds. Document assumptions in plain language. Publish verified source code where appropriate, maintain a change log, use reproducible builds and establish an incident-response plan. Bug bounties, monitoring and staged rollouts can reduce impact, but they should complement—not replace—secure design. AI-assisted coding and analysis may generate tests, identify patterns or explain traces. Its output must be reviewed by a competent engineer. Generated tests can repeat the same mistaken assumption as the implementation, and generated code may introduce unsafe authorization or arithmetic choices. Treat AI as an assistant, not as an auditor or accountable decision-maker. Chapter SummarySmart-contract security requires layered evidence. Unit tests verify local behaviour; integration tests expose interface mismatches; fuzz tests explore broad inputs; invariant tests examine sequences of state changes; and fork tests validate interactions with real network state. Debugging depends on traces, revert data, storage inspection and minimized regression cases. Static-analysis tools efficiently identify patterns but require human triage and cannot replace specification or economic review. Threat modeling organizes security around assets, actors, trust boundaries and attack paths. Major risks include reentrancy, weak authorization, manipulated or stale oracles, transaction ordering, arithmetic and rounding errors, gas-based denial of service, signature replay and unsafe upgrades. Effective controls include explicit invariants, least privilege, safe external-call patterns, validated data sources, domain-separated signatures, bounded operations, timelocked governance and continuous monitoring. A professional release process combines automated tests, manual review, deployment rehearsal and post-deployment response. Key TermsUnit test: A test that isolates a small contract component or function. Integration test: A test of interactions among contracts or external dependencies. Fuzz testing: Automated testing with generated input values. Invariant: A property that should remain true across permitted state transitions. Fork test: A test executed against a copy of real blockchain state. Test oracle: A rule that determines whether observed behaviour is correct. Trace: A record of nested calls, state actions and failures during execution. Static analysis: Inspection of code without executing every possible path. Threat model: A structured description of assets, actors, assumptions and attack paths. Reentrancy: Callback-driven repeated entry before an earlier call completes. Access control: Rules determining which actors may perform operations. Oracle manipulation: Deliberate distortion or exploitation of external data used by a protocol. Front-running: Transaction ordering that exploits publicly visible pending intent. Invariant testing: Automated checking of properties over generated action sequences. Replay attack: Reuse of an authorization message in an unintended context or time. Nonce: A value used to make a signed or ordered action unique. Upgradeability: The ability to change contract logic after deployment. Timelock: A delay between approval and execution of a privileged action. Checks-effects-interactions: A pattern that validates, updates state and then performs external calls. Least privilege: Granting each actor only the authority required for its function. Review QuestionsShort Answer Questions- What is the principal difference between a unit test and an integration test?
- Why can code coverage fail to demonstrate smart-contract security?
- What information does a transaction trace provide?
- Name two controls against signature replay.
- Why are unbounded loops dangerous in smart contracts?
Long Answer Questions- Explain how unit, fuzz, invariant and fork tests complement one another in a release process.
- Describe a threat-modeling process for a token vault, including assets, actors and trust boundaries.
- Explain reentrancy and compare checks-effects-interactions with a reentrancy guard.
- Discuss oracle manipulation, including attack conditions, testing methods and mitigations.
- Explain the principal security risks of proxy-based upgradeability and propose governance controls.
Analytical / Application Questions- A vault updates a user’s balance only after sending tokens to the user. Design an adversarial test and explain the invariant it may violate.
- A protocol accepts a signed withdrawal containing only a recipient and amount. Identify replay and domain-separation risks, then propose a safer message structure.
- A lending protocol uses a shallow exchange pool as its sole price source. Construct a threat model and a layered test plan for detecting and reducing manipulation risk.
Multiple Choice Questions with AnswersQuestion 1Which test type is designed to check properties across generated sequences of state-changing actions? A. Unit testing B. Invariant testing C. Snapshot testing D. Compilation testing Correct Answer: B. Invariant testing Question 2What is the safest order in the checks-effects-interactions pattern? A. External call, state update, validation B. State update, external call, validation C. Validation, state update, external call D. Validation, external call, state update Correct Answer: C. Validation, state update, external call Question 3Which condition most directly enables a classic withdrawal reentrancy attack? A. A function emits an event B. An external call occurs before accounting is updated C. A contract uses a custom error D. A token has 18 decimals Correct Answer: B. An external call occurs before accounting is updated Question 4Which feature most directly helps prevent replay of a signed action? A. A per-user nonce B. A larger gas limit C. An indexed event D. A payable function Correct Answer: A. A per-user nonce Question 5Why is a shallow spot-market price especially risky for lending collateral valuation? A. It cannot be represented as an integer B. It may be moved temporarily by a relatively small trade C. It always returns a stale value D. It prevents all liquidations Correct Answer: B. It may be moved temporarily by a relatively small trade Question 6What is a principal limitation of static-analysis tools? A. They cannot detect any syntax errors B. They require every contract to be deployed first C. They may produce false positives and miss business-logic defects D. They only work on forked networks Correct Answer: C. They may produce false positives and miss business-logic defects Question 7A Solidity integer calculation performs 7 ÷ 3 using non-negative integers. What quotient is produced? A. 2 B. 2.333... C. 3 D. 0 Correct Answer: A. 2 Question 8Which design best reduces denial of service caused by a reverting recipient in a payment loop? A. Use an unbounded push-payment loop B. Use pull payments or bounded withdrawals C. Ignore all transfer return values D. Require every recipient to be an administrator Correct Answer: B. Use pull payments or bounded withdrawals Question 9What is a principal purpose of a timelock on an upgrade? A. It guarantees that the new implementation is bug-free B. It provides a delay for visibility and response before execution C. It removes the need for access control D. It prevents all storage-layout errors Correct Answer: B. It provides a delay for visibility and response before execution Question 10Which practice is most appropriate after a fuzz test discovers a failing input? A. Discard the input because it was random B. Increase gas until it passes C. Save a minimized version as a regression test D. Disable fuzzing permanently Correct Answer: C. Save a minimized version as a regression test
|