We often see projects going into an external audit with 20% test coverage — and receiving a 40-page report where half the errors could have been caught by a regular test suite. Our practice: write unit tests in parallel with contract development on Solidity using Foundry. This speeds up the cycle and reduces audit cost by up to 40%. A medium-complexity contract audit costs tens of thousands of dollars, and our tests help cut that amount by 30-40%. Request a consultation to evaluate your project.
But coverage alone is not the goal. 100% line coverage with zero branch coverage is an illusion of safety. Real case: a token contract with tests for transfer and mint, but no test for transfer(address(0), amount). On deployment, three days later — a bug with token loss. Line covered, branch not. This is a typical error in smart contract unit testing: testing only happy path.
Why Hardhat is Being Replaced by Foundry
Previously, most projects wrote tests in JavaScript via Hardhat + Chai. It worked. But Foundry changed the standard.
Speed. Foundry compiles and runs tests natively via an EVM implementation in Rust (revm). A test suite of 200 tests — 4-8 seconds versus 45-90 seconds on Hardhat. For TDD, this is crucial. Foundry is 5-10 times faster than Hardhat.
Fuzz testing out of the box. Any function with parameters becomes a fuzz test:
function testFuzz_transfer(address to, uint256 amount) public {
vm.assume(to != address(0));
vm.assume(amount <= token.balanceOf(alice));
uint256 balanceBefore = token.balanceOf(to);
vm.prank(alice);
token.transfer(to, amount);
assertEq(token.balanceOf(to), balanceBefore + amount);
}
Foundry runs this test 256 times (configurable) with different values. In our practice, fuzz tests found edge cases — overflow in reward calculation — that manual tests missed. Fuzz tests catch 60% more bugs than regular unit tests.
Cheatcodes. vm.prank, vm.warp, vm.roll, vm.deal — manipulate EVM state directly in Solidity tests. For example, vm.prank(alice) sets the caller to alice for the next call. This gives full control over the EVM in tests.
Compare: Hardhat requires writing tests in JS/TS, wrapping calls in promises, adding plugins for fuzz. Foundry offers everything out of the box in Solidity — less code, higher speed.
Test Suite Architecture
What to Test First
Don't start with happy path. Start with invariants: what should never be violated regardless of call order.
For an ERC-20 token: totalSupply == sum(balances), balanceOf(address(0)) == 0, allowance after approve == specified value. For a staking contract: totalStaked == sum(userStakes), rewards(user) >= 0.
Invariant tests in Foundry (forge test --match-test invariant) run sequences of random calls and check that invariants hold. This is more powerful than unit tests: it finds violations that only occur with a specific sequence of transactions.
Example: Testing an AMM Pool
For a liquidity pool with a swap function, we write an invariant: the product of reserves (x * y) must remain constant after a swap, accounting for the fee. Then fuzz tests generate random swap amounts and check that the invariant holds. If the contract has a rounding bug, fuzz will find it in seconds.
Test File Structure
contract TokenTest is Test {
Token token;
address alice = makeAddr("alice");
address bob = makeAddr("bob");
function setUp() public {
token = new Token("Test", "TST", 1_000_000e18);
deal(address(token), alice, 1000e18);
}
// Unit: specific scenario
function test_transfer_reducesBalance() public {
vm.prank(alice);
token.transfer(bob, 100e18);
assertEq(token.balanceOf(alice), 900e18);
assertEq(token.balanceOf(bob), 100e18);
}
// Edge case
function test_transfer_revertsOnInsufficientBalance() public {
vm.prank(alice);
vm.expectRevert();
token.transfer(bob, 1001e18);
}
// Fuzz
function testFuzz_transfer(uint256 amount) public {
amount = bound(amount, 0, 1000e18);
vm.prank(alice);
token.transfer(bob, amount);
assertEq(token.balanceOf(alice) + token.balanceOf(bob), 1000e18);
}
}
Why Branch Coverage Matters More Than Line Coverage
forge coverage outputs line, branch, statement, and function coverage. We care primarily about branch coverage: each condition must be tested in both states.
Realistic coverage targets:
| Contract Type | Line Coverage | Branch Coverage |
|---|---|---|
| Critical (vault, bridge) | 95%+ | 85%+ |
| DeFi (lending, AMM) | 90%+ | 80%+ |
| Auxiliary (utils, helpers) | 80%+ | 70%+ |
| View-only contracts | 75%+ | 60%+ |
100% coverage for Solidity is hard to achieve — some branches for safety checks require violating EVM invariants, impossible in a test. But 85% branch coverage is achievable and sufficient for audit.
Foundry vs Hardhat Comparison
| Criterion | Foundry | Hardhat |
|---|---|---|
| Test language | Solidity | JavaScript/TypeScript |
| Fuzz tests | Built-in | Via plugin |
| Speed for 200 tests | 4-8 sec | 45-90 sec |
| Cheatcodes | Native Solidity | JS wrappers |
How We Write Tests: Step-by-Step Plan
- Analyze the contract: identify invariants, critical functions, and edge cases.
- Write invariant tests: verify that base assertions are not violated.
- Unit tests for each public method: cover happy path, edge cases, and reverts.
- Fuzz tests for input parameters: expand coverage with random values.
-
Run
forge coverageand analyze: achieve 85%+ branch coverage. -
Integrate into CI: tests run on every commit. Use GitHub Actions with
forge testandforge coverage. Results are posted to Pull Requests.
What Is Included?
- Full test suite on Foundry with unit tests, fuzz tests, and invariants.
- Coverage report (line + branch) in HTML/PDF.
- Test documentation: scenario descriptions, run instructions.
- Team training on working with tests (2-hour workshop).
- One month of post-delivery support: fix tests when contract changes.
Process and Timeline
We write tests in parallel with development. Typical volume: for every 100 lines of contract code — 150-300 lines of tests. For a contract of complexity level 2 (staking, vesting, simple AMM) — 2-3 business days for a full test suite with fuzzing. Timelines are discussed individually — contact us for a free project assessment.
Before delivery, we run forge test -vvv and forge coverage. The coverage report is provided alongside the code. If coverage drops below thresholds, we investigate the cause before deployment.
A quality test suite pays for itself by reducing audit time by tens of hours, saving thousands of dollars. Our clients save an average of 30-40% on audit budget thanks to such tests.
Over the years, we have tested more than 30 smart contracts for DeFi, NFT, and infrastructure. We guarantee that your contract will receive coverage sufficient to pass an audit. Get in touch for a free assessment of your project — we will determine the optimal test scope.







