Why Foundry setup falls apart on a large project
Foundry setup seems like an evening's work: forge builds, cast calls contracts, anvil starts a local network, fuzzing finds edge inputs on its own. While there are a dozen contracts and one developer, that is true.
On a large project, the local environment is the first thing to fall apart — and it slows down not one engineer but the entire team.
Unstable contract tests are the most expensive habit in such a project. Fuzzing generates a new set of inputs on every run, and a test that was green yesterday fails today on a value no one saved.
Once a test depends on block.timestamp or block.number, there is no way to reproduce the failure: the report has an error, but no way to repeat it. The team starts trusting not the run but memory of the last successful run.
Build errors in the local environment hit new engineer onboarding. One has a pinned compiler, another has a floating version, dependencies pulled from different sources — and forge build fails for the new person due to mismatched remappings.
Dependency version desync means a fix that is green on the author's machine is not reproduced by colleagues or in the verification pipeline.
The long cycle of verifying changes is made up of small things: build, test run, bringing up fork state, manual calls via cast from memory, deployment to a test network. An iteration takes tens of minutes, and half is spent repeating what should be done with one command.
We break down how this is closed — from environment pinning to runs you can trust.
What a well-structured build and contract verification cycle gives
First measurable effect — time to first result.
When a Foundry environment comes up with one command and dependencies are pinned, an engineer gets a working stand not after a week of approvals, but on day one: a local network for debugging, a deployed contract, and ready verification scenarios in place.
Then automatic test runs on every change work — and the share of failures caught before deployment grows.
Early detection of errors is cheaper than any subsequent investigation: checks of access rights, limits, and state transitions are performed before the code goes to the network.
Contract fuzzing adds what no engineer would write by hand — thousands of random and edge calls, and it is they that expose overflows and bypasses of checks.
Reproducible build closes a separate class of problems. The same result on different machines and in server build means the artifact is verified by fingerprint: if the code diverges, it is visible before publication, not after. Rollback and re-verification cease to be an investigation of causes.
In total, the cycle gives three measurable values: time to first result, share of paths covered by checks, and share of errors caught before deployment. The earlier a failure is found, the cheaper it is — and the fewer surprises in production network.
Network launch modes: local node, fork, test stand
In Foundry, the environment is assembled from three modes, and they are not interchangeable: a local node gives speed, a mainnet fork gives accuracy, a test stand gives acceptance reproducibility.
Choosing the wrong mode is expensive: verification on an empty node misses dependency on a real oracle, while running on a fork without a pinned block breaks repeatability of results between runs.
| Mode | When it fits | Limitations |
|---|---|---|
| Local node (anvil) | Unit checks of logic, debugging individual calls, runs with state snapshots | No real tokens, oracles, or liquidity — external dependencies are replaced with stubs |
| Mainnet fork | Integration checks against already running contracts on current network state | Each run requests fresh data; without pinning the block number, the result is not reproducible |
| Test stand | Pre-release acceptance, checking access rights, migrations, and upgrades | Deployment and seeding take time; state must be kept up to date |
In practice, modes are combined: a fast edit cycle runs in an isolated environment, where state rollback returns the chain one step back, and scenarios with external contracts are moved to a fork with a pinned block.
The test stand remains the final point — it checks what cannot be reproduced in a fork: upgrading a contract through a proxy, administrator roles, and data migration order.
Foundry setup: work stages from repository audit to handover
Foundry setup rarely comes down to installing tools — the task is for the engineer and the build agent to get the same environment from one description. Therefore, we do the work sequentially, and verify the result of each step before moving to the next.
- Repository audit. We analyze contract structure, external dependencies, presence of tests and deployment scripts, and find bottlenecks in the current build.
- Environment scheme. We pin the tool composition, installation method, directory structure, and variables that differ on a local machine and on the build agent.
- Deployment. We bring up the environment strictly according to the description and verify that it builds from scratch on a clean machine without manual modifications.
- Verification pipeline. We establish the order of steps: build, static analyzers, unit tests, property checks, coverage report — with a single result for everyone.
- Environment acceptance. We run control scenarios, measure full cycle time and rollback time, and verify reproducibility on a second machine.
- Handover procedure. We hand over documentation, instructions for common failures, and an update procedure, and go through everything with the customer's team.
At the output — an environment that is deployed with one command, updated without stopping work, and handed over to a new engineer without verbal explanations.
Access policies, signing keys, and build artifacts
Foundry setup is not only a working build and verification pipeline, but also a set of artifacts that remain with the customer's team.
Without them, in six months no one remembers which key signs what, why the build is reproducible for one engineer and fails for another, and who even has the right to publish a release.
Below is the list of what we hand over and record in documentation. Each item is checked during acceptance together with your engineer, and does not remain «in words».
- Access policies: roles in the repository and artifact registry — who merges into the main branch, who publishes builds, who grants and revokes rights.
- Role separation: the developer does not touch the signing key; signing is performed by a separate circuit in an isolated environment without access to the public network.
- Signing key procedure: where they are stored, who owns them, rotation order, what to do in case of compromise, and how to revoke a release.
- Pipeline scheme from commit to publication: where checks run, where build artifacts are assembled, at what point the signature is applied.
- Action logging: who, when, and what signed or changed in access; the log is stored separately from the build environment and cannot be edited.
- Reproducibility of build artifacts: pinned dependencies and a single authorized build method so the result matches for everyone.
- Documentation: local launch, access recovery, release rollback, and a pre-publication checklist for the on-call engineer.
Case: how contract release downtime was reduced threefold
The project — a protocol with frequent logic updates: before the process was restructured, releasing a new version took three to four days, checks were done manually on a test network, and rare scenarios were caught already in mainnet.
Over several releases, incidents of one class accumulated: incorrect rounding in calculations and bypassing limits with a certain sequence of calls. Each such case was expensive — the contract state had already changed, and the fix had to be made under load.
We moved the verification cycle to a local stand on Foundry: contract deployment, scenario runs, and network state snapshot are performed in one run, and state is rolled back to the required block.
An incident from mainnet is now reproduced locally in minutes — with the same sequence of calls and the same state in which it occurred.
The key change is regression of behaviors, not checking individual functions. A set of invariants (balance, access rights, limits, operation order) is run on every change, and fuzzing substitutes boundary and random input values.
This removed an entire class of failures that used to be caught only in mainnet: overflows, incorrect rounding, bypassing checks during nested calls.
Result: releasing a version — one working day instead of three to four, recovery time after a failure decreased from several hours to tens of minutes, regression scenarios cover almost all contract execution paths. New releases go out without manual checks in mainnet.
How does a local network fork differ from a test stand?
A local fork is a snapshot of network state at a specific block, brought up on the developer's machine. Transactions in it live in process memory: restart it — state returns to the original block, history resets.
Data is isolated, and this is the main advantage: experiments do not interfere with colleagues or load the main network.
While one engineer debugs a contract, a fork is enough — Foundry brings it up in seconds, accounts and balances are substituted from the snapshot.
The problem appears when a second person is involved in the scenario: isolated environments do not share a common transaction history, and someone else's run cannot be repeated one-to-one.
| Option | When it fits | Limitations |
|---|---|---|
| Fork on a network snapshot | Debugging calls to external protocols, checking integrations | The snapshot becomes outdated, others' transactions are not reproduced |
| Fork on an empty blockchain | Running token logic, quick checks of a separate function | No external dependencies or real state |
| Isolated stand | Acceptance scenarios, load, combination of several services | Requires deployment and state versioning |
| Shared stand with fixed history | Parallel team work, reproducing failures | Requires access regulations and migration transfer |
Data isolation is an advantage exactly until the scenario needs to be handed over. A fork is not reproducible: everyone has their own block, their own order, and their own unexported transactions.
A stand pins state together with history and stores them between runs — an acceptance run gives the same result for everyone, and a failure is reproduced and rolled back by the same block.
Technical doubts before starting work
Before starting, four questions usually come up: what to do with already written tests, how not to drag private keys into the repository and logs, why the build produces different bytecode for different engineers, and who holds the environment after handover. Below — how we close each one.
Tests are already written on another framework — rewrite everything?
No. We preserve scenario logic: assertions and mocks are adapted to the Foundry runner, integration ones are deployed on a local node with a mainnet fork. Homemade helpers go into shared utilities, otherwise every new test drags a copy along. We add fuzzing as a layer on top — the previous checks remain a support point.
How are deployer and owner keys isolated?
Keys live only in environment secrets; signing is through a hardware wallet; they are not in code, logs, or artifacts. Locally we use deterministic test accounts; in networks, roles are separated: the deployer does not match the contract owner. Address books record which key deployed what.
Will the build on my machine and in CI match?
The compiler, dependencies, and profiles are pinned in the repository, so the local and CI run produce the same bytecode. We verify by artifact hashes — divergence is visible before deployment, not after. We check reproducibility in a clean environment, not on the author's machine.
What happens after project handover?
We hand over the environment as code: deployment scripts, profiles, CI tasks, documentation on launch and rollback. The customer's engineer reproduces the run according to the instructions without our help.
After that — an agreed support mode: dependency updates, analyzing fuzzing failures, support during upgrades.
Let's discuss the task and come back with an estimate and plan
To come back with a plan rather than general words, we need a slice of your environment: how contracts are built, where deployment keys are stored, which networks are used, and what is already covered by checks.
A detailed document is not needed — a short description of the task is enough; we will ask clarifying questions ourselves.
We will break the plan into stages so that timelines are based on the scope of work, not on a feeling. Usually it looks like this:
- Environment analysis: repositories, compiler versions, dependencies, access to nodes and registries.
- Contract build: reproducible process, artifact cache, code size within network limits.
- Local stand on Foundry: node for running scenarios, state snapshots, launch in minutes.
- Test circuit: unit checks and property checks with input generation, report on uncovered branches.
- Deployment and migration scripts: idempotent scenarios, checking role rights, rollback plan to the previous version.
- Procedure for updating the compiler and dependencies so the environment does not diverge between machines.
- Handover: launch documentation, walkthrough with the team, order of further improvements.
Describe the task and current environment — we will come back with a stage plan, scope of work, and timelines for each item. If part of the circuit is already set up and handling load, we will say directly what can be left alone.







