Solidity Security Tips

Write secure Solidity smart contracts. This guide covers Solidity-specific security patterns and best practices to prevent common vulnerabilities. --- solidity...

Last updated: January 14, 2026

Solidity Security Tips

Write secure Solidity smart contracts.

Overview

This guide covers Solidity-specific security patterns and best practices to prevent common vulnerabilities.


Compiler Settings

Use Latest Stable Version

// Good: Specific recent version
pragma solidity 0.8.20;

// Avoid: Floating pragma
pragma solidity ^0.8.0;

// Avoid: Old versions
pragma solidity 0.6.12;

Optimizer Settings

# foundry.toml
[profile.default]
optimizer = true
optimizer_runs = 200  # Balance deployment vs. runtime cost

[profile.production]
optimizer = true
optimizer_runs = 10000  # More runtime-optimized

Access Control

Use OpenZeppelin

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";

// Simple ownership
contract Token is Ownable {
    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }
}

// Role-based access
contract Vault is AccessControl {
    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
    bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");

    function withdrawFees() public onlyRole(ADMIN_ROLE) {
        // ...
    }

    function pauseVault() public onlyRole(OPERATOR_ROLE) {
        // ...
    }
}

Two-Step Ownership Transfer

import "@openzeppelin/contracts/access/Ownable2Step.sol";

contract SafeOwnable is Ownable2Step {
    // transferOwnership() requires acceptOwnership() from new owner
}

Reentrancy Protection

Use ReentrancyGuard

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

contract Vault is ReentrancyGuard {
    function withdraw(uint256 amount) public nonReentrant {
        require(balances[msg.sender] >= amount);
        balances[msg.sender] -= amount;
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success);
    }
}

Checks-Effects-Interactions Pattern

function withdraw(uint256 amount) public {
    // 1. Checks
    require(balances[msg.sender] >= amount, "Insufficient balance");
    require(amount > 0, "Amount must be positive");

    // 2. Effects (state changes)
    balances[msg.sender] -= amount;
    totalDeposits -= amount;

    // 3. Interactions (external calls)
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success, "Transfer failed");

    emit Withdrawal(msg.sender, amount);
}

Safe External Calls

SafeERC20 for Token Transfers

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

contract TokenHandler {
    using SafeERC20 for IERC20;

    function deposit(IERC20 token, uint256 amount) public {
        // Handles non-standard tokens (USDT, etc.)
        token.safeTransferFrom(msg.sender, address(this), amount);
    }

    function withdraw(IERC20 token, address to, uint256 amount) public {
        token.safeTransfer(to, amount);
    }
}

Check Low-Level Call Results

function sendEther(address payable to, uint256 amount) public {
    // BAD
    to.call{value: amount}("");

    // GOOD
    (bool success, ) = to.call{value: amount}("");
    require(success, "Transfer failed");
}

Input Validation

Validate All Parameters

function setConfig(
    address token,
    uint256 fee,
    uint256 maxAmount
) public onlyOwner {
    require(token != address(0), "Invalid token");
    require(fee <= MAX_FEE, "Fee too high");
    require(maxAmount >= MIN_AMOUNT, "Amount too low");
    require(maxAmount <= MAX_AMOUNT, "Amount too high");

    config.token = token;
    config.fee = fee;
    config.maxAmount = maxAmount;
}

Use Custom Errors (Gas Efficient)

error InvalidAddress(address provided);
error FeeTooHigh(uint256 provided, uint256 maximum);
error AmountOutOfRange(uint256 provided, uint256 min, uint256 max);

function setFee(uint256 newFee) public onlyOwner {
    if (newFee > MAX_FEE) {
        revert FeeTooHigh(newFee, MAX_FEE);
    }
    fee = newFee;
}

Arithmetic Safety

Solidity 0.8+ Built-in Protection

// Automatically reverts on overflow/underflow
uint256 a = type(uint256).max;
uint256 b = a + 1;  // Reverts

// Use unchecked only when overflow is impossible
function sumArray(uint256[] memory arr) public pure returns (uint256) {
    uint256 sum;
    for (uint256 i; i < arr.length; ) {
        sum += arr[i];
        unchecked { ++i; }  // Loop counter can't overflow
    }
    return sum;
}

