Hardhat setup: why contract builds and deployments break
A build that passes locally without comment fails at compilation in the test environment — and the cause is usually not the contract code.
Every engineer has their own Hardhat setup: configuration diverges across branches, dependency versions are pulled from the local cache, artifacts from previous runs remain in the working directory and take part in the next smart contract build.
Deployment to the test environment breaks for no apparent reason, and troubleshooting goes into comparing environments instead of checking logic. You have to sort this out manually, and every time from scratch.
The cost of such instability is higher than the hours spent troubleshooting. Contract compilation errors become a familiar background, and a real logic defect slips through along with them.
Build reproducibility is lost: one engineer's result matches the expected one, another's differs by a single character, and there is no way to prove that the tested version is what was deployed to the environment. Manual runs before deployment eat up time and still do not cover rare branches.
Below we break down step by step how this is closed: what we fix in configuration, how we move checks into automated runs, where the boundary lies between local build and the test environment, and why build reproducibility is a condition for release approval, not a wish.
Hardhat setup: what changes in contract work
A smart contract development environment is usually assembled from a dozen disparate steps: compiler, local network, tests, signing wallet, deployment scripts.
Hardhat brings them together into a single configuration file — a new machine gets a working environment with one command, not by following a two-page instruction.
The main effect is a shorter change verification cycle. A contract edit runs on the local network in seconds: state is rolled back after each test, so scenarios do not affect each other.
The engineer sees the result before switching to another task and completes noticeably more iterations per day.
A single build command removes discrepancies between machines. Compilation, tests, artifact size checks, and deployment preparation run with one line — the same for everyone, regardless of local settings.
This eliminates the classic “it works on my machine”: the result no longer depends on whose computer ran the build.
Fewer deployment failures are a consequence of fixed network parameters and explicit checks before publication.
Addresses, signing keys, and confirmations are moved into configuration and checked automatically: if the network does not match or a required confirmation is missing, deployment stops before the transaction is sent, not after.
Local network, fork, and shared test environment: comparison of modes
The environment mode is not a matter of taste, but a question of which classes of defects can surface at all before release.
The local network catches contract logic errors, a mainnet fork catches incompatibility with already deployed dependencies, and the shared test environment catches behavior under real load and with developers working in parallel.
These modes are configured alongside each other, so they are kept simultaneously and separated by stage: quick runs locally, integration runs on a fork, acceptance in the shared environment.
| Mode | When it fits | Limitations |
|---|---|---|
| Local network | Checking the logic of an individual contract, debugging, edit cycle in seconds | Artificial state: no real confirmation delays or third-party code — integration errors are not reproduced |
| Mainnet fork | Calls to already deployed contracts, incident investigation, debugging third-party interfaces | The state snapshot is tied to a specific block and quickly becomes outdated; external data sources have to be substituted |
| Shared test environment | Customer acceptance, load runs, parallel work of several developers | Slower than local runs; state is shared among participants — isolated accounts and a reset procedure are needed |
The limitations of a local node are the price of speed: checks run instantly but do not show how the contract will behave alongside third-party code.
Therefore, in the setup plan we include transition between modes as a separate step — with fork warming at the required block and a reset procedure for the shared environment, otherwise tests begin to interfere with each other and stop proving anything.
Work stages: from repository review to handover to the team
We deploy the environment so that contracts build identically for the developer, in the test network, and in production.
The order of work is fixed: first we review what is already in the repository, then design the environment scheme, and only after that write configuration, tasks, and scripts.
-
Contract repository audit. We look at the structure, dependency versions, existing deployment scripts, and tests. We note what we transfer unchanged and what we rewrite — this way breakages that surface during migration are visible in advance, not at release.
-
Environment scheme. We determine how many networks are needed (local, test, production) and which keys and variables belong to each. We move secrets into separate files outside version control and distribute access by role.
-
Build configuration. We configure the compiler, optimization level, and contract language versions. We get a deterministic build: the same code produces the same artifact on any machine.
-
Tasks and scripts. We describe Hardhat tasks: deployment, verification, migrations, utility calls. Each task is parameterized by network, so manual commands and “custom” edits go away.
-
Environment acceptance checks. We run the build and tests on a clean machine, check repeated deployment and rollback to the previous version. The configuration must be reproducible from scratch without manual adjustments.
-
Documentation and handover. We document the environment scheme, list of variables, and command order. We conduct handover to the team: engineers reproduce the process themselves and then run it without us.
What remains in the repository: configurations, tasks, documentation
Environment setup ends not with successful compilation, but with the state of the repository. We proceed from the assumption that in six months the build will be set up by your engineer, not the one who assembled it — so all agreements live in files next to the code, not in chat.
- Configuration files — a single root Hardhat config with externalized compiler, artifact path, and network parameters; no secrets inside, only references to environment variables.
- Network profiles — separate sets of settings for the local environment, test stand, and production network, switched with one variable when running a task.
- Check run procedure — test, linter, and contract size control tasks assembled into one command, and an explicit description of what blocks merging changes.
- Deployment and verification scripts — repeatable tasks that bring up contracts in any environment and publish source code for external audit.
- Key access policies — where signing keys are stored, who issues and revokes them, how they are injected into scripts, and what to do in case of compromise.
- Environment documentation — an instruction in the repository root: runtime version, installation order, list of variables, and typical deployment errors.
Handover is considered complete when the repository is cloned to a clean machine and the environment is brought up according to the documentation without our hints. After that you edit the configuration and add tasks yourself — we join in for complex changes.
Case: reduced contract verification time before release
Starting point
The client is a team that released contract updates once every one to two weeks. Pre-release verification relied on manual runs: an engineer brought up a local node, remembered which environment variables to set, and ran scenarios through a list of steps.
A full pass took 3-4 hours, and discrepancies between the local environment and the test network caused false failures in roughly every third run — part of the time went into investigating what was not an error.
What was changed and what was achieved
We reduced the environments to a single description and replaced the manual list with one run command: it brings up the node, deploys contracts, runs scenarios, and collects a report.
The Hardhat setup here is only part of the work: more important were pinned compiler versions, predictable accounts, and identical parameters for local startup and the test network.
Checks that previously were left to the engineer's discretion — method call permissions, gas limits, failure behavior — became part of the automated run.
Pre-release verification time dropped from 3-4 hours to 10-15 minutes, and false failures due to environment discrepancies disappeared as a class. The run report is attached to the release, so it is clear which scenarios passed and what exactly failed.
A separate class of failures — “it works for us, but not on the network” — was closed: previously it surfaced after release, now environment differences are identified before it.
What to do if some contracts are already deployed to mainnet?
A deployed contract cannot be changed — only how it is worked with changes. Therefore the environment is built around fixed addresses: the existing address and its interface are added to the configuration, rather than waiting for a new deployment.
Current processes do not stop — the new configuration stands alongside the old one and is enabled as it becomes ready.
The first step is inventory. We compile a list of networks, addresses, and code versions and compare them with the actual state: whether owner rights, roles, and links between contracts match.
If the code in the repository diverges from what is on the network, the build is based on the network, not the repository — otherwise the setup relies on an incorrect picture.
Next, Hardhat is configured so that keys and connection parameters do not end up in the repository: they come from environment variables, and the configuration is divided into profiles — production network, test network, local copy.
Deployment scripts are made idempotent: before sending a transaction, it checks whether a contract already exists at the address, so a repeated run does not create a duplicate.
A mandatory stage is verification on a network copy. The local copy reproduces the real state: balances, accumulated data, granted roles. On it, upgrade and rollback scenarios are run without affecting production processes. Errors in access rights and call sequence become visible before they reach the live network.
Environment migration proceeds in parallel: old scripts continue to work, new ones are tested on the copy, and switching happens one step at a time. This way a live project gets an up-to-date configuration without a pause in development.
Subtle points: compilation, plugins, and build reproducibility
Questions before starting are almost always the same — from choosing a compiler to where private keys are stored.
These are not trifles: the answers determine whether the project will build for a new engineer on the first run and whether the release will survive an auditor's review.
Below are short answers to what most often comes up when setting up Hardhat before even the first line of code.
How do you choose compilation settings?
We formulate the profile for the task: we select the contract language version and optimizer level so as to fit within the deployment limit, not “by eye.” For tests we keep a separate profile — with debug information and without optimizations.
How many plugins should be left connected?
Only those that solve a real problem: deployment, contract verification in a block explorer, tests, coverage. Every extra one is another version in dependencies and another source of incompatibility during updates.
What does build reproducibility mean?
The same commit produces the same bytecode for any engineer and on the build server. Version pinning with a lockfile, moving settings off the developer's machine, and running the build in a container help.
Where should secrets be stored?
Never in the repository. We build secret storage as follows: deployer keys and node access live in environment variables locally and in a secure vault on the build server; in code — only variable names and a sample file without values.
Let's discuss the task: we will assemble an environment for your project
To discuss the task concretely means coming with the minimum inputs by which scope is calculated, not “roughly.” Without this data, the Hardhat environment setup plan turns into assumptions that get redone on live networks, where the cost of an error is higher.
- List of contracts and their purpose: what is already written, what is planned, how modules are connected to each other.
- Target networks: test environments and production ones, including third-party compatible networks if deployment goes to more than one point.
- Current repository state: version control system, branching scheme, what builds the project now and what is unsatisfactory about it.
- Check requirements: which scenarios are considered critical, whether coverage reports and vulnerability checks are needed.
- Accesses and roles: who owns deployment keys, how secrets are stored, whether there are restrictions on external connections.
- Release priorities: what must go out first, where a manual step is acceptable, and where a fully automated rollout is needed.
In response you get an estimate of the scope of work broken down by stage and an environment setup plan: project structure, list of tasks, set of checks, and release order.
Then come configurations, a description of environment variables, and startup instructions for a new machine. Handover to the team is accompanied by a walkthrough — your engineers will be able to run the build and deployment without us, rather than coming back for every change.







