Smart contracts development: a practical deployment checklist
The most dangerous point in smart contracts development is not compilation. It is the transition from code that behaves correctly in isolated tests to code that must preserve its invariants while…

The most dangerous point in smart contracts development is not compilation. It is the transition from code that behaves correctly in isolated tests to code that must preserve its invariants while interacting with adversarial callers, mutable market conditions, upgrade administrators, token implementations, and a live blockchain state.
That distinction defines the deployment process. A contract can pass unit tests, compile cleanly with Solidity 0.8.x, and still expose an attack vector through authorization logic, accounting assumptions, oracle dependencies, or an unexpected external call. In 2023, decentralized protocols lost a combined $1.8 billion to hacks and exploits, according to OpenZeppelin. The figure is not an argument for treating audits as a ceremonial release gate. It is evidence that protocol security must be designed as a sequence of independent controls.
A reliable workflow combines arithmetic safety, static analysis, adversarial testing, privilege minimization, external review, and post-deployment monitoring. Each layer catches a different class of failure. None provides a guarantee in isolation.
Start with the state model, not the Solidity syntax
The first security review should concern the protocol’s state transitions. Solidity code is only the implementation of a state machine, and most severe DeFi exploits occur when the implementation permits a transition that the economic model did not intend.
Before writing functions, define:
- which values represent user balances, protocol reserves, debt, collateral, shares, and fees;
- which operations can increase or decrease each value;
- who may trigger those operations;
- what must remain true after every external call;
- which assumptions depend on an oracle, token standard, block timestamp, or governance action;
- which state changes are irreversible once the transaction is finalized.
For a lending protocol, a useful invariant may be that the total debt recorded by the system cannot exceed the value of issued assets under the protocol’s accounting rules. For a liquidity pool, the invariant may concern the relationship between reserves and liquidity shares. For a yield strategy, the state model must distinguish principal, accrued yield, harvested rewards, and assets temporarily held by an external protocol.
These relationships should exist before the test suite. Otherwise, testing tends to verify examples rather than properties.
Arithmetic safety in Solidity 0.8.x
Solidity 0.8.0 introduced built-in overflow and underflow checking. In standard arithmetic operations, an overflow or underflow now reverts automatically instead of silently wrapping around. This removed a large class of historical arithmetic failures and reduced the need for legacy SafeMath libraries in contracts targeting Solidity 0.8.0 and later.
It did not remove arithmetic risk.
The unchecked block can deliberately disable those checks. That may be appropriate for a bounded loop or a calculation where the range has been proven, but it creates a local trust boundary. Every unchecked operation should have an explicit reason, a bounded input domain, and a test that demonstrates the bound.
The remaining problems are often semantic rather than numerical:
- dividing before multiplying can truncate value unexpectedly;
- a percentage expressed in basis points may use the wrong denominator;
- token decimals can be confused with protocol precision;
- signed and unsigned values can produce unexpected conversions;
- a share-price calculation can become manipulable when total supply is near zero;
- rounding direction can systematically favor a caller or the protocol;
- a fee may be applied twice across nested accounting functions.
A contract may therefore be mathematically safe at the machine level while still implementing the wrong financial formula.
Solidity’s checked arithmetic prevents silent wrapping; it does not prove that the protocol’s accounting model is correct.
The state model should also identify the contract’s externally reachable surface. Explicit visibility modifiers are not stylistic decoration. external, public, internal, and private communicate which functions form part of the callable interface and reduce the chance that an internal helper becomes an unintended entry point.
For every state-changing function, document the expected caller, preconditions, state mutations, external interactions, and postconditions. This is particularly valuable when several functions share accounting logic. A reviewer can then compare the intended transition with the actual order of operations rather than reading the contract as a sequence of isolated methods.
Static analysis is a filter, not a security verdict
Automated static analysis is most useful before the code reaches an external auditor. It can identify patterns that are easy to miss during manual review and can make security regressions visible in continuous integration.
Slither, for example, includes more than 40 built-in detectors for common smart contract vulnerabilities and code-quality problems. Detectors can flag issues involving reentrancy patterns, dangerous low-level calls, unused return values, shadowed variables, incorrect inheritance assumptions, and access-control weaknesses.
The practical workflow is straightforward:
1. Run static analysis on every pull request that changes contract code.
2. Treat new high-confidence findings as build failures.
3. Classify accepted findings rather than suppressing them without explanation.
4. Re-run analysis with production compiler settings and optimization flags.
5. Review the generated report alongside the diff, because the same warning can represent different levels of risk in different architectures.
Static analyzers work from code patterns. They do not understand the full economic intent of a protocol. A detector may not recognize that a price update can be manipulated through a flash loan, that a callback can alter the assumptions of a swap, or that a governance vote can approve an unsafe parameter combination.
The output should therefore be converted into engineering work:
| Finding category | Typical implication | Required follow-up |
|---|---|---|
| Access control | A privileged or state-changing function may be callable by an unintended account | Trace every authorization path and test unauthorized callers |
| External interaction | A call may transfer control to an untrusted contract | Review reentrancy, return values, callbacks, and state-update order |
| Arithmetic or precision | The operation may revert, truncate, or produce an economically incorrect result | Test boundary values, rounding direction, decimals, and zero states |
| Gas usage | A function may become uncallable as storage or input size grows | Model worst-case state growth and block gas constraints |
| Compiler or inheritance behavior | The deployed bytecode may differ from the developer’s assumption | Pin compiler versions and inspect inheritance linearization |
| Unused or unchecked return data | A failed token operation may be treated as successful | Use safe wrappers and test non-standard token behavior |
A clean static-analysis report is useful evidence that known code patterns have been reviewed. It is not evidence that business logic is sound, oracle assumptions are safe, or governance can never be abused.
The same principle applies to an external cybersecurity threshold analysis from another engineering domain: identifying a critical security boundary is valuable only when it changes the release decision and the controls around the system. Smart contract teams should apply that discipline to their own deployment gates rather than treating security language as documentation.
Testing must include hostile state and hostile callers
Unit tests are necessary because they provide fast feedback on local behavior. They are insufficient because a DeFi protocol does not operate in a collection of clean, independent examples.
A serious test strategy has at least four layers.
Unit tests for local correctness
Unit tests should cover successful paths, failure paths, and boundary conditions. For a tokenized vault, that includes deposits of the smallest supported amount, withdrawals that empty a position, fee calculations at zero and maximum values, and behavior when total assets or total shares are near zero.
Every authorization branch deserves a negative test. Do not test only that an administrator can pause a contract. Test that a user, an old administrator, a zero address, and an account with a similar but insufficient role cannot do it.
The same applies to token interactions. ERC-20 implementations are not perfectly uniform. Some tokens return booleans, some historically did not, some charge transfer fees, and some impose transfer restrictions. If the protocol assumes standard behavior, that assumption must be explicit and enforced.
Fuzz testing for input space
Fuzzing replaces a small set of manually chosen values with a broad range of generated inputs. It is particularly effective for arithmetic boundaries, share conversions, liquidation thresholds, fee schedules, and combinations of user actions.
A useful fuzz test does not merely assert that a function does not revert. It asserts a property:
- withdrawing after depositing should not create assets from nothing;
- a user’s balance should not become negative through any sequence of valid calls;
- a liquidation should not leave the liquidator with more value than the permitted incentive;
- a paused contract should reject all designated operations;
- a fee should remain within its configured maximum;
- total supply and aggregate balances should remain consistent after mint and burn operations.
Fuzzing becomes more valuable when the generated calls are sequences rather than single transactions. Many accounting failures require a deposit, a partial withdrawal, a donation, a rate update, and another withdrawal before the discrepancy appears.
Invariant testing for protocol properties
Invariant testing examines what must remain true across arbitrary sequences of transactions. It is the closest of the common testing techniques to expressing a protocol’s state model directly.
The challenge is selecting invariants that reflect economic reality. A superficial invariant can pass while the protocol remains exploitable. For example, checking that a variable remains non-negative says little if the attacker can still inflate its value.
For each invariant, specify:
- the state variables involved;
- the permitted rounding error;
- the accounts allowed to change the state;
- the external conditions under which the invariant applies;
- whether the property must hold after every call or only after a complete operation.
This level of precision also exposes ambiguous specifications. If the team cannot agree whether a fee belongs to users, liquidity providers, or the treasury, the contract is not ready for deployment.
Mainnet fork testing
Mainnet fork testing places the protocol logic against realistic chain state. It can reveal assumptions that never appear in local mocks:
- live token balances and decimals;
- existing pool liquidity;
- deployed oracle contracts;
- non-standard token behavior;
- current governance configuration;
- realistic reserve ratios and market depth;
- interaction with already deployed protocol versions.
Fork tests are especially valuable for upgrade rehearsals, migrations, liquidation paths, and integrations with lending markets or automated market makers. They can also model whether a transaction sequence is feasible within available liquidity and gas limits.
A practical comparison between common Solidity development workflows looks like this:
| Dimension | Hardhat-oriented workflow | Foundry-oriented workflow |
|---|---|---|
| Primary strength | Broad JavaScript or TypeScript integration and an established plugin ecosystem | Fast Solidity-native testing, fuzzing, invariant testing, and scripting |
| Test style | Convenient for application-level fixtures and front-end integration | Efficient for property-based tests and repeated execution |
| Fork testing | Supported through network configuration and plugins | Strong native workflow through forked execution and scripts |
| Team fit | Useful when the surrounding stack is TypeScript-heavy | Useful when protocol engineers want most tests and tooling in Solidity |
| Security consequence | No inherent security advantage from the framework alone | No inherent security advantage from the framework alone |
| Selection criterion | Choose based on integration needs and reviewability | Choose based on testing depth, execution speed, and team expertise |
The hardhat vs foundry framework decision should not become a proxy for security maturity. A poorly specified invariant remains poor in either framework. The stronger choice is the one that lets the team express protocol properties, reproduce failures, and maintain the test suite after deployment.
Reentrancy is a control-flow problem
Reentrancy is commonly presented as a failure to use a guard. The deeper issue is that an external call transfers control before the contract has completed a logically atomic operation.
The checks-effects-interactions pattern remains a useful baseline:
1. Validate the caller and input.
2. Update internal state.
3. Interact with the external contract.
A reentrancy guard adds another layer by preventing a function from being entered again while it is executing. It does not automatically protect a related function that can be called during the same operation, nor does it resolve cross-contract or cross-function reentrancy where the attacker re-enters through a different route.
Reviewers should map every external interaction, including:
- ERC-20 transfers and callbacks;
- ERC-721 or ERC-1155 receiver hooks;
- arbitrary calls made by routers or vaults;
- oracle adapters;
- flash-loan callbacks;
- tokenized position managers;
- upgrade and initialization functions.
The key question is not simply whether the function has a guard. It is whether the protocol’s state remains coherent if control returns from the external contract with unexpected behavior.
Low-level calls require equal caution. The return value should be handled according to the target interface, and failures must not be silently treated as successful state transitions. When a protocol supports arbitrary integrations, the attack surface expands from the local contract to the assumptions made about every target.
Access control should be treated as an attack surface
Many smart contract failures are authorization failures rather than cryptographic failures. The contract correctly executes a dangerous operation for the wrong account.
Begin by separating roles according to capability. A protocol may need distinct permissions for pausing, changing fees, updating an oracle, upgrading implementation code, managing supported assets, and withdrawing emergency funds. Concentrating these powers in one administrator increases the blast radius of a compromised key and makes operational mistakes harder to contain.
Administrative privileges should generally be placed behind a multisig wallet or a timelock where the protocol’s risk model permits it. The point is not that multisig approval makes an action correct. It creates separation of duties, a visible execution process, and an opportunity to react to a malicious or erroneous proposal.
The deployment review should answer concrete questions:
- Is the initializer callable only once?
- Can the zero address become an administrator or trusted dependency?
- Are role grants and revocations emitted as events?
- Can an administrator bypass pause controls?
- Does an upgrade preserve storage layout?
- Is a timelock applied to the same operations that the documentation describes as delayed?
- Can governance change an oracle, fee, or collateral parameter in a single transaction?
- Is there an emergency path that can recover funds without creating an unrestricted withdrawal mechanism?
Upgradeability requires a separate storage review. A proxy pattern can preserve an address while changing the implementation logic, but it does not eliminate the need to validate storage slots, initializer state, authorization, and compatibility with existing balances. A storage collision or incorrectly initialized implementation can turn a routine upgrade into a protocol-wide failure.
Governance itself is an attack vector. Token-based voting can be influenced by flash loans, delegated voting power, low quorum, vote buying, or a mismatch between snapshot timing and execution timing. A governance token may be technically secure while the decision process remains economically manipulable.
Security standards as a review map
The Smart Contract Security Verification Standard, version 1.2, organizes review work across 14 categories, including access control, gas usage and limitations, business logic, and decentralized finance. A standard of this kind is most useful as a coverage map.
It helps prevent a team from treating reentrancy as the entire security problem. A review should also cover:
- denial of service through unbounded loops or storage growth;
- unsafe assumptions about block timestamps and block ordering;
- oracle freshness, manipulation resistance, and fallback behavior;
- economic attacks involving flash loans and temporary liquidity;
- token compatibility and approval flows;
- event completeness for operational monitoring;
- deployment and initialization procedures;
- upgrade and emergency-response controls.
The checklist should produce evidence: test cases, design decisions, accepted risks, and owner assignments. A standard that exists only as a completed document has little defensive value.
An audit can identify defects in a defined scope; it cannot validate every future integration, governance decision, or market-dependent assumption.
Audit preparation determines audit quality
An external smart contract security audit is more effective when the codebase is stable and the protocol’s intended behavior is documented. Sending an auditor a moving target produces findings that are difficult to classify and encourages late changes without sufficient regression testing.
The audit package should include:
- the exact compiler version and optimizer configuration;
- a list of deployed and planned contracts;
- architecture documentation and trust assumptions;
- privileged roles and their intended operators;
- known limitations and accepted risks;
- a description of oracle, bridge, token, and protocol dependencies;
- the invariants used in testing;
- deployment scripts and initialization order;
- a clear scope boundary for excluded components.
The team should also provide threat scenarios rather than only function descriptions. For example, explain what happens if a token charges a transfer fee, if the oracle stops updating, if the administrator key is compromised, or if an attacker temporarily controls substantial voting power.
Findings must be triaged by exploitability and impact. A low-severity style issue should not distract from a business-logic error that allows a user to withdraw more than their recorded claim. Conversely, a finding that is not exploitable under current assumptions may become material after a future integration. The accepted-risk record should therefore state the assumption, its owner, and the condition that would trigger re-evaluation.
No audit report, static-analysis result, or test suite guarantees complete protection against exploits. The purpose of combining them is to reduce independent failure modes and make residual risk legible.
Deployment is a controlled state transition
The deployment transaction is part of the protocol, not an administrative afterthought. A correct implementation can be compromised by incorrect constructor parameters, a wrong chain ID, an uninitialized proxy, an incorrect oracle address, or a role granted to a temporary deployment account.
A production deployment sequence should be deterministic and reviewable. At minimum, verify:
1. The bytecode was built from the reviewed commit with pinned dependencies and compiler settings.
2. Constructor and initializer arguments match the intended network configuration.
3. Proxy implementation, admin, and beacon relationships are correct where applicable.
4. Roles are assigned to the intended multisig or governance controller.
5. Temporary deployer privileges are revoked.
6. Supported tokens, oracle addresses, fee recipients, and timelock addresses are confirmed on-chain.
7. Events from initialization and role changes are captured and inspected.
8. Contract verification and deployment metadata are published for independent review.
9. Critical functions are exercised in a controlled transaction after deployment.
10. Monitoring begins before user funds are accepted.
A deployment checklist should include a rollback or containment strategy even when the contract itself is immutable. For an immutable contract, rollback may mean pausing deposits, disabling a market, limiting new positions, or migrating users to a safer deployment. The available response depends on the design; it should not be invented during an incident.
Immutability changes the meaning of a defect. In an ordinary application, a patch can be deployed through a standard release pipeline. In a non-upgradeable smart contract, the deployed bytecode cannot be changed. In an upgradeable system, the code may be changeable, but the upgrade authority becomes part of the security boundary.
Post-deployment monitoring closes the operational gap
Security review ends at deployment only on paper. The live protocol introduces state combinations and interactions that no finite pre-deployment test suite can enumerate completely.
Monitoring should track both technical and economic signals:
- abnormal changes in total value, reserves, debt, or collateral ratios;
- unexpected role grants, ownership transfers, and implementation upgrades;
- oracle updates outside defined ranges;
- unusual borrow, liquidation, mint, burn, or withdrawal patterns;
- repeated failed transactions around a critical function;
- changes in gas consumption that may indicate state bloat;
- pauses, emergency withdrawals, and parameter changes;
- divergence between emitted events and expected accounting state.
Alert thresholds should be tied to protocol invariants rather than generic transaction volume. A large withdrawal may be valid in a liquid market. A small withdrawal that breaks the relationship between shares and assets may be a more meaningful signal.
State bloat deserves explicit attention. Any design that grows storage, iterates over user-controlled arrays, or accumulates inactive positions can eventually turn a valid function into an uncallable one because of gas limits. The failure may appear long after deployment, when the protocol has accumulated enough history. Gas usage and storage growth therefore belong in both pre-deployment modeling and post-deployment telemetry.
Monitoring also needs an incident procedure. Define who can pause the system, who can authorize an upgrade, how alerts are escalated, and which actions require multiple approvals. A technical control without an operator and a response time is only a latent capability.
The deployment standard is layered, not ceremonial
A defensible smart contracts development process does not ask whether the code has been audited once. It asks whether the protocol’s assumptions have been represented in code, tests, review artifacts, permissions, and monitoring.
The minimum credible sequence is:
- model the state transitions and economic invariants;
- use Solidity 0.8.x safely without treating checked arithmetic as a complete solution;
- run automated static analysis and review every material finding;
- test with unit, fuzz, invariant, and mainnet fork strategies;
- map external calls and reentrancy paths;
- minimize administrative privileges and protect them with multisig or timelock controls;
- use a recognized security standard to expose review gaps;
- provide auditors with stable code, threat assumptions, and reproducible tests;
- deploy through deterministic scripts with on-chain verification;
- monitor invariants, privileges, oracle behavior, and state growth after launch.
This workflow cannot eliminate uncertainty. It can, however, prevent the most common category error in DeFi security: confusing successful execution with correct execution.
Long-term network security depends on making protocol behavior observable and authority constrained. Regulatory compliance increasingly depends on the same properties, even when the legal terminology differs: clear control boundaries, traceable administrative actions, documented risk decisions, and the ability to respond when deployed software behaves outside its intended model. Smart contract deployment is therefore not the final step in development. It is the point at which engineering assumptions become public, persistent, and economically consequential.