The Difference Between Demo Contracts and Production Contracts
The Solidity documentation and most online tutorials focus on getting code to compile and run. Production contracts have additional requirements that tutorials rarely cover: gas efficiency, reentrancy protection, upgrade patterns, access control, and comprehensive test coverage. The financial consequences of missing any of these in a deployed contract can be catastrophic — unlike web applications, there's no "patch and redeploy" for on-chain vulnerabilities.
This guide covers what separates a contract that works in a test environment from one that's safe to deploy with real user funds.
The stakes context: $3.8 billion in crypto assets were lost to smart contract exploits in 2023. The vast majority of these exploits used attack vectors that are well-documented and preventable with correct engineering practices.
Contract Architecture Patterns
The single-responsibility principle: Each contract should do one thing. A token contract handles token logic. A staking contract handles staking. A governance contract handles governance. Monolithic contracts that do everything are harder to audit, harder to upgrade, and more expensive to deploy.
The proxy pattern for upgradeability: Solidity contracts are immutable by default — deployed bytecode cannot be changed. The OpenZeppelin transparent proxy pattern (or UUPS proxy) separates storage (proxy contract, permanent address) from logic (implementation contract, replaceable). Users interact with the proxy; the proxy delegates calls to the current implementation. This allows upgrading contract logic without changing the user-facing address or migrating storage.
The diamond pattern: For very large contracts that exceed the 24KB size limit, the Diamond/EIP-2535 pattern splits functionality across multiple "facets" accessed through a single proxy. Complex DeFi protocols and NFT marketplaces use this pattern.
Security: The Critical Vulnerabilities and How to Prevent Them
Reentrancy: The most famous vulnerability (exploited in The DAO hack, 2016). Occurs when a contract makes an external call before updating its own state — allowing the callee to re-enter the calling contract and exploit the stale state. Prevention: follow the Checks-Effects-Interactions pattern (validate, update state, then make external calls), use OpenZeppelin's ReentrancyGuard modifier.
Integer overflow/underflow: In Solidity <0.8.0, arithmetic operations could silently overflow. Solidity 0.8.0+ reverts on overflow by default. If you use unchecked {} blocks for gas optimisation, manually verify overflow safety.
Access control vulnerabilities: Functions that should be admin-only but aren't properly gated. Use OpenZeppelin's AccessControl or Ownable — don't implement custom role systems unless necessary. Apply the principle of least privilege: each role should have only the permissions it needs.
Oracle manipulation: Price feeds from a single on-chain source (like a DEX spot price) can be manipulated in a single block. Use Chainlink decentralised oracles for price data; use TWAP (time-weighted average price) for on-chain price feeds where Chainlink isn't available.
private visibility prevents other contracts from accessing a variable — it does NOT make blockchain data private. All contract storage is readable by anyone who queries the Ethereum state. Never store secrets, private keys, or sensitive data in contract storage.Testing with Hardhat: The Coverage You Need
Smart contract testing should be more rigorous than typical software testing — bugs can't be patched after deployment. Aim for 100% line coverage and 100% branch coverage, plus dedicated security scenario tests.
Testing structure with Hardhat + Ethers.js:
- Unit tests: Test each function in isolation with mocked dependencies. Cover happy path, edge cases, and reverts.
- Integration tests: Test multi-contract interactions that mirror production use.
- Security scenario tests: Explicitly test reentrancy attacks, integer bounds, and access control bypass attempts. Write the attack; verify the contract resists it.
- Fork tests: Fork mainnet (or testnet) at a specific block and test against real deployed protocol states. Catches issues that only manifest with real contract interactions.
Use Hardhat's hardhat-coverage plugin for coverage reporting. CI pipeline should enforce coverage thresholds — reject any PR that decreases coverage below 95%.
Gas Optimisation and Mainnet Deployment
Gas optimisation techniques:
- Storage is the most expensive operation: Minimise SSTORE calls. Use memory variables for intermediate calculations; only write final results to storage.
- Pack storage variables: Solidity stores variables in 32-byte slots. Pack multiple smaller variables into a single slot:
uint128 a; uint128 b;uses one slot vsuint256 a; uint256 b;using two. - Use events instead of storage for history: Events are much cheaper than storage and are sufficient for data that needs to be queried off-chain (transaction history, audit logs).
- Short-circuit boolean evaluations: Place cheaper conditions first in AND/OR chains — if the first condition fails, the second isn't evaluated.
Mainnet deployment checklist: Complete Slither static analysis (zero high/medium findings), complete manual audit by a qualified auditor, testnet deployment and stress testing, multi-sig ownership transfer, emergency pause mechanism verified, and monitoring via OpenZeppelin Defender.
Third-party audits: Any contract handling user funds above £50,000 should be audited by a professional smart contract security firm (Trail of Bits, OpenZeppelin, Zellic). Audit costs range from £15,000–£80,000 — cheap relative to the value at risk.