Skip to content

Inline configuration for Solidity tests

Hardhat lets you override Solidity test settings on a per-function basis using NatSpec comments. This is useful when different tests need different parameters. For example, a critical function may need more fuzz iterations, while an invariant test may need a greater depth.

Inline overrides take precedence over the global settings defined in your Solidity tests configuration. They can also be scoped to a Test Profile.

Each override is a single line inside a NatSpec comment (line /// or block /** */), following the format hardhat-config: <key> = <value>.

Keys can be written in camelCase, snake_case, or kebab-case, and string values must be double-quoted:

/// hardhat-config: fuzz.runs = 10000
/// hardhat-config: fuzz.maxTestRejects = 500
/// hardhat-config: evmVersion = "cancun"
function testTransferFuzz(uint256 amount) public {
// ...
}

Block comments are also supported:

/**
* hardhat-config: invariant.runs = 100
* hardhat-config: invariant.depth = 50
* hardhat-config: invariant.failOnRevert = true
*/
function invariantBalanceAlwaysPositive() public {
// ...
}

An unprefixed override applies under every Test Profile. To apply it under a single profile, prefix the key with the profile’s name:

/// hardhat-config: ci.fuzz.runs = 10000
function testTransferFuzz(uint256 amount) public {
// ...
}

Combine both forms on the same test to use different values. When its profile is selected, the prefixed override takes precedence over the unprefixed override for the same key:

/// hardhat-config: fuzz.runs = 100
/// hardhat-config: ci.fuzz.runs = 10000
function testTransferFuzz(uint256 amount) public {
// ...
}

This runs 10000 iterations under the ci profile, and 100 under any other.

The order of the two lines doesn’t matter, and the prefixes are matched exactly as your profiles are named. A prefix that doesn’t name a declared profile is an error, so a typo fails whichever profile you run with.

Keys with the fuzz./invariant. prefix apply only to that test type; the rest apply to the test as a whole and work on both fuzz and invariant tests.

KeyDescription
fuzz.runsNumber of fuzz iterations to run
fuzz.maxTestRejectsMaximum number of rejected inputs before aborting
fuzz.showLogsWhether to show console logs during fuzzing
fuzz.timeoutTimeout for the fuzz test
invariant.runsNumber of invariant test runs
invariant.depthNumber of calls per run to attempt to break the invariant
invariant.failOnRevertWhether to fail the invariant if a revert occurs
invariant.callOverrideWhether to override unsafe external calls
invariant.timeoutTimeout for the invariant test
allowInternalExpectRevertSee below
isolateRun each top-level call as a separate transaction in its own EVM context
evmVersionEVM version to use for this test

By default, the expectRevert cheatcode only catches reverts from external calls, that is, calls at a lower depth than the test itself. If your test directly calls an internal function that reverts, expectRevert won’t catch it and you’ll see an error like call didn't revert at a lower depth than cheatcode call depth.

Setting allowInternalExpectRevert to true allows expectRevert to work on calls at the same depth as the test:

/// hardhat-config: allowInternalExpectRevert = true
function testInternalRevert() public {
vm.expectRevert("some error");
myInternalHelper(); // reverts at the same call depth
}

Hardhat also accepts the forge-config: prefix, so you can keep using it:

/// forge-config: fuzz.runs = 10000
/// forge-config: fuzz.max-test-rejects = 500
function testTransferFuzz(uint256 amount) public {
// ...
}

The default. prefix is the one thing that changes meaning. In Foundry it applies under every profile, because profiles inherit from default. In Hardhat it applies only under the default profile, so drop it to keep the Foundry behavior:

/// forge-config: default.fuzz.runs = 10000
/// forge-config: fuzz.runs = 10000
function testTransferFuzz(uint256 amount) public {
// ...
}

Annotations for other profiles, like ci.fuzz.runs, keep their meaning.