10 Common Smart Contract Vulnerabilities (With Code Examples)
A technical deep-dive into the most exploited smart contract vulnerabilities, with real code examples and prevention strategies.
Understanding common vulnerabilities is the first step to writing secure smart contracts. This guide covers the most frequently exploited issues, with code examples showing both vulnerable and secure patterns.
1. Reentrancy Attacks
Reentrancy remains one of the most dangerous vulnerabilities in smart contracts. It occurs when a contract makes an external call before updating its state, allowing the called contract to re-enter and exploit the inconsistent state.
The infamous DAO hack in 2016 exploited exactly this pattern, draining $60 million worth of ETH. The attack worked because the contract sent ETH before updating the user's balance, allowing recursive withdrawals.
Vulnerable Code:
function withdraw(uint amount) public {
require(balances[msg.sender] >= amount);
// Dangerous: external call before state update
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] -= amount;
}
Secure Code:
function withdraw(uint amount) public {
require(balances[msg.sender] >= amount);
// Update state first
balances[msg.sender] -= amount;
// Then make external call
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
The fix follows the checks-effects-interactions pattern: verify conditions, update state, then interact with external contracts. Adding a reentrancy guard modifier provides additional protection.
2. Access Control Issues
Missing or improperly implemented access controls allow unauthorized users to call privileged functions. This seems obvious in theory, but it's surprisingly common in practice, especially in complex protocols with many admin functions.
The Parity wallet freeze permanently locked $150 million because a critical function lacked proper access control. Anyone could call it and take ownership of the library contract.
Vulnerable Code:
function mint(address to, uint amount) public {
// Anyone can mint tokens!
_mint(to, amount);
}
Secure Code:
function mint(address to, uint amount) public onlyOwner {
_mint(to, amount);
}
Use established patterns like OpenZeppelin's Ownable or AccessControl contracts. Review every external and public function to ensure appropriate restrictions are in place.
3. Integer Overflow and Underflow
Before Solidity 0.8, arithmetic operations could silently overflow or underflow. While the compiler now includes built-in checks, many contracts still run on older versions or use unchecked blocks for gas optimization.
The BeautyChain exploit used integer overflow to mint billions of tokens, destroying the project's economy overnight.
Vulnerable Code (Solidity < 0.8):
function transfer(address to, uint256 amount) public {
// Can underflow if amount > balance
balances[msg.sender] -= amount;
balances[to] += amount;
}
Secure Code:
function transfer(address to, uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
}
Use Solidity 0.8+ where possible. When using unchecked blocks for gas optimization, be absolutely certain overflow is impossible in that context.
4. Flash Loan Attacks
Flash loans allow borrowing massive amounts without collateral, as long as the loan is repaid within the same transaction. Attackers use this to manipulate prices, drain liquidity, and exploit economic assumptions in protocol design.
Cream Finance lost $130 million to a flash loan attack that manipulated the price of a collateral token. The attacker borrowed, pumped the price, used the inflated collateral to borrow more, then repaid everything at a profit.
Vulnerable Pattern:
function getPrice() public view returns (uint) {
// Spot price can be manipulated with flash loans
return reserve1 / reserve0;
}
Secure Pattern:
function getPrice() public view returns (uint) {
// Use time-weighted average price (TWAP)
return oracle.consult(token, 1e18);
}
Never trust spot prices for critical operations. Use time-weighted averages, Chainlink oracles, or other manipulation-resistant price sources.
5. Oracle Manipulation
Price oracles are critical infrastructure for DeFi, but they're also a common attack vector. When a protocol relies on a single oracle or a manipulable price source, attackers can profit by moving the price in their favor.
Mango Markets lost $114 million when an attacker manipulated the MNGO token price on low-liquidity exchanges, then used the inflated price to borrow against their position.
Vulnerable Pattern:
function liquidate(address user) public {
// Single source of truth
uint price = uniswapPair.getPrice();
require(isUnderCollateralized(user, price));
// ...
}
Secure Pattern:
function liquidate(address user) public {
// Multiple sources with sanity checks
uint chainlinkPrice = chainlinkOracle.latestAnswer();
uint twapPrice = uniswapOracle.consult(token, 3600);
require(
priceDiff(chainlinkPrice, twapPrice) < MAX_DEVIATION,
"Price deviation too high"
);
// ...
}
Use multiple oracle sources and implement circuit breakers when prices deviate significantly. Consider the liquidity of the underlying markets when choosing price sources.
6. Front-Running and MEV
Transactions sit in the mempool before being mined, visible to anyone watching. Miners and bots can see pending transactions and insert their own transactions before or after to extract value. This is especially problematic for DEX trades and NFT mints.
Vulnerable Pattern:
function swap(uint amountIn, uint minAmountOut) public {
// Bots can see this and front-run
uint amountOut = calculateOutput(amountIn);
require(amountOut >= minAmountOut);
// ...
}
Mitigation Strategies:
Commit-reveal schemes hide transaction details until execution. Slippage protection limits how much value can be extracted. Private mempools and MEV protection services like Flashbots can route transactions around front-runners.
7. Denial of Service
DoS vulnerabilities can make contracts unusable, often permanently. Common patterns include unbounded loops that exceed gas limits, external calls that can always revert, and unexpected self-destructs in dependent contracts.
Vulnerable Code:
function distributeRewards() public {
// Unbounded loop - will fail with too many users
for (uint i = 0; i < users.length; i++) {
users[i].transfer(rewards[i]);
}
}
Secure Code:
function claimRewards() public {
// Pull pattern - users claim individually
uint reward = pendingRewards[msg.sender];
pendingRewards[msg.sender] = 0;
payable(msg.sender).transfer(reward);
}
Prefer pull patterns over push patterns. Let users withdraw their own funds rather than pushing funds to multiple recipients in a single transaction.
8. Improper Input Validation
Failing to validate inputs can lead to unexpected behavior. This includes missing zero-address checks, unconstrained numerical inputs, and assumptions about array lengths.
Vulnerable Code:
function setAdmin(address newAdmin) public onlyOwner {
// No validation - admin can be set to zero address
admin = newAdmin;
}
Secure Code:
function setAdmin(address newAdmin) public onlyOwner {
require(newAdmin != address(0), "Invalid address");
require(newAdmin != admin, "Already admin");
admin = newAdmin;
}
Validate all inputs, even in admin functions. Check for zero addresses, reasonable bounds on numerical values, and array length constraints.
9. Signature Replay Attacks
When contracts accept signed messages for authorization, the same signature can potentially be used multiple times or on different chains unless proper protections are in place.
Vulnerable Code:
function executeWithSignature(
address to,
uint amount,
bytes memory signature
) public {
// Can be replayed!
address signer = recoverSigner(to, amount, signature);
require(signer == owner);
// ...
}
Secure Code:
function executeWithSignature(
address to,
uint amount,
uint nonce,
bytes memory signature
) public {
require(nonces[owner] == nonce, "Invalid nonce");
nonces[owner]++;
bytes32 hash = keccak256(abi.encodePacked(
address(this), // Contract address
block.chainid, // Chain ID
to,
amount,
nonce
));
// ...
}
Include nonces, chain IDs, and contract addresses in signed messages. This prevents replays across transactions, chains, and contract deployments.
10. Logic Errors
Not all vulnerabilities come from technical exploits. Sometimes the business logic itself has flaws that allow unintended behavior. These are often the hardest bugs to find because they require understanding the protocol's intended behavior.
Compound accidentally distributed $90 million in extra COMP tokens due to a logic error in their rewards calculation. The code worked exactly as written, but the logic was wrong.
These bugs require careful specification of intended behavior, extensive testing with edge cases, and thorough manual review. Automated tools often miss logic errors because the code is technically correct.
Prevention Checklist
Writing secure smart contracts requires discipline throughout the development process. Follow the checks-effects-interactions pattern for all external calls. Use established libraries like OpenZeppelin rather than writing security-critical code from scratch. Implement comprehensive test suites including edge cases and attack scenarios. Run automated scanners to catch common issues. And get manual audits before deploying significant value.
Security is never finished. Monitor your contracts after deployment, have an incident response plan ready, and consider bug bounties to incentivize responsible disclosure.
Want to scan your contracts for these vulnerabilities? Start free or talk to our team about a comprehensive security review.