Common Vulnerability Patterns
Understand and prevent the most frequent smart contract vulnerabilities. This guide covers vulnerability patterns frequently detected by BlockSecOps, along...
Common Vulnerability Patterns
Understand and prevent the most frequent smart contract vulnerabilities.
Overview
This guide covers vulnerability patterns frequently detected by BlockSecOps, along with examples and fixes.
Critical Severity
Reentrancy
What it is: External calls that allow callback into the contract before state updates complete.
Vulnerable Code:
function withdraw() public {
uint256 amount = balances[msg.sender];
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] = 0; // State updated after call
}
Fixed Code:
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract Vault is ReentrancyGuard {
function withdraw() public nonReentrant {
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0; // State updated before call
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
}
Prevention:
- Use ReentrancyGuard
- Follow Checks-Effects-Interactions pattern
- Update state before external calls
Access Control Missing
What it is: Functions that modify state without proper authorization checks.
Vulnerable Code:
function setAdmin(address newAdmin) public {
admin = newAdmin; // Anyone can call
}
Fixed Code:
function setAdmin(address newAdmin) public onlyOwner {
require(newAdmin != address(0), "Invalid address");
admin = newAdmin;
}
Prevention:
- Use
onlyOwneror role-based access - Validate new values
- Consider timelocks for sensitive changes
Arbitrary External Call
What it is: User-controlled targets for external calls.
Vulnerable Code:
function execute(address target, bytes calldata data) public {
target.call(data); // Attacker controls target
}
Fixed Code:
mapping(address => bool) public allowedTargets;
function execute(address target, bytes calldata data) public onlyOwner {
require(allowedTargets[target], "Target not allowed");
(bool success, ) = target.call(data);
require(success, "Call failed");
}
Prevention:
- Whitelist allowed targets
- Validate call data
- Restrict to admin functions
High Severity
Integer Overflow/Underflow
What it is: Arithmetic that wraps around min/max values.
Vulnerable Code (pre-0.8.0):
pragma solidity ^0.7.0;
function transfer(address to, uint256 amount) public {
balances[msg.sender] -= amount; // Can underflow
balances[to] += amount; // Can overflow
}
Fixed Code:
pragma solidity ^0.8.0; // Built-in overflow protection
function transfer(address to, uint256 amount) public {
balances[msg.sender] -= amount; // Reverts on underflow
balances[to] += amount; // Reverts on overflow
}
Prevention:
- Use Solidity 0.8.0+
- Use SafeMath for older versions
- Validate inputs
Unchecked Return Value
What it is: Ignoring return values from external calls.
Vulnerable Code:
function transferTokens(address token, address to, uint256 amount) public {
IERC20(token).transfer(to, amount); // Return value ignored
}
Fixed Code:
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
function transferTokens(address token, address to, uint256 amount) public {
SafeERC20.safeTransfer(IERC20(token), to, amount);
}
Prevention:
- Use SafeERC20 for token transfers
- Check return values with require
- Use try/catch for external calls
Delegate Call to Untrusted Contract
What it is: Using delegatecall with user-controlled addresses.
Vulnerable Code:
function upgrade(address newImpl) public {
(bool success, ) = newImpl.delegatecall(msg.data);
}
Fixed Code:
address public implementation;
function upgrade(address newImpl) public onlyOwner {
require(newImpl != address(0));
implementation = newImpl;
}
function _delegate(address impl) internal {
(bool success, ) = impl.delegatecall(msg.data);
require(success);
}
Prevention:
- Use established upgrade patterns (UUPS, Transparent Proxy)
- Restrict who can set implementation
- Validate new implementation
Medium Severity
Front-Running Vulnerability
What it is: Transactions that can be observed and exploited by miners/bots.
Vulnerable Code:
function buyToken(uint256 maxPrice) public payable {
uint256 price = getPrice();
require(price <= maxPrice);
// Attacker sees this and front-runs with price manipulation
}
Fixed Code:
function buyToken(uint256 maxPrice, uint256 deadline) public payable {
require(block.timestamp <= deadline, "Expired");
uint256 price = getPrice();
require(price <= maxPrice, "Price too high");
// Use commit-reveal for sensitive operations
}
Prevention:
- Use commit-reveal schemes
- Add deadline parameters
- Use slippage protection
Timestamp Dependence
What it is: Relying on block.timestamp for critical logic.
Vulnerable Code:
function roll() public returns (uint256) {
return uint256(keccak256(abi.encode(block.timestamp))) % 6;
}
Fixed Code:
// Use Chainlink VRF for randomness
function roll() public returns (uint256 requestId) {
requestId = vrfCoordinator.requestRandomWords(...);
}
Prevention:
- Use Chainlink VRF for randomness
- Allow timestamp variance in time-based logic
- Don't use timestamp as sole entropy source
Missing Input Validation
What it is: Not validating function parameters.
Vulnerable Code:
function setFee(uint256 newFee) public onlyOwner {
fee = newFee; // Could be set to 100% or higher
}
Fixed Code:
uint256 public constant MAX_FEE = 1000; // 10%
uint256 public constant FEE_DENOMINATOR = 10000;
function setFee(uint256 newFee) public onlyOwner {
require(newFee <= MAX_FEE, "Fee too high");
fee = newFee;
}
Prevention:
- Validate all inputs
- Use bounds checking
- Define sensible limits
Low Severity
Floating Pragma
What it is: Using ^ in pragma allowing multiple compiler versions.
Issue:
pragma solidity ^0.8.0; // Could be 0.8.0, 0.8.5, 0.8.20...
Fixed:
pragma solidity 0.8.20; // Exact version
Why it matters:
- Reproducible builds
- Consistent behavior
- Known security properties
Missing Events
What it is: State changes without event emission.
Issue:
function setOwner(address newOwner) public onlyOwner {
owner = newOwner; // No event
}
Fixed:
event OwnershipTransferred(address indexed previous, address indexed current);
function setOwner(address newOwner) public onlyOwner {
emit OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
Why it matters:
- Off-chain monitoring
- Audit trail
- User transparency
Unused Variables
What it is: Declared variables that are never used.
Issue:
function process() public {
uint256 temp = 0; // Never used
// ...processing without temp
}
Fixed:
function process() public {
// Remove unused variable
// ...processing
}
Why it matters:
- Gas waste
- Code clarity
- Potential logic errors
Vulnerability Categories
By Impact
| Severity | Examples |
|---|---|
| Critical | Reentrancy, Access Control, Arbitrary Call |
| High | Overflow, Unchecked Return, Delegate Call |
| Medium | Front-running, Timestamp, Input Validation |
| Low | Pragma, Events, Unused Variables |
By Type
| Type | Examples |
|---|---|
| Access Control | Missing checks, Privilege escalation |
| Arithmetic | Overflow, Division by zero, Precision loss |
| External Calls | Reentrancy, Unchecked returns |
| Data Validation | Missing validation, Type confusion |
Next Steps
- Remediation Priorities - How to prioritize fixes
- Solidity Security Tips - Language-specific guidance
- Severity Levels - Understanding severity