The Ultimate Solidity Security Checklist (2026)

Last updated: April 2026 · 14 min read

Every major smart contract exploit in 2025 could have been prevented by following known security practices. The problem is not a lack of knowledge — it is a lack of systematic checking. Developers know about reentrancy, but miss the edge case in their specific implementation.

This checklist covers the 8 most exploited vulnerability categories in Solidity, with code examples showing both the vulnerable pattern and the fix. Use it as a pre-deployment review guide.

Want a printable version? The spectr-ai Security Checklist ($19) includes this checklist as a structured template with automated scanning for each item, plus additional checks for DeFi-specific patterns.

1. Reentrancy

Reentrancy remains the most exploited vulnerability class. It occurs when an external call allows the called contract to re-enter the calling function before state updates are complete.

Vulnerable Pattern

VULNERABLE
function withdraw(uint amount) external {
    require(balances[msg.sender] >= amount);

    // External call BEFORE state update
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success);

    // State update happens after the call
    // Attacker re-enters before this line executes
    balances[msg.sender] -= amount;
}

Fix: Checks-Effects-Interactions

SECURE
function withdraw(uint amount) external nonReentrant {
    require(balances[msg.sender] >= amount);

    // State update BEFORE external call
    balances[msg.sender] -= amount;

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

Checklist

2. Access Control

Missing or incorrect access control allows unauthorized users to call admin functions, drain funds, or modify contract state. This includes unprotected initializers in proxy patterns.

Vulnerable Pattern

VULNERABLE
// No access control — anyone can call
function setPrice(uint newPrice) external {
    price = newPrice;
}

// Initializer without protection
function initialize(address _owner) external {
    owner = _owner;  // Can be called by anyone, even after init
}

Fix: Explicit Access Control

SECURE
function setPrice(uint newPrice) external onlyOwner {
    price = newPrice;
}

// Protected initializer
function initialize(address _owner) external initializer {
    __Ownable_init(_owner);
}

Checklist

3. Integer Handling

Solidity 0.8+ has built-in overflow protection, but unchecked blocks, casting, and division edge cases still cause issues. Precision loss in financial calculations is a common source of bugs.

Vulnerable Pattern

VULNERABLE
// Precision loss — division before multiplication
function calculateFee(uint amount, uint rate)
    external pure returns (uint)
{
    // If amount=99, rate=100, basis=10000:
    // 99/10000 = 0, then 0*100 = 0 (fee lost!)
    return (amount / 10000) * rate;
}

// Unsafe downcast
function storeBalance(uint256 amount) external {
    // Silently truncates values > type(uint128).max
    userBalance[msg.sender] = uint128(amount);
}

Fix: Safe Math Patterns

SECURE
// Multiplication before division
function calculateFee(uint amount, uint rate)
    external pure returns (uint)
{
    return (amount * rate) / 10000;
}

// Safe downcast with OpenZeppelin
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";

function storeBalance(uint256 amount) external {
    // Reverts if amount > type(uint128).max
    userBalance[msg.sender] = SafeCast.toUint128(amount);
}

Checklist

4. External Calls

Every external call is a potential attack vector. Low-level calls can silently fail, return data can be manipulated, and gas forwarding enables griefing attacks.

Vulnerable Pattern

VULNERABLE
// Return value not checked — silent failure
function distribute(address payable recipient, uint amount)
    external
{
    recipient.send(amount);  // Returns false on failure, ignored
}

// Unbounded gas forwarding
function execute(address target, bytes calldata data)
    external
{
    // Forwards all gas — target can consume it
    target.call(data);
}

Fix: Safe External Calls

SECURE
// Check return value
function distribute(address payable recipient, uint amount)
    external
{
    (bool success, ) = recipient.call{value: amount}("");
    require(success, "Transfer failed");
}

// Or use OpenZeppelin's Address library
import {Address} from "@openzeppelin/contracts/utils/Address.sol";

function distribute(address payable recipient, uint amount)
    external
{
    Address.sendValue(recipient, amount);
}

Checklist

5. Token Handling

Not all ERC-20 tokens behave the same. Fee-on-transfer tokens, rebasing tokens, tokens with blocklists, and tokens that return false instead of reverting all break naive implementations.

Vulnerable Pattern

VULNERABLE
// Assumes transfer amount equals received amount
function deposit(uint amount) external {
    token.transferFrom(msg.sender, address(this), amount);
    // Fee-on-transfer tokens: actual received < amount
    balances[msg.sender] += amount;  // Inflated balance!
}

Fix: Measure Actual Balance Change

SECURE
function deposit(uint amount) external {
    uint balanceBefore = token.balanceOf(address(this));
    SafeERC20.safeTransferFrom(token, msg.sender, address(this), amount);
    uint received = token.balanceOf(address(this)) - balanceBefore;
    balances[msg.sender] += received;
}

Checklist

6. Oracle Security

Price oracles are a frequent attack vector in DeFi. Spot prices from AMMs can be manipulated in a single transaction via flash loans. Stale oracle data leads to incorrect pricing.

Vulnerable Pattern

VULNERABLE
// Using spot price — manipulable via flash loan
function getPrice() external view returns (uint) {
    (uint reserve0, uint reserve1, ) = pair.getReserves();
    return (reserve1 * 1e18) / reserve0;  // Flash-loanable!
}

Fix: Use Time-Weighted Oracles

SECURE
function getPrice() external view returns (uint) {
    (
        ,
        int256 price,
        ,
        uint256 updatedAt,

    ) = chainlinkFeed.latestRoundData();

    // Check for stale data
    require(
        block.timestamp - updatedAt < STALENESS_THRESHOLD,
        "Stale price"
    );
    require(price > 0, "Invalid price");

    return uint256(price);
}

Checklist

7. Denial of Service

DoS vulnerabilities prevent legitimate users from interacting with the contract. Common vectors include unbounded loops over user-controlled arrays and strict equality checks on balances.

Vulnerable Pattern

VULNERABLE
// Unbounded loop — gas limit DoS
function distributeRewards() external {
    // If stakers array grows large enough,
    // this exceeds block gas limit
    for (uint i = 0; i < stakers.length; i++) {
        token.transfer(stakers[i], rewards[stakers[i]]);
    }
}

// Strict balance check — can be broken by dust deposit
function withdrawAll() external {
    require(address(this).balance == totalDeposits);
    // Attacker sends 1 wei via selfdestruct, breaks equality
}

Fix: Pull Pattern and Inequality Checks

SECURE
// Pull pattern — users claim individually
function claimReward() external {
    uint reward = rewards[msg.sender];
    require(reward > 0, "No reward");
    rewards[msg.sender] = 0;
    token.transfer(msg.sender, reward);
}

// Use >= instead of ==
function withdrawAll() external {
    require(address(this).balance >= totalDeposits);
}

Checklist

8. Testing and Verification

Testing is not optional. Every vulnerability category above should have explicit test cases that verify the protection works.

Checklist

Pre-Deployment Summary

Before deploying to mainnet, run through this condensed checklist:

  1. Run Slither and spectr-ai — fix all findings
  2. Verify all external calls follow checks-effects-interactions
  3. Confirm access control on every state-changing function
  4. Test with fee-on-transfer and rebasing tokens if applicable
  5. Validate oracle staleness thresholds match real-world update frequencies
  6. Ensure no unbounded loops exist
  7. Run fuzz tests with at least 10,000 runs
  8. Deploy to testnet and run full integration tests
  9. Have another developer review the final code
  10. Document all known limitations and assumptions

Automate This Checklist

The spectr-ai Security Checklist ($19) turns this guide into an automated workflow. Scan your contracts against each item, get severity ratings, and export a pre-audit report. The spectr-ai Pro API integrates into your CI/CD pipeline.

Get the Checklist ($19) ->

Or try the free open-source engine for basic scanning.