Division Precision

// BAD: Precision loss
uint256 share = amount / totalSupply * userBalance;

// GOOD: Multiply before divide
uint256 share = amount * userBalance / totalSupply;

// BEST: Use higher precision
uint256 PRECISION = 1e18;
uint256 share = amount * userBalance * PRECISION / totalSupply / PRECISION;

Storage Patterns

Initialize Properly

contract Initializable {
    bool private _initialized;

    modifier initializer() {
        require(!_initialized, "Already initialized");
        _initialized = true;
        _;
    }

    function initialize(address admin) public initializer {
        _admin = admin;
    }
}

Use Immutable for Constants

contract Token {
    // Gas efficient - stored in bytecode
    address public immutable owner;
    uint256 public immutable deployTime;
    IERC20 public immutable baseToken;

    constructor(IERC20 _baseToken) {
        owner = msg.sender;
        deployTime = block.timestamp;
        baseToken = _baseToken;
    }
}

Event Emission

Log All State Changes

event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
event FeeUpdated(uint256 oldFee, uint256 newFee);
event Paused(address account);
event Unpaused(address account);

function setOwner(address newOwner) public onlyOwner {
    emit OwnershipTransferred(owner, newOwner);
    owner = newOwner;
}

Use Indexed Parameters

event Transfer(
    address indexed from,    // Indexed for filtering
    address indexed to,      // Indexed for filtering
    uint256 amount          // Not indexed - stored in data
);

Gas Optimization Without Sacrificing Security

Safe Optimizations

// Cache array length
function process(uint256[] memory items) public {
    uint256 length = items.length;  // Cache
    for (uint256 i; i < length; ) {
        // Process items[i]
        unchecked { ++i; }  // Safe: i < length
    }
}

// Use calldata for read-only arrays
function sum(uint256[] calldata numbers) public pure returns (uint256) {
    // calldata is cheaper than memory
}

// Pack structs
struct User {
    address wallet;     // 20 bytes
    uint64 balance;     // 8 bytes
    uint32 lastUpdate;  // 4 bytes
    // Total: 32 bytes (1 slot)
}

Avoid Unsafe Optimizations

// DON'T skip checks for gas savings
function unsafeTransfer(address to, uint256 amount) public {
    // Skipping balance check is dangerous!
    balances[msg.sender] -= amount;
    balances[to] += amount;
}

// DO include safety checks
function safeTransfer(address to, uint256 amount) public {
    require(balances[msg.sender] >= amount, "Insufficient");
    require(to != address(0), "Invalid recipient");
    balances[msg.sender] -= amount;
    balances[to] += amount;
}

Common Pitfalls

tx.origin vs msg.sender

// BAD: tx.origin can be manipulated
function withdraw() public {
    require(tx.origin == owner);  // Vulnerable to phishing
}

// GOOD: Use msg.sender
function withdraw() public {
    require(msg.sender == owner);
}

Delegatecall Dangers

// BAD: User-controlled delegatecall
function execute(address target) public {
    target.delegatecall(msg.data);
}

// GOOD: Controlled implementation
address public implementation;

function upgrade(address newImpl) public onlyOwner {
    implementation = newImpl;
}

Approve Race Condition

// Use increaseAllowance/decreaseAllowance instead of approve
function safeIncreaseAllowance(
    IERC20 token,
    address spender,
    uint256 addedValue
) internal {
    uint256 currentAllowance = token.allowance(address(this), spender);
    token.approve(spender, currentAllowance + addedValue);
}

Testing Recommendations

Test Security Properties

function test_ReentrancyProtection() public {
    ReentrantAttacker attacker = new ReentrantAttacker(vault);
    vm.expectRevert();
    attacker.attack();
}

function test_AccessControlEnforced() public {
    vm.prank(nonOwner);
    vm.expectRevert("Ownable: caller is not the owner");
    vault.withdrawFees();
}

function testFuzz_NoOverflow(uint256 a, uint256 b) public {
    vm.assume(a <= type(uint128).max);
    vm.assume(b <= type(uint128).max);
    assertEq(calculator.add(a, b), a + b);
}

Next Steps