2025-12-24 4 min read

Smart Contract Audits: Why Known Vulnerabilities Still Slip Through

Reentrancy, integer overflow, and access control bugs dominate audit reports—despite being documented for years. Here's why developers keep making the same mistakes.

It's 2024, and we're still finding reentrancy vulnerabilities in production smart contracts. The Checks-Effects-Interactions pattern has been available since 2016. Yet every month, auditors discover the same exploitable patterns that cost millions.

This isn't a knowledge problem anymore. It's a discipline problem.

The Repeat Offenders

Three vulnerability classes account for roughly 60% of critical findings across audits we've reviewed at LavaPi:

Reentrancy Attacks

The DAO hack in 2016 was the wake-up call. We learned that external calls must complete before state changes. Yet developers continue to write code like this:

solidity
function withdraw(uint amount) public {
    require(balances[msg.sender] >= amount);
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success);
    balances[msg.sender] -= amount;
}

The vulnerability here is obvious in hindsight. The contract sends funds before updating the balance. An attacker's fallback function can call

code
withdraw
again before the state updates. The fix is equally straightforward—reverse the order:

solidity
function withdraw(uint amount) public {
    require(balances[msg.sender] >= amount);
    balances[msg.sender] -= amount;
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success);
}

Yet this pattern keeps appearing. Why? Many developers treat security as a final checklist item rather than a design principle.

Integer Overflow and Underflow

Solidity 0.8.0 added automatic overflow/underflow checks by default. Problem solved, right? Not quite.

Developers still disable these protections with

code
unchecked
blocks for gas optimization, then fail to validate the math themselves:

solidity
uint256 public totalSupply = 10**18;

function burn(uint256 amount) public {
    unchecked { totalSupply -= amount; }
}

Without validation, a user passing

code
amount > totalSupply
causes the counter to wrap around to a massive number. The protection exists—it's just actively circumvented.

Access Control Gaps

Missing or incorrectly implemented access controls remain a top five issue:

solidity
function emergencyWithdraw() public {
    payable(owner).transfer(address(this).balance);
}

This looks reasonable. The problem:

code
owner
was never initialized, or worse, it's set to
code
address(0)
. Funds vanish. Or the function uses
code
tx.origin
instead of
code
msg.sender
, breaking under proxy patterns.

Why Patterns Persist

Time Pressure and Scope Creep

Projects launch audits with incomplete code. Auditors find issues. Teams patch obvious bugs but skip architectural fixes. The underlying design flaw remains for the next audit cycle.

Copy-Paste Development

Developers grab code from GitHub repos, Stack Overflow answers, or tutorial repos written in 2018. Those examples weren't tested against current threat models. They propagate bad patterns across new projects.

Insufficient Testing

Many teams write tests for happy paths only. Fuzzing and adversarial testing—the techniques that catch these vulnerabilities—require time and expertise most teams don't allocate.

Over-Optimization

Gas optimization can't come before security. Using

code
unchecked
without proof of safety, storing sensitive data in public variables to save storage costs, or skipping validation checks creates technical debt auditors find months later.

What Actually Works

The teams shipping secure contracts do three things consistently:

  1. Design for security first. Apply known patterns (Checks-Effects-Interactions, pull over push) at the architecture stage, not as patches.

  2. Automate detection. Use static analysis tools like Slither. They catch 70% of these issues before human review.

  3. Test adversarially. Write tests assuming malicious inputs and reentrant calls. Property-based testing frameworks catch edge cases that manual testing misses.

The Bottom Line

Smart contract vulnerabilities aren't staying around because fixes are undiscovered. They persist because implementation discipline is missing. The fixes are free. They're documented. They're proven.

The gap isn't in knowledge—it's between knowing what's secure and building secure systems consistently. That requires treating security as non-negotiable, not negotiable, from day one.

Share
LP

LavaPi Team

Digital Engineering Company

All articles