Smart Contract Testing and Invariants

Smart-contract testing should establish which state transitions are allowed, which must fail, and which properties should remain true across operation sequences. Combine example tests, boundary cases, sequence-based properties and adversarial callback scenarios. Passing tests supplies evidence within the tested implementation and environment; it does not certify security or authorize deployment.

Begin with a non-monetary specification and local environment. State the compiler, dependency versions, execution settings and assumptions before interpreting a result. No wallet, network deployment, token transfer or real asset is needed to explain the testing method in this article.

TL;DR

Quick Example

This Python model represents a tiny owner-controlled bounded counter. It is deliberately not Solidity and does not execute EVM instructions. The sequence checks cover rejected calls and bounded state, and the same approach extends to exhaustive combinations of starting values, deltas, and actors.

The model's immutable tuple remains unchanged after rejected transitions. Its final value is two. These are results about the Python transition function only. They do not establish Solidity revert behavior, fixed-width arithmetic, contract access control, transaction ordering, reentrancy resistance, compiler correctness, or gas behavior. The actual contract implementation must be tested independently.

Core Concepts

An invariant needs a defined state space

Write the property in terms of the state and operations it covers. For the toy, the owner remains owner and the value stays between zero and ten. A more complex system may need conservation, ownership, authorization or accounting properties that span multiple contracts and external dependencies.

Avoid a property that passes because the test never reaches meaningful states. Check that authorized operations actually succeed, invalid operations actually fail for the intended reason, and the sequence generator exercises the relevant transitions. A test suite that rejects everything may preserve a bound while implementing no useful system.

Access control needs negative cases

Test the authorized actor and unrelated actors for every privileged transition. Include role changes and any delegated paths when the real design provides them. Assert the state after failure as well as the error condition, and avoid hiding the original behavior behind an overly permissive test fixture.

The Solidity security guide warns against using tx.origin for authorization and explains that external calls can execute other code and call back into the system. Those mechanisms motivate tests of the actual calling context and callback behavior. Solidity security considerations

The table proposes real contract tests. The Python example executes only the simplified first-state model, with no callback or deployed-contract fixture.

Arithmetic semantics belong to the chosen runtime

Solidity's documentation distinguishes checked arithmetic from unchecked wrapping and notes that checked overflow can still leave an application unable to progress through a required transition. Test both the numerical boundary and the intended behavior when it is reached. A revert is not automatically the correct business response.

Python integers and Solidity's fixed-width integer types have different semantics. Do not use a Python model's success to claim that a Solidity expression handles overflow, signed boundaries, conversion or truncation correctly. Pin the actual compiler and test those implementation-specific paths locally.

Reentrancy tests need real interaction

Checks, state updates and external interactions need a deliberate order and system-wide argument. An adversarial local receiver can attempt callbacks into the same or another function and expose assumptions that a single happy-path call misses. Review shared state and dependencies rather than testing only one named withdrawal function.

The Python counter has no external call and therefore cannot exercise reentrancy. A source review or a pure state property is also insufficient to claim a real callback path safe. Preserve that gap in the test plan instead of treating the absence of a failing test as completed coverage.

Best Practices

Translate each important requirement into an observable assertion. For a rejected unauthorized operation, verify the caller context, expected failure and relevant state preservation. For a successful operation, verify the resulting state and any required emitted evidence. Keep the test's expectation independent enough to catch an incorrect implementation rather than repeating the same mistake in two places.

Use a pinned local compiler and test environment for the actual Solidity implementation. Record optimizer and target settings when they affect the tested artifact. Moving documentation can explain general mechanisms without identifying the exact compiler binary that produced a tested contract. No compiler version or contract execution is claimed for this article.

For generated sequences, record seeds and failing traces so a discovered issue becomes reproducible. Define which actors and operations the generator may use and ensure useful transitions remain reachable. Minimize a failing sequence without removing the condition that caused the failure.

Keep monetary and production systems outside exploratory tests. Use local synthetic balances or non-monetary examples, and obtain qualified contract/security review before treating actual asset-handling logic as ready for deployment. Testing, auditing and formal methods contribute different evidence; none should be described as a universal security guarantee.

Reverse a design recommendation when its assumptions fail. If a dependency introduces callbacks, revisit invariants and interaction tests. If a role can change, test the transition and recovery authority rather than only the initial owner. The test plan must evolve with the system's state space.

Common Mistakes

Reporting a Python model as an EVM test

Testing only the happy path

Trusting invariants checked after rejected calls

Calling a contract reentrancy-safe from one pattern

FAQ

Was Solidity executed for this example?

No. Only the explicit Python state model and its bounded combinations were executed. Compiler, EVM, revert, callback and gas behavior remain untested.

Does invariant testing replace example tests?

No. Concrete examples help establish expected transitions and failures, while sequence-based properties explore combinations. Both need meaningful state coverage and assertions tied to the intended specification.

When is a contract ready to deploy?

That requires the actual implementation, environment, authority and consequences to pass the publisher's qualified review and deployment process. This article supplies a local testing framework and a limited model demonstration, not deployment approval or security certification.

Related Topics

References