Foundational security concepts that must be validated before any smart contract deployment to mainnet.
Smart Contract Security Checklist Before Mainnet Deployment
Core Security Principles
Access Control
Role-based permissions define who can execute critical functions. Implement using modifiers like onlyOwner or OpenZeppelin's AccessControl.
- Restrict state-changing operations to authorized addresses.
- Use a multi-signature scheme for privileged admin actions.
- Proper access control prevents unauthorized minting, fund withdrawal, or parameter changes.
Reentrancy Guards
Reentrancy attacks occur when an external call allows a malicious contract to re-enter and exploit state changes.
- Apply the Checks-Effects-Interactions pattern.
- Use OpenZeppelin's
ReentrancyGuardfor functions with external calls. - This prevents attackers from draining funds through recursive callback exploits.
Integer Overflow/Underflow
Arithmetic overflows happen when a number exceeds its storage bounds, causing unexpected wraps.
- Use Solidity 0.8.x which has built-in overflow checks.
- For older versions, employ SafeMath libraries.
- This protects token balances and accounting logic from being manipulated.
Input Validation & Sanitization
Parameter validation ensures function inputs are within expected bounds before processing.
- Check for zero addresses, reasonable amount limits, and valid array lengths.
- Revert transactions early with descriptive error messages.
- This prevents logic errors and denial-of-service conditions from malformed data.
Upgradeability & Data Separation
Proxy patterns allow for fixing bugs post-deployment while preserving state.
- Implement using UUPS or Transparent Proxy standards.
- Keep logic and storage contracts separate.
- This enables security patches without requiring user migration or losing data.
Event Emission for Critical Actions
Event logging provides an immutable, off-chain record of on-chain state changes.
- Emit events for all significant transactions like transfers, approvals, and ownership changes.
- Include all relevant parameters as indexed and non-indexed data.
- This ensures transparency, enables subgraph indexing, and aids in debugging.
Manual Code Review Checklist
A systematic process for manually auditing smart contract code to identify vulnerabilities and logic flaws.
Review Access Control and Authorization
Examine all functions for proper permission checks and ownership models.
Detailed Instructions
Begin by mapping out the authorization model. Identify all onlyOwner, onlyRole, or custom modifier patterns. Verify that sensitive functions (e.g., fund withdrawal, parameter changes, pausing) are correctly protected. A common failure is missing checks on critical state-changing operations.
- Sub-step 1: Trace the inheritance chain for
Ownable,AccessControl, or similar contracts to confirm correct initialization. - Sub-step 2: For each external/public function, verify the presence and correctness of access control modifiers. Check for functions that should be internal/private but are exposed.
- Sub-step 3: Examine role management functions like
grantRoleortransferOwnershipfor proper timelocks or multi-signature requirements.
solidity// Example: Check for missing onlyOwner modifier function withdrawAll() external { // Missing modifier! payable(msg.sender).transfer(address(this).balance); }
Tip: Use a tool like Slither to generate a function summary, then manually verify each function's visibility and permissions.
Analyze External Calls and Reentrancy Guards
Inspect all interactions with external contracts and ensure state is managed safely.
Detailed Instructions
External calls to untrusted contracts are a primary attack vector. Apply the checks-effects-interactions pattern rigorously. Look for any state changes that occur after an external call, which can enable reentrancy attacks.
- Sub-step 1: Identify all
.call,.transfer,.send, and low-leveldelegatecalloperations. Note the target addresses (are they user-supplied?). - Sub-step 2: For each call, check if the contract state (balances, flags, counters) is updated before the call. If not, it's a vulnerability.
- Sub-step 3: Verify the use of reentrancy guards like OpenZeppelin's
ReentrancyGuard. Ensure thenonReentrantmodifier is applied to all functions making external calls.
solidity// Vulnerable pattern: State update after call function withdraw(uint amount) public { require(balances[msg.sender] >= amount); (bool success, ) = msg.sender.call{value: amount}(""); // Interaction first require(success); balances[msg.sender] -= amount; // Effects last - DANGER! }
Tip: Manually trace the flow of functions that call other functions in the same contract, as internal reentrancy is often overlooked.
Audit Arithmetic Operations and Data Types
Check for integer overflows/underflows and correct data type usage.
Detailed Instructions
Solidity's fixed-size integers can silently wrap. Since Solidity 0.8.x, built-in overflow checks exist, but they can be disabled with unchecked blocks. Scrutinize all arithmetic, especially in loops and calculations involving user input.
- Sub-step 1: Locate all
unchecked { ... }blocks. Verify the logic inside is safe and does not allow an overflow/underflow to break invariants. - Sub-step 2: Check for division before multiplication, which can lead to precision loss. Prefer multiplying first to maintain fidelity.
- Sub-step 3: Verify that calculations for fees, rewards, or ratios use sufficient precision (e.g., scaling by
1e18). Ensure token amounts useuint256and are compatible with the token's decimals.
solidity// Example: Precision loss risk function calculateShare(uint amount, uint total) public pure returns (uint) { return (amount / total) * 100; // Division first, result may be 0 // Better: return (amount * 100) / total; }
Tip: Pay special attention to calculations in
for-loop conditions. An overflow in the loop counter can cause infinite gas consumption or unexpected termination.
Validate Event Emissions and Off-Chain Logic
Ensure all critical state changes are logged and off-chain dependencies are handled.
Detailed Instructions
Events are essential for off-chain monitoring and creating a transparent audit trail. Their absence can hide malicious activity. Also, review any logic that depends on block data (like block.timestamp or blockhash), which is manipulable by miners/validators to a degree.
- Sub-step 1: For every function that updates key storage variables (balances, ownership, totals), verify a corresponding event is emitted with all relevant parameters.
- Sub-step 2: Check the use of
block.timestamp. It should not be used for precise timing or as a sole source of randomness. For deadlines, a tolerance (e.g.,> block.timestamp + 5 minutes) is acceptable. - Sub-step 3: Inspect any reliance on
blockhashfor randomness. This is insecure for high-value applications; consider a commit-reveal scheme or oracle.
solidity// Good practice: Emitting an event for a critical transfer event TokensTransferred(address indexed from, address indexed to, uint256 amount); function _transfer(address from, address to, uint256 amount) internal { balances[from] -= amount; balances[to] += amount; emit TokensTransferred(from, to, amount); // Essential log }
Tip: Compare the contract's event list with the project's proposed front-end or subgraph queries to ensure all necessary data is captured.
Examine Upgradeability and Initialization Patterns
Review proxy patterns, initialization functions, and storage layout safety.
Detailed Instructions
If the contract uses upgradeability (e.g., UUPS or Transparent Proxy), the risks multiply. The primary concern is storage collisions between the proxy and implementation, and secure initialization to prevent takeover.
- Sub-step 1: Verify the use of established libraries like OpenZeppelin's UUPS or Transparent upgradeable contracts. Check that the implementation contract inherits the correct base (e.g.,
UUPSUpgradeable). - Sub-step 2: Locate the
initializefunction. Confirm it is protected (e.g.,initializermodifier) and that critical state variables are set only once. A missing initializer allows re-initialization attacks. - Sub-step 3: Manually review the storage layout of the implementation. Ensure no variable declarations are added or re-ordered in a way that would corrupt storage slots when upgrading.
solidity// Example: Secure initializer using OZ import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; contract MyContract is Initializable { address public owner; function initialize(address _owner) public initializer { owner = _owner; // Can only be called once } }
Tip: For UUPS, explicitly check that the
upgradeTofunction is secured with proper access control, as it resides in the implementation itself.
Comprehensive Testing Protocol
A systematic process for validating smart contract security and functionality before deployment.
Establish Unit Test Coverage
Write and execute isolated tests for all contract functions.
Detailed Instructions
Begin by writing unit tests for each individual function in your smart contract. Use a framework like Hardhat or Foundry to isolate and test core logic, including edge cases and failure modes. Aim for 100% branch coverage to ensure every possible code path is executed.
- Sub-step 1: For each
publicandexternalfunction, create a test that calls it with valid inputs. - Sub-step 2: Create negative tests for each function, passing invalid inputs or calling from unauthorized addresses to test
requireandrevertstatements. - Sub-step 3: Verify that state changes (like balances in a mapping) are updated correctly after each function call.
solidity// Example Foundry test for a mint function function test_MintIncreasesBalance() public { uint256 initialBalance = token.balanceOf(user); vm.prank(owner); token.mint(user, 100); assertEq(token.balanceOf(user), initialBalance + 100); }
Tip: Use fuzzing (e.g., Foundry's
forge test --fuzz-runs 10000) to automatically generate a wide range of inputs and discover edge cases.
Conduct Integration Testing
Test contract interactions with external dependencies and other contracts.
Detailed Instructions
Simulate real-world interactions by testing your contracts in conjunction with their dependencies. This includes oracles, other protocol contracts, and token standards. Deploy a local forked mainnet environment using tools like Hardhat's hardhat node --fork to test with real contract addresses and state.
- Sub-step 1: Deploy mock versions of external dependencies (e.g., a mock Chainlink aggregator) and test your contract's interaction with them.
- Sub-step 2: On a forked network, test interactions with live contracts, such as swapping tokens on Uniswap V3 (0xE592427A0AEce92De3Edee1F18E0157C05861564).
- Sub-step 3: Verify that cross-contract calls and delegatecall operations preserve expected security contexts and state isolation.
javascript// Hardhat example: Forking mainnet for an integration test await hre.network.provider.request({ method: "hardhat_reset", params: [{ forking: { jsonRpcUrl: process.env.MAINNET_RPC_URL, blockNumber: 18900000 } }] });
Tip: Pay special attention to reentrancy guards when testing callback patterns common in DeFi integrations.
Execute Formal Verification and Static Analysis
Use automated tools to mathematically prove correctness and detect common vulnerabilities.
Detailed Instructions
Apply formal verification and static analysis tools to mathematically prove the absence of certain bug classes. Use Slither for static analysis to detect vulnerabilities like incorrect ERC-20 interfaces or dangerous delegatecall usage. For critical invariants, use a tool like Certora Prover or the Scribble specification language with MythX to write formal rules.
- Sub-step 1: Run
slither . --exclude-informationalon your codebase and address all high-severity findings. - Sub-step 2: Define key invariants (e.g., "total supply must equal sum of all balances") and encode them as Scribble annotations in your source code.
- Sub-step 3: Run the MythX CLI with the Scribble plugin to verify these invariants hold across all possible execution paths.
solidity// Example Scribble annotation for an invariant /// #invariant {:msg "Total supply consistency"} unchecked_sum(_balances) == _totalSupply; mapping(address => uint256) private _balances;
Tip: Integrate these tools into your CI/CD pipeline to automatically block merges if new violations are introduced.
Perform Manual and Scenario-Based Testing
Conduct structured manual reviews and simulate complex user and attack scenarios.
Detailed Instructions
Beyond automated checks, conduct manual code review and design scenario tests. Create detailed test plans that simulate malicious actor behavior, governance actions, and edge-case user flows. Use a testnet like Sepolia or Goerli to deploy the final candidate and execute these scenarios in a live environment.
- Sub-step 1: Manually review all access control modifiers, pause mechanisms, and upgrade paths for logic errors.
- Sub-step 2: Script a scenario where a whale user deposits 10,000 ETH, triggering all fee and reward calculations at scale.
- Sub-step 3: Simulate a front-running attack by bundling a victim transaction with a malicious transaction in a single block using Hardhat's
hardhat_setNextBlockBaseFeePerGas.
javascript// Simulating a specific block state for scenario testing await network.provider.send("hardhat_setNextBlockBaseFeePerGas", ["0x2540be400"]); // 10 Gwei base fee await network.provider.send("evm_mine");
Tip: Document every scenario test and its expected outcome. This suite becomes a regression test for future upgrades.
Run a Testnet Bug Bounty and Final Audit
Deploy to a public testnet and initiate a controlled bug bounty program before the final audit.
Detailed Instructions
Deploy the audited code to a public testnet and fund a bug bounty program for whitehat hackers. Allocate a portion of the treasury (e.g., 1-5%) for critical bug rewards. Run the bounty for a fixed period (e.g., 2-4 weeks) on a platform like Immunefi. Concurrently, engage a reputable security audit firm for a final review, providing them with all prior test results and documentation.
- Sub-step 1: Deploy the final testnet version with a verified source code on Etherscan for transparency.
- Sub-step 2: Create a clear scope and reward schedule for the bug bounty, specifying which contracts (by address) are in scope.
- Sub-step 3: Compile all findings from the bug bounty and the final audit into a single report. Triage each issue, implement fixes, and re-run the unit and integration test suites.
Tip: Ensure the audit firm reviews the fixed code. Do not proceed to mainnet until all Critical and High severity issues from both the bounty and audit are resolved and verified.
Audit and Formal Verification
Understanding the Security Review Process
Audits and formal verification are distinct but complementary security checks. A smart contract audit is a manual and automated review by security experts to find bugs and vulnerabilities, similar to a code review. Formal verification uses mathematical proofs to verify a contract's logic matches its specification, ensuring it behaves exactly as intended under all conditions.
Key Differences
- Audit Scope: Audits find known vulnerabilities (e.g., reentrancy, overflow) but cannot prove their absence. They rely on expert heuristics and testing.
- Verification Proof: Formal verification mathematically proves the code is logically correct against a formal spec, offering higher assurance for critical functions.
- Practical Use: Most projects get an audit. Formal verification is used for high-value, complex contracts like decentralized exchange engines or lending protocol core logic, as seen in projects like MakerDAO and Compound.
Example
When a protocol like Uniswap v3 deploys a new factory contract, it undergoes multiple audits from firms like Trail of Bits and ABDK. For its concentrated liquidity math, which is novel and critical, the team might also employ formal verification tools to prove that the fee calculation and tick management are flawless.
Secure Deployment and Initialization
Process overview
Deploy to a Testnet First
Validate contract behavior in a live, low-stakes environment.
Detailed Instructions
Deploy your compiled contract bytecode to a public testnet like Sepolia or Goerli. This step is critical for verifying that your deployment scripts, constructor arguments, and environment configurations work as intended outside of a local development network. Use the same tools and procedures you plan to use for mainnet.
- Sub-step 1: Execute your deployment script (e.g., Hardhat script, Foundry script) targeting the testnet RPC URL.
- Sub-step 2: Monitor the transaction for success and confirm the contract address is created. Use a block explorer to verify the deployment.
- Sub-step 3: Interact with the newly deployed contract using a simple script to call a non-critical function, ensuring the initialization state is correct.
bash# Example Foundry command for testnet deployment forge script script/Deploy.s.sol:DeployScript \ --rpc-url $SEPOLIA_RPC_URL \ --private-key $DEPLOYER_PRIVATE_KEY \ --broadcast
Tip: Fund your testnet deployer address sufficiently and verify gas estimations are realistic before proceeding to mainnet.
Verify and Publish Source Code
Make your contract source code publicly verifiable on block explorers.
Detailed Instructions
After deployment, immediately verify your smart contract source code on the relevant block explorer (Etherscan, Arbiscan). This provides transparency, allows for independent auditing, and is often required for user trust and dApp integration. Use the explorer's UI or an automated plugin.
- Sub-step 1: Gather all necessary files, including the main contract, imported libraries, and interfaces. Ensure your compiler version and optimization settings match the deployment exactly.
- Sub-step 2: Submit the source code for verification. For complex setups, use the "Via Flattened Source Code" method or a tool like
hardhat-etherscan. - Sub-step 3: Confirm successful verification on the explorer. The contract tab should show a "Contract Source Code Verified" badge and the "Read Contract" and "Write Contract" interfaces.
javascript// Example using Hardhat Etherscan plugin await hre.run("verify:verify", { address: deployedContractAddress, constructorArguments: ["0xabc...", 1000000], });
Tip: Include NatSpec comments in your code; they will be displayed on the explorer, improving usability for other developers.
Initialize Contract State Securely
Set up initial parameters, roles, and ownership without introducing vulnerabilities.
Detailed Instructions
Execute the contract's initialization function, if any, to set crucial state variables like admin addresses, fee parameters, or oracle addresses. This must be done in a single, atomic transaction immediately after deployment to prevent the contract from being in an uninitialized, vulnerable state.
- Sub-step 1: Prepare the initialization transaction. Encode the function call with all required arguments (e.g.,
initialize(address admin, uint256 feeBps)). - Sub-step 2: Send the transaction from the deployer address. Use a moderate gas limit to ensure it doesn't fail. Do not broadcast this over a public mempool if it contains sensitive addresses; use a private transaction relay if necessary.
- Sub-step 3: Verify the initialization was successful by checking the contract's public view functions (e.g.,
owner(),getFee()) on a block explorer.
solidity// Example of a simple initializer with access control function initialize(address _treasury, uint256 _initialFee) external initializer { __Ownable_init(msg.sender); treasury = _treasury; fee = _initialFee; }
Tip: For upgradeable contracts (UUPS/Transparent), always call the initializer through the proxy, not the implementation contract address.
Renounce or Transfer Privileged Roles
Finalize the access control configuration by limiting or removing deployer powers.
Detailed Instructions
Many contracts start with the deployer holding powerful roles like owner, admin, or DEFAULT_ADMIN_ROLE. Decide and execute the final configuration for these privileges. For truly decentralized protocols, consider renouncing ownership entirely. For managed contracts, transfer roles to a secure multisig or governance contract.
- Sub-step 1: Determine the final authority. If using a multisig, ensure it is fully set up with the correct signer addresses and threshold (e.g., Gnosis Safe at
0x...with 3-of-5 signers). - Sub-step 2: Execute the role transfer. Call functions like
transferOwnership(address newOwner)orgrantRole(ADMIN_ROLE, multisigAddress). - Sub-step 3: Relinquish any remaining deployer privileges. If renouncing, call
renounceOwnership()orrenounceRole(ADMIN_ROLE, deployerAddress). This is irreversible.
solidity// Sequence for transferring and then renouncing a role accessControl.grantRole(ADMIN_ROLE, multisigAddr); accessControl.revokeRole(ADMIN_ROLE, msg.sender); // Revoke from self
Tip: Always verify the new role holder can perform administrative actions before the deployer revokes their own access, to avoid locking the contract.
Conduct Final On-Chain Verification
Perform a series of checks to confirm the live contract matches the intended specification.
Detailed Instructions
Before announcing the deployment, conduct a final verification by interacting with the live mainnet contract. This ensures no last-minute configuration errors were introduced and that all security features are active.
- Sub-step 1: Verify access control. Attempt to call a protected function (e.g.,
pause()) from a non-admin address using a read-only call (callStatic) to confirm it reverts. - Sub-step 2: Check critical state variables. Query values like
paused(),owner(),fee(), andmaxLimit()to ensure they match your deployment configuration. - Sub-step 3: Test a core user flow. Simulate a deposit or swap transaction using
estimateGasandcallStaticto ensure the main logic executes without unexpected reverts or excessive gas costs.
javascript// Example using ethers.js for static calls const isPaused = await contract.callStatic.paused(); console.assert(isPaused === false, "Contract should not be paused on deployment"); const estimatedGas = await contract.estimateGas.deposit(ETH(1)); console.assert(estimatedGas.lt(300000), "Gas estimate seems unusually high");
Tip: Use a separate, freshly funded verification wallet for these checks to avoid any risk of interacting with the main deployer keys.
Security Tooling and Analysis
Comparison of automated analysis tools and services for smart contract auditing.
| Tool/Service | Type | Key Capabilities | Typical Cost/Model |
|---|---|---|---|
Slither | Static Analysis Framework | Detects 100+ vulnerability patterns, visualizes inheritance, computes metrics | Free / Open Source |
Mythril | Symbolic Execution Engine | Analyzes EVM bytecode, detects SWC vulnerabilities, taint analysis | Free / Open Source |
CertiK | Professional Audit Firm | Formal verification, manual review, runtime monitoring (Skynet) | $50k+ / Project-based |
ConsenSys Diligence | Professional Audit Firm | Manual review, automated testing (MythX), security best practices | $30k+ / Project-based |
OpenZeppelin Defender | Security Operations Platform | Admin automation, access controls, monitoring, incident response | Tiered SaaS, ~$500/month |
Forta Network | Runtime Monitoring | Real-time threat detection via decentralized agent network | Free tier + paid agent subscriptions |
Echidna | Fuzzing Tool | Property-based testing, generates random inputs to break invariants | Free / Open Source |
Post-Deployment Monitoring and Response
Further Reading and Resources
Ready to Start Building?
Let's bring your Web3 vision to life.
From concept to deployment, ChainScore helps you architect, build, and scale secure blockchain solutions.