Frequently Asked Questions

Continuous Integration (CI)

It is a best practice to run your coverage job separately from your standard test job in CI. The instrumentation process modifies your contracts and uses high gas limits, which can mask real-world gas issues or introduce unexpected behavior.

Examples:

Coveralls vs. Codecov

We recommend Coveralls for projects that require accurate branch coverage reporting for Solidity. While both are excellent services, Coveralls has been observed to provide better out-of-the-box branch coverage visualization for Solidity require and if statements.

Missed Branch Example

Running out of Memory

For large projects, the Node.js process might exceed its default memory limit during the compilation of instrumented contracts. You can increase the memory allocation for the Hardhat process as follows:

node --max-old-space-size=4096 ./node_modules/.bin/hardhat coverage

If you still encounter memory-related errors from solc (the Solidity compiler), such as RuntimeError: memory access out of bounds, you can reduce the instrumentation footprint by disabling statement coverage in your .solcover.js:

module.exports = {
  measureStatementCoverage: false
};
This will still provide line, branch, and function coverage.

Running out of Time

Tests run significantly slower under coverage. If you have long-running tests, they might hit Mocha's default timeout. You can disable timeouts for the coverage task in .solcover.js:

module.exports = {
  mocha: {
    enableTimeouts: false
  }
};

Running out of Stack ("Stack Too Deep")

Stack-too-deep errors can occur in large, complex projects, especially those using ABI encoder V2 or Solidity >= 0.8.x. This happens because solidity-coverage disables the solc optimizer to trace code execution correctly, but some projects require the optimizer to be enabled to compile.

Here are several workarounds to try in your .solcover.js:

Workaround #1: Enable the Yul optimizer.

module.exports = {
  configureYulOptimizer: true
};

Workaround #2: Configure specific Yul optimizer details.

module.exports = {
  configureYulOptimizer: true,
  solcOptimizerDetails: {
    peephole: false,
    inliner: false,
    jumpdestRemover: false,
    orderLiterals: true,  // Important for stack issues
    deduplicate: false,
    cse: false,
    constantOptimizer: false,
    yul: false
  }
};

Workaround #3: Use an alternative Yul configuration.

module.exports = {
  configureYulOptimizer: true,
  solcOptimizerDetails: {
    yul: true,
    yulDetails: {
      optimizerSteps: ""
    },
  }
};

Notes on Gas Distortion

solidity-coverage injects statements into your code, which increases the gas cost of execution. Be aware of the following:

  • Gas usage reports and simulations will not be accurate.
  • Tests with hardcoded gas costs may fail.
  • Contract logic that depends on precise gas usage (e.g., within gasleft() constraints) may fail.

It is recommended to use estimateGas or default gas settings in your tests to make them more resilient. Relying on specific gas costs is often considered an anti-pattern, as EVM gas costs can change between forks.

Notes on Branch Coverage

solidity-coverage treats require and assert statements as code branches. This is because they conditionally control the execution flow: if the condition is true, execution continues; if false, it reverts.

  • If a require or assert is marked with an I (if) in the coverage report, it means the condition was never true during tests.
  • If it is marked with an E (else), it means the condition was never false.

To achieve 100% branch coverage, your tests must cover both the success and failure cases for each of these checks.