ChainScore Labs
All Guides

Smart Contract Security Checklist Before Mainnet Deployment

LABS

Smart Contract Security Checklist Before Mainnet Deployment

Chainscore © 2025

Core Security Principles

Foundational security concepts that must be validated before any smart contract deployment to mainnet.

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 ReentrancyGuard for 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.

1

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 grantRole or transferOwnership for 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.

2

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-level delegatecall operations. 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 the nonReentrant modifier 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.

3

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 use uint256 and 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.

4

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 blockhash for 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.

5

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 initialize function. Confirm it is protected (e.g., initializer modifier) 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 upgradeTo function 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.

1

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 public and external function, 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 require and revert statements.
  • 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.

2

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.

3

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-informational on 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.

4

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.

5

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

1

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.

2

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.

3

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.

4

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) or grantRole(ADMIN_ROLE, multisigAddress).
  • Sub-step 3: Relinquish any remaining deployer privileges. If renouncing, call renounceOwnership() or renounceRole(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.

5

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(), and maxLimit() to ensure they match your deployment configuration.
  • Sub-step 3: Test a core user flow. Simulate a deposit or swap transaction using estimateGas and callStatic to 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/ServiceTypeKey CapabilitiesTypical 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

SECTION-POST_DEPLOYMENT

Post-Deployment Monitoring and Response

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.