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.
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.
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;
}
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);
}
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.
// 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
}
function setPrice(uint newPrice) external onlyOwner {
price = newPrice;
}
// Protected initializer
function initialize(address _owner) external initializer {
__Ownable_init(_owner);
}
initializer modifier (proxy patterns)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.
// 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);
}
// 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);
}
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.
// 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);
}
// 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);
}
transfer() or send() (use call with return check)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.
// 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!
}
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;
}
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.
// 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!
}
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);
}
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.
// 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
}
// 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);
}
Testing is not optional. Every vulnerability category above should have explicit test cases that verify the protection works.
Before deploying to mainnet, run through this condensed 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.