7Block Labs
audit

ByAUJay

Audit Findings We See Most in Solidity Code

Description:
Explore the most common Solidity code vulnerabilities uncovered during audits, backed by real-world examples, best practices, and precise recommendations to enhance your blockchain security posture.


Introduction

As blockchain adoption accelerates across startups and enterprises, ensuring the security of Solidity smart contracts remains paramount. Despite evolving best practices, certain vulnerabilities persist, often stemming from overlooked coding patterns, complex logic, or outdated practices. This article distills the most frequent audit findings we've encountered, providing concrete insights, practical examples, and actionable recommendations to elevate your smart contract security.


1. Reentrancy Attacks: The Persistent Threat

Overview

Reentrancy remains one of the most infamous vulnerabilities, exemplified by the 2016 DAO attack. It occurs when a contract calls an external contract before updating its state, allowing malicious contracts to recursively call back and drain funds.

Common Causes

  • External calls made before state updates
  • Use of
    call()
    ,
    send()
    , or
    transfer()
    without proper guards
  • Lack of reentrancy guards

Typical Findings

  • Contracts with functions like:
function withdraw(uint amount) external {
    require(balances[msg.sender] >= amount);
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success);
    balances[msg.sender] -= amount;
}
  • Absence of reentrancy lock (e.g.,
    nonReentrant
    modifier)

Best Practices & Mitigation

  • Use the Checks-Effects-Interactions pattern:
function withdraw(uint amount) external {
    require(balances[msg.sender] >= amount);
    balances[msg.sender] -= amount;
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success);
}
  • Implement reentrancy guards using OpenZeppelin's
    ReentrancyGuard
    :
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

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

Key Takeaway

Always perform external calls after updating state variables and leverage existing security libraries.


2. Integer Overflow & Underflow: Hidden Dangers

Overview

Although Solidity 0.8+ has built-in overflow checks, older versions or manual unchecked blocks can introduce overflow vulnerabilities.

Common Findings

  • Use of
    unchecked
    blocks in Solidity versions prior to 0.8, leading to unchecked arithmetic:
unchecked {
    balances[msg.sender] += amount; // Potential overflow
}
  • Reliance on third-party libraries without proper versioning

Practical Example

A token contract with:

function transfer(address recipient, uint256 amount) public {
    balances[msg.sender] -= amount; // Underflow if `amount > balances[msg.sender]`
    balances[recipient] += amount;
}

Best Practices

  • Use Solidity 0.8+ which includes overflow checks by default.
  • Avoid
    unchecked
    blocks unless intentionally optimizing with full understanding.
  • For older versions, incorporate SafeMath library:
using SafeMath for uint256;
balances[msg.sender] = balances[msg.sender].sub(amount);

Key Takeaway

Always validate arithmetic operations; leverage compiler features or well-audited libraries.


3. Inadequate Access Control & Ownership Checks

Overview

Ownership and role-based access control are critical to prevent unauthorized contract interactions.

Common Findings

  • Missing or weak owner checks:
function emergencyWithdraw() external {
    require(msg.sender == owner);
    payable(owner).transfer(address(this).balance);
}
  • Use of
    public
    functions without access restrictions

Practical Example

A contract allows anyone to execute sensitive functions:

function setParameter(uint newParam) public {
    parameter = newParam; // No access control
}

Best Practices & Solutions

  • Implement robust access control via OpenZeppelin's
    Ownable
    :
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyContract is Ownable {
    function setParameter(uint newParam) external onlyOwner {
        parameter = newParam;
    }
}
  • For multi-role management, utilize
    AccessControl
    :
import "@openzeppelin/contracts/access/AccessControl.sol";

contract RoleBased is AccessControl {
    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
    constructor() {
        _setupRole(ADMIN_ROLE, msg.sender);
    }

    function adminFunction() external onlyRole(ADMIN_ROLE) {
        // ...
    }
}

Key Takeaway

Implement explicit, tested access control mechanisms for all privileged functions.


4. Improper Handling of Token Transfers & ERC Standards

Overview

Non-compliance or incorrect implementation of ERC-20/ERC-721 standards often leads to vulnerabilities and interoperability issues.

Common Findings

  • Incorrect
    transfer
    return values:
function transfer(address recipient, uint256 amount) public returns (bool) {
    require(balances[msg.sender] >= amount);
    balances[msg.sender] -= amount;
    balances[recipient] += amount;
    emit Transfer(msg.sender, recipient, amount);
    return true;
}
  • Missing checks for
    transferFrom
    approvals
  • Inconsistent event emissions

Practical Example

A custom token that does not return a boolean, violating ERC-20:

function transfer(address recipient, uint256 amount) public {
    require(balances[msg.sender] >= amount);
    balances[msg.sender] -= amount;
    balances[recipient] += amount;
    emit Transfer(msg.sender, recipient, amount);
}

Best Practices

  • Use OpenZeppelin's ERC implementations to ensure compliance:
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyToken is ERC20 {
    constructor() ERC20("MyToken", "MTK") {
        _mint(msg.sender, 1_000_000 * 10 ** decimals());
    }
}
  • Always verify return values and event emissions to match standards

Key Takeaway

Stick to audited, standard implementations, and rigorously test compliance with ERC standards.


5. Gas Optimization & Denial of Service (DoS) Risks

Overview

Gas inefficiencies and potential DoS vectors can cripple contract functionality.

Common Findings

  • Unbounded loops over dynamic arrays:
function distributeRewards() external {
    for (uint i = 0; i < users.length; i++) {
        // distribute rewards
    }
}
  • Excessive storage reads/writes
  • Using
    call()
    for fund transfers without fallback mechanisms

Practical Example

A contract that blocks operations if array size exceeds a threshold:

function batchProcess() external {
    require(users.length <= 1000, "Too many users");
    for (uint i=0; i<users.length; i++) {
        // process
    }
}

Best Practices

  • Use event-driven or batch processing with limits
  • Optimize storage access patterns
  • Incorporate circuit breakers and time delays

Key Takeaway

Design with scalability and gas efficiency in mind, and implement safeguards against DoS attacks.


6. Handling of Ether & Token Receipts: Unsafe Patterns

Overview

Contracts often mishandle incoming Ether or tokens, leading to lost funds or vulnerabilities.

Common Findings

  • Relying solely on fallback or receive functions without proper validation:
receive() external payable {
    // no validation or event logging
}
  • Missing
    revert()
    on unexpected token transfers

Practical Example

A contract that accepts Ether but does not log or validate:

fallback() external payable {}

Best Practices

  • Use explicit functions for receiving Ether:
function deposit() external payable {
    emit Deposited(msg.sender, msg.value);
}
  • Validate token transfers via
    IERC20
    transferFrom()
    with allowance checks
  • Avoid accepting Ether in fallback functions unless necessary

Key Takeaway

Explicit, validated deposit mechanisms prevent accidental fund loss and improve auditability.


7. Time & Block Number Manipulation

Overview

Contracts relying on block timestamps or block numbers for critical logic are vulnerable to miner manipulation.

Common Findings

  • Using
    block.timestamp
    for deadlines:
require(block.timestamp <= deadline, "Expired");
  • Using
    block.number
    for randomness or critical logic

Practical Example

A time-locked contract with:

uint public deadline;

constructor(uint _duration) {
    deadline = block.timestamp + _duration;
}

Best Practices

  • Use
    block.timestamp
    only for approximate timing; avoid critical security logic
  • For randomness, prefer VRF solutions (e.g., Chainlink VRF)
  • Consider oracle-based timestamps for high-security deadlines

Key Takeaway

Minimize reliance on miner-manipulable values in security-sensitive logic.


8. Insufficient Testing & Formal Verification Gaps

Overview

Many vulnerabilities stem from inadequate testing, especially in complex logic.

Findings

  • Missing edge case tests
  • Lack of formal verification for critical functions
  • Overlooking reentrancy, overflow, or access control in test cases

Practical Approach

  • Implement comprehensive unit and integration tests covering:

    • Boundary conditions
    • Attack vectors
    • State transitions
  • Use formal verification tools like MythX, Certora, or Solidity's SMT solvers for critical logic

Best Practices

  • Adopt Test-Driven Development (TDD)
  • Regularly perform security audits and static analysis
  • Use tools like Slither and MythX as part of CI/CD pipelines

Key Takeaway

Rigorous testing and formal verification drastically reduce the likelihood of overlooked vulnerabilities.


Conclusion

Security audits consistently reveal certain patterns of vulnerabilities in Solidity code, often rooted in common pitfalls, outdated practices, or overlooked edge cases. By understanding these frequent issues — from reentrancy and integer overflow to access control lapses and gas inefficiencies — and applying best practices, your smart contracts can achieve a higher security standard.

Key takeaways:

  • Always update to Solidity 0.8+ and leverage its built-in safety checks.
  • Use battle-tested libraries like OpenZeppelin for standardized implementations.
  • Enforce strict access control and validate all external inputs.
  • Optimize for gas efficiency and be mindful of DoS vectors.
  • Incorporate comprehensive testing and consider formal verification for mission-critical functions.

Final advice:
Regular, thorough audits combined with adherence to best practices are essential to deploying secure, reliable blockchain solutions that foster trust and uphold your company's reputation in the evolving Web3 landscape.


For tailored, comprehensive smart contract security assessments, contact 7Block Labs — your trusted partner in blockchain development and auditing.

Like what you're reading? Let's build together.

Get a free 30‑minute consultation with our engineering team.

7BlockLabs

Full-stack blockchain product studio: DeFi, dApps, audits, integrations.

7Block Labs is a trading name of JAYANTH TECHNOLOGIES LIMITED.

Registered in England and Wales (Company No. 16589283).

Registered Office address: Office 13536, 182-184 High Street North, East Ham, London, E6 2JA.

© 2025 7BlockLabs. All rights reserved.