Vulnerability types

Vulnerability Types

Understanding common smart contract vulnerabilities and how to prevent them

Smart contract vulnerabilities can lead to devastating consequences, from stolen funds to completely compromised systems. Understanding these vulnerabilities is the first step in building secure blockchain applications.

Awareius scans for over 140 different vulnerability types. Below, we cover the most critical and common vulnerabilities you'll encounter in smart contract development.

Common Vulnerability Types

Reentrancy Attacks

Critical

One of the most dangerous vulnerabilities where an external contract can call back into your contract before the first execution is complete, potentially draining funds.

Vulnerable Code

// Vulnerable code
function withdraw(uint amount) public {
    require(balances[msg.sender] >= amount);

    // External call before state update (VULNERABLE!)
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success);

    balances[msg.sender] -= amount;
}

Secure Code

// Secure code with Checks-Effects-Interactions
function withdraw(uint amount) public {
    require(balances[msg.sender] >= amount);

    // Update state BEFORE external call
    balances[msg.sender] -= amount;

    (bool success, ) = msg.sender.call{value: amount}("");
    require(success);
}

Prevention Guidelines

  • Follow the Checks-Effects-Interactions pattern
  • Update state variables before making external calls
  • Use ReentrancyGuard from OpenZeppelin
  • Implement mutex locks for critical functions

Integer Overflow/Underflow

High

Occurs when arithmetic operations exceed the maximum or minimum values that can be stored, potentially leading to unexpected behavior and fund loss.

Vulnerable Code

// Vulnerable in Solidity < 0.8.0
uint8 balance = 255;
balance += 1; // Overflows to 0

uint256 amount = 0;
amount -= 1; // Underflows to max uint256 value

Secure Code

// Use Solidity 0.8.0+ (built-in overflow checks)
pragma solidity ^0.8.0;

// Or use SafeMath library for older versions
using SafeMath for uint256;

uint256 balance = balance.add(amount);
uint256 result = value.sub(amount);

Prevention Guidelines

  • Use Solidity 0.8.0 or higher (automatic overflow checks)
  • For older versions, use OpenZeppelin's SafeMath library
  • Validate all arithmetic operations
  • Set reasonable bounds for numeric inputs

Access Control Issues

Critical

Improperly implemented access controls can allow unauthorized users to execute privileged functions, modify critical state, or drain funds.

Vulnerable Code

// Vulnerable code - missing access control
function withdrawAll() public {
    // Anyone can call this!
    payable(owner).transfer(address(this).balance);
}

Secure Code

// Secure code with proper access control
modifier onlyOwner() {
    require(msg.sender == owner, "Not authorized");
    _;
}

function withdrawAll() public onlyOwner {
    payable(owner).transfer(address(this).balance);
}

// Better: Use OpenZeppelin's Ownable
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyContract is Ownable {
    function withdrawAll() public onlyOwner {
        payable(owner()).transfer(address(this).balance);
    }
}

Prevention Guidelines

  • Use OpenZeppelin's access control contracts (Ownable, AccessControl)
  • Implement role-based access control for complex systems
  • Always validate msg.sender in privileged functions
  • Test access control with different user roles

Timestamp Dependence

Medium

Relying on block.timestamp for critical logic can be manipulated by miners within a small range, potentially affecting randomness or time-based conditions.

Vulnerable Code

// Vulnerable code
function claimReward() public {
    // Miners can manipulate timestamp by ~15 seconds
    require(block.timestamp > claimTime, "Too early");
    require(block.timestamp % 2 == 0, "Bad random");
    // ... distribute reward
}

Secure Code

// Better approach
function claimReward() public {
    // Use block numbers instead for critical timing
    require(block.number > claimBlock, "Too early");

    // Use Chainlink VRF for true randomness
    // Never use timestamp for randomness
    uint256 randomNumber = requestRandomness();
}

Prevention Guidelines

  • Avoid using block.timestamp for critical logic
  • Use block.number for time-based checks when possible
  • Never use timestamp for randomness generation
  • Use Chainlink VRF or similar oracle for random numbers

Unprotected Ether Transfer

High

Sending Ether without proper checks or using deprecated transfer/send methods can lead to stuck funds or failed transactions.

Vulnerable Code

// Vulnerable patterns
function withdraw() public {
    // transfer() has fixed 2300 gas - can fail
    payable(msg.sender).transfer(amount);

    // send() returns false on failure - must check
    payable(msg.sender).send(amount);
}

Secure Code

// Secure pattern with call()
function withdraw() public {
    uint256 amount = balances[msg.sender];
    require(amount > 0, "No balance");

    // Update state first (prevent reentrancy)
    balances[msg.sender] = 0;

    // Use call() with proper error handling
    (bool success, ) = payable(msg.sender).call{value: amount}("");
    require(success, "Transfer failed");
}

// Or use OpenZeppelin's Address.sendValue
import "@openzeppelin/contracts/utils/Address.sol";

Address.sendValue(payable(recipient), amount);

Prevention Guidelines

  • Use call() instead of transfer() or send()
  • Always check return values and revert on failure
  • Update state before external calls (prevent reentrancy)
  • Consider using pull payment pattern with OpenZeppelin

Visibility Issues

Medium

Incorrectly set function visibility can expose internal functions or fail to restrict access, leading to unauthorized execution.

Vulnerable Code

// Vulnerable code
// No visibility specified - defaults to public
function _internalCalculation(uint a, uint b) returns (uint) {
    return a * b + secretValue;
}

// State variable exposed
address owner; // public by default

Secure Code

// Proper visibility
// Internal functions should be marked internal
function _internalCalculation(uint a, uint b) internal pure returns (uint) {
    return a * b;
}

// Explicit visibility for state variables
address private owner;
uint256 public totalSupply;

// External is cheaper than public for functions only called externally
function externalFunction() external view returns (uint) {
    return someValue;
}

Prevention Guidelines

  • Always explicitly specify visibility (public, external, internal, private)
  • Use external for functions only called from outside
  • Mark internal helpers as internal or private
  • Review all function visibility before deployment

Denial of Service (DoS)

High

Attackers can block contract functionality through various means, including gas exhaustion, failed external calls, or state manipulation.

Vulnerable Code

// Vulnerable to DoS
function distributeRewards() public {
    // Unbounded loop - attacker can make array huge
    for(uint i = 0; i < users.length; i++) {
        users[i].transfer(reward);
    }
}

// DoS via revert
function refund() public {
    for(uint i = 0; i < investors.length; i++) {
        // If one transfer fails, all fail
        investors[i].transfer(amounts[i]);
    }
}

Secure Code

// Use pull payment pattern
mapping(address => uint256) public withdrawable;

function claimReward() public {
    uint256 amount = withdrawable[msg.sender];
    require(amount > 0, "Nothing to claim");

    withdrawable[msg.sender] = 0;
    payable(msg.sender).transfer(amount);
}

// Limit iterations or use pagination
uint256 constant MAX_BATCH = 100;

function distributeBatch(uint256 start, uint256 end) public {
    require(end - start <= MAX_BATCH, "Batch too large");
    for(uint i = start; i < end; i++) {
        _distribute(users[i]);
    }
}

Prevention Guidelines

  • Use pull payment pattern instead of push payments
  • Limit loop iterations or implement pagination
  • Avoid external calls in loops
  • Set gas limits for operations

Logic Errors

Medium

Flawed business logic, incorrect calculations, or unhandled edge cases can lead to unexpected behavior and potential exploits.

Vulnerable Code

// Vulnerable logic
function vote(uint proposalId) public {
    // Missing check - users can vote multiple times
    votes[proposalId] += 1;
}

// Off-by-one error
function isEligible(uint age) public pure returns (bool) {
    return age > 18; // Should be >= 18
}

Secure Code

// Fixed logic
mapping(uint => mapping(address => bool)) public hasVoted;

function vote(uint proposalId) public {
    require(!hasVoted[proposalId][msg.sender], "Already voted");
    hasVoted[proposalId][msg.sender] = true;
    votes[proposalId] += 1;
}

function isEligible(uint age) public pure returns (bool) {
    return age >= 18; // Correct logic
}

Prevention Guidelines

  • Write comprehensive unit tests covering edge cases
  • Document assumptions and invariants clearly
  • Use formal verification for critical logic
  • Conduct thorough peer code reviews

How Awareius Detects Vulnerabilities

Our AI-powered scanner uses multiple analysis techniques to identify vulnerabilities in your smart contracts:

Static Analysis

Examines your code structure and patterns without executing it, identifying potential vulnerabilities through pattern matching and data flow analysis.

Symbolic Execution

Explores different execution paths to find edge cases and potential exploit scenarios that might not be obvious from manual review.

AI Pattern Recognition

Uses machine learning trained on thousands of audited contracts to identify subtle vulnerabilities and antipatterns.

Best Practice Checks

Compares your code against industry standards and recommendations from security frameworks like OpenZeppelin.

Continue Learning