Smart Contract Vulnerabilities
Learn about common security vulnerabilities in smart contracts and how to prevent them. Essential knowledge for every blockchain developer.
Reentrancy
Occurs when external contract calls are made before state changes, allowing attackers to re-enter the function.
Code Example
// Vulnerable
function withdraw() external {
uint256 balance = balances[msg.sender];
(bool success, ) = msg.sender.call{value: balance}("");
require(success);
balances[msg.sender] = 0; // State change after external call!
}
// Fixed
function withdraw() external {
uint256 balance = balances[msg.sender];
balances[msg.sender] = 0; // State change before external call
(bool success, ) = msg.sender.call{value: balance}("");
require(success);
}Prevention
- Use Checks-Effects-Interactions pattern
- Use ReentrancyGuard from OpenZeppelin
- Avoid external calls when possible
Integer Overflow/Underflow
Arithmetic operations that exceed the maximum or minimum value for the integer type, causing unexpected behavior.
Code Example
// In Solidity < 0.8.0, this would wrap around
uint8 x = 255;
x = x + 1; // x becomes 0 (overflow)
uint8 y = 0;
y = y - 1; // y becomes 255 (underflow)
// Solidity 0.8+ automatically checks for overflow/underflow
// Use unchecked { } block if you need the old behaviorPrevention
- Use Solidity 0.8+ (built-in overflow checks)
- Use SafeMath library for older versions
- Validate input ranges
Access Control
Missing or incorrect access control allows unauthorized users to execute privileged functions.
Code Example
// Vulnerable - anyone can call
function setOwner(address newOwner) public {
owner = newOwner;
}
// Fixed - only owner can call
function setOwner(address newOwner) public onlyOwner {
owner = newOwner;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}Prevention
- Use OpenZeppelin AccessControl or Ownable
- Implement role-based access control
- Always verify caller permissions
Flash Loan Attacks
Attackers use flash loans to manipulate prices, governance votes, or exploit vulnerable protocols within a single transaction.
Code Example
// Vulnerable price oracle
function getPrice() public view returns (uint256) {
// Using spot price from DEX - easily manipulated
return dex.getReserve0() / dex.getReserve1();
}
// Better approach - use time-weighted average
function getPrice() public view returns (uint256) {
return oracle.consult(token, 1e18); // TWAP oracle
}Prevention
- Use time-weighted average prices (TWAP)
- Use Chainlink oracles
- Implement flash loan guards
- Add time delays for sensitive operations
Oracle Manipulation
Manipulating price oracles to exploit DeFi protocols that rely on them for pricing.
Code Example
// Vulnerable - single source oracle
uint256 price = uniswapPair.getReserves();
// Better - use Chainlink
AggregatorV3Interface priceFeed = AggregatorV3Interface(chainlinkAddress);
(, int256 price,,,) = priceFeed.latestRoundData();
require(price > 0, "Invalid price");Prevention
- Use decentralized oracles (Chainlink)
- Implement price deviation checks
- Use multiple oracle sources
- Add circuit breakers
Front-Running / MEV
Attackers observe pending transactions and submit their own with higher gas to execute first.
Code Example
// Vulnerable - predictable transaction
function buyTokens(uint256 amount) external {
uint256 price = getCurrentPrice();
token.transferFrom(msg.sender, address(this), price * amount);
}
// Mitigation - commit-reveal scheme
function commitBuy(bytes32 hash) external {
commits[msg.sender] = Commit(hash, block.number);
}
function revealBuy(uint256 amount, bytes32 salt) external {
require(keccak256(abi.encode(amount, salt)) == commits[msg.sender].hash);
// Process buy
}Prevention
- Use commit-reveal schemes
- Implement slippage protection
- Use private mempools (Flashbots)
- Batch transactions
Signature Replay
Reusing valid signatures across different contexts, chains, or after state changes.
Code Example
// Vulnerable - no nonce or chain ID
function execute(bytes memory signature, address to, uint256 value) {
bytes32 hash = keccak256(abi.encode(to, value));
require(recoverSigner(hash, signature) == owner);
// Execute...
}
// Fixed - include nonce and chain ID
function execute(bytes memory signature, address to, uint256 value, uint256 nonce) {
require(nonce == nonces[msg.sender]++);
bytes32 hash = keccak256(abi.encode(to, value, nonce, block.chainid));
require(recoverSigner(hash, signature) == owner);
}Prevention
- Include nonce in signed messages
- Include chain ID (EIP-155)
- Use EIP-712 typed signatures
- Invalidate signatures after use
Unsafe Delegatecall
Using delegatecall with untrusted contracts can allow attackers to manipulate contract storage.
Code Example
// Vulnerable - delegatecall to user-controlled address
function execute(address target, bytes memory data) external {
target.delegatecall(data); // Dangerous!
}
// The called contract runs in the context of this contract
// and can overwrite storage, including owner variablePrevention
- Never delegatecall to untrusted addresses
- Use well-audited proxy patterns
- Validate target contracts
- Understand storage layout implications