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.
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()
, orsend()
without proper guardstransfer() - 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.,
modifier)nonReentrant
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
blocks in Solidity versions prior to 0.8, leading to unchecked arithmetic:unchecked
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
blocks unless intentionally optimizing with full understanding.unchecked - 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
functions without access restrictionspublic
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
return values:transfer
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
approvalstransferFrom - 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
for fund transfers without fallback mechanismscall()
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
on unexpected token transfersrevert()
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
with allowance checkstransferFrom() - 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
for deadlines:block.timestamp
require(block.timestamp <= deadline, "Expired");
- Using
for randomness or critical logicblock.number
Practical Example
A time-locked contract with:
uint public deadline; constructor(uint _duration) { deadline = block.timestamp + _duration; }
Best Practices
- Use
only for approximate timing; avoid critical security logicblock.timestamp - 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. We’ll discuss your goals and suggest a pragmatic path forward.

