Securite·7 min de lecture·Par Solingo

Smart Contract Audit Checklist — 20 Things to Check Before Deployment

Comprehensive pre-deployment checklist covering access control, reentrancy, overflow protection, input validation, gas optimization, events, and upgradability.

# Smart Contract Audit Checklist — 20 Things to Check Before Deployment

Deploying a smart contract to mainnet is permanent. There's no "undo" button, no hotfixes, no patches. One vulnerability can drain millions.

This checklist covers 20 critical security checks every developer should perform before deployment. Whether you're self-auditing or preparing for a professional audit, these points will help you catch dangerous issues early.

Access Control (1-3)

1. ✅ All Privileged Functions Protected

Every function that modifies critical state must have access control.

Check:

// BAD: Anyone can withdraw

function withdraw(uint256 amount) external {

payable(msg.sender).transfer(amount);

}

// GOOD: Only owner can withdraw

function withdraw(uint256 amount) external onlyOwner {

payable(msg.sender).transfer(amount);

}

Tools: Slither detects unprotected functions with --detect unprotected-upgrade

2. ✅ Owner Set Correctly in Constructor

Ensure ownership is properly initialized.

// BAD: Owner defaults to zero address

address public owner;

// GOOD: Owner set in constructor

constructor() {

owner = msg.sender;

}

Test: Verify owner is set correctly after deployment.

3. ✅ Two-Step Ownership Transfer

Prevent accidental ownership loss.

// BAD: Single-step, irreversible

function transferOwnership(address newOwner) external onlyOwner {

owner = newOwner; // Typo = permanent loss

}

// GOOD: Two-step process

address public pendingOwner;

function transferOwnership(address newOwner) external onlyOwner {

pendingOwner = newOwner;

}

function acceptOwnership() external {

require(msg.sender == pendingOwner);

owner = pendingOwner;

pendingOwner = address(0);

}

Use: OpenZeppelin's Ownable2Step

Reentrancy (4-5)

4. ✅ Checks-Effects-Interactions Pattern

State updates before external calls.

// BAD: External call before state update

function withdraw() external {

uint256 amount = balances[msg.sender];

(bool success,) = msg.sender.call{value: amount}("");

require(success);

balances[msg.sender] = 0; // TOO LATE

}

// GOOD: State update first

function withdraw() external {

uint256 amount = balances[msg.sender];

balances[msg.sender] = 0; // Update BEFORE call

(bool success,) = msg.sender.call{value: amount}("");

require(success);

}

5. ✅ ReentrancyGuard on External Calls

Defense in depth.

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract Safe is ReentrancyGuard {

function withdraw() external nonReentrant {

// Protected

}

}

Test: Write reentrancy attack tests to verify protection.

Integer Operations (6-7)

6. ✅ No Unchecked Math (Unless Intentional)

Solidity 0.8+ has built-in overflow protection. Don't disable it without reason.

// BAD: Unchecked without justification

unchecked {

balance += amount; // Can overflow

}

// GOOD: Use unchecked only when safe

unchecked {

// Loop counter won't realistically overflow

for (uint256 i = 0; i < items.length; ++i) {

// ...

}

}

7. ✅ Division Before Multiplication

Minimize rounding errors.

// BAD: Division first (loses precision)

uint256 result = (value / 100) * 25; // 0.25% of value

// GOOD: Multiplication first

uint256 result = (value * 25) / 100;

Input Validation (8-10)

8. ✅ All Inputs Validated

Never trust user input.

function setFee(uint256 _fee) external onlyOwner {

require(_fee <= 1000, "Fee too high"); // Max 10%

fee = _fee;

}

function transfer(address to, uint256 amount) external {

require(to != address(0), "Invalid address");

require(amount > 0, "Invalid amount");

// ...

}

9. ✅ Array Length Checks

Prevent out-of-gas errors.

function batchTransfer(address[] calldata recipients, uint256 amount) external {

require(recipients.length <= 100, "Too many recipients");

for (uint256 i = 0; i < recipients.length; i++) {

_transfer(msg.sender, recipients[i], amount);

}

}

10. ✅ Zero Address Checks

Prevent burning tokens/ETH accidentally.

function setTreasury(address _treasury) external onlyOwner {

require(_treasury != address(0), "Zero address");

treasury = _treasury;

}

Gas Optimization (11-13)

11. ✅ Storage Access Minimized

Cache storage reads in memory.

// BAD: Multiple storage reads

function calculateReward() external view returns (uint256) {

return userBalance[msg.sender] * rewardRate * stakingPeriod / 1e18;

// Each variable read from storage costs 2100 gas

}

// GOOD: Cache in memory

function calculateReward() external view returns (uint256) {

uint256 balance = userBalance[msg.sender]; // One storage read

uint256 rate = rewardRate;

uint256 period = stakingPeriod;

return balance * rate * period / 1e18;

}

12. ✅ Loop Gas Limits Considered

Unbounded loops can run out of gas.

// BAD: Unbounded loop

function distributeRewards() external {

for (uint256 i = 0; i < users.length; i++) {

// If users.length = 10000, this will fail

_transfer(users[i], rewardAmount);

}

}

// GOOD: Batch processing

function distributeRewards(uint256 start, uint256 end) external {

require(end <= users.length);

require(end - start <= 100, "Batch too large");

for (uint256 i = start; i < end; i++) {

_transfer(users[i], rewardAmount);

}

}

13. ✅ Efficient Data Types

Use appropriate sizes.

// BAD: Wasteful storage

struct User {

uint256 id; // Could be uint32

uint256 timestamp; // Could be uint32

bool active; // 1 byte but takes full slot

}

// GOOD: Packed storage

struct User {

uint32 id; // 4 bytes

uint32 timestamp; // 4 bytes

bool active; // 1 byte

// All fit in one 32-byte slot

}

Events (14-15)

14. ✅ Critical Actions Emit Events

Essential for off-chain tracking.

event Transfer(address indexed from, address indexed to, uint256 amount);

event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

event Paused(address account);

function transfer(address to, uint256 amount) external {

// ...

emit Transfer(msg.sender, to, amount);

}

15. ✅ Indexed Parameters for Filtering

Up to 3 indexed parameters per event.

// GOOD: Key fields indexed

event Deposit(

address indexed user,

address indexed token,

uint256 amount,

uint256 timestamp

);

External Interactions (16-17)

16. ✅ External Calls Handled Safely

Always check return values.

// BAD: Ignoring return value

token.transfer(recipient, amount);

// GOOD: Check return value

bool success = token.transfer(recipient, amount);

require(success, "Transfer failed");

// BETTER: Use SafeERC20

import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

using SafeERC20 for IERC20;

token.safeTransfer(recipient, amount); // Reverts on failure

17. ✅ msg.sender Used (Not tx.origin)

See our dedicated article on this.

// BAD: Phishing vulnerable

require(tx.origin == owner);

// GOOD: Secure

require(msg.sender == owner);

Upgradability (18-19)

18. ✅ Storage Layout Preserved (Upgradeable Contracts)

Don't rearrange variables in upgrades.

// V1

contract TokenV1 {

address public owner;

uint256 public totalSupply;

}

// BAD V2: Inserted variable breaks storage

contract TokenV2 {

address public owner;

uint256 public newVariable; // BREAKS totalSupply location

uint256 public totalSupply;

}

// GOOD V2: Append only

contract TokenV2 {

address public owner;

uint256 public totalSupply;

uint256 public newVariable; // Appended at end

}

19. ✅ Initialization (Not Constructor) for Proxies

Proxies don't execute constructors.

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract TokenUpgradeable is Initializable {

address public owner;

// BAD: Constructor won't run

constructor() {

owner = msg.sender;

}

// GOOD: Initialize function

function initialize() public initializer {

owner = msg.sender;

}

}

Testing (20)

20. ✅ Comprehensive Test Coverage

Aim for 100% line coverage, but also test:

Unit Tests:

function testOwnerCanWithdraw() public {

vm.prank(owner);

contract.withdraw(100);

assertEq(owner.balance, 100);

}

Negative Tests:

function testNonOwnerCannotWithdraw() public {

vm.prank(user);

vm.expectRevert("Not owner");

contract.withdraw(100);

}

Edge Cases:

function testWithdrawZero() public {

vm.prank(owner);

vm.expectRevert("Amount must be > 0");

contract.withdraw(0);

}

Fuzzing:

function testWithdrawFuzz(uint256 amount) public {

vm.assume(amount > 0 && amount <= address(contract).balance);

vm.prank(owner);

contract.withdraw(amount);

}

Tools to Run

Before deployment, run these automated tools:

# Static analysis

slither .

# Formal verification (if applicable)

certora-cli

# Test coverage

forge coverage

# Gas optimization

forge test --gas-report

Professional Audit

This checklist catches common issues, but professional audits are irreplaceable for high-value contracts.

When to get audited:

  • Managing >$100k in TVL
  • Complex DeFi logic (DEX, lending, derivatives)
  • Upgradeable contracts
  • Multi-signature or DAO governance
  • Before mainnet launch of any public protocol

Top audit firms:

  • Trail of Bits
  • OpenZeppelin
  • ConsenSys Diligence
  • Certora
  • ChainSecurity

Final Checklist

Before deploying to mainnet:

✅ All 20 points from this article addressed

✅ Slither run with no high/medium issues

✅ 100% test coverage achieved

✅ Fuzzing tests passed

✅ Gas optimizations applied

✅ Professional audit completed (if high-value)

✅ Deployment tested on testnet

✅ Emergency pause mechanism in place

✅ Bug bounty program prepared

✅ Documentation complete

Conclusion

Security isn't a checkbox — it's a mindset. This checklist provides a solid foundation, but staying secure requires:

  • Continuous learning (attacks evolve)
  • Conservative coding (simplicity > cleverness)
  • Defense in depth (multiple protections)
  • Humility (assume you missed something)

The stakes are real. A single vulnerability can cost millions. Take the time to audit thoroughly.

Practice secure development on Solingo — our platform includes automated security checks, real-world vulnerability simulations, and step-by-step remediation guides for all items on this checklist.

Prêt à mettre en pratique ?

Applique ces concepts avec des exercices interactifs sur Solingo.

Commencer gratuitement