Blockchain Security: Common Vulnerabilities and Attacks

Smart contract security is critical for protecting user funds and maintaining trust in blockchain applications. This post covers the most common vulnerabilities and attack vectors in Solidity smart contracts, with practical examples, mitigation strategies, and references to the excellent Hack Solidity video series by Smart Contract Programmer.
Table of Contents
- Reentrancy
- Arithmetic Overflow & Underflow
- Forceful Ether Send (selfdestruct)
- Accessing Private Data
- Unsafe Delegatecall
- Insecure Source of Randomness
- Denial of Service
- Phishing with tx.origin
- Hiding Malicious Code
- Honeypot
- Front Running
- Block Timestamp Manipulation
- Signature Replay
- Contract With Zero Code Size
- Read Only Reentrancy
- Vault Inflation Attack
- WETH Permit
- Front Run ERC20 Approval
- 63/64 Gas Rule Attack
Reentrancy

What is it? An attacker repeatedly calls a vulnerable contract before the first invocation finishes, often draining funds.
- This happens when:
- A contract calls an external contract.
- The external contract calls back into the original contract.
- The original contract wasn't finished updating its state yet.
- The attacker exploits this to drain funds or break logic.
🧠 Simple Analogy: Imagine a vending machine: • You insert a coin. • It starts dispensing. • But before it finishes, you jam a stick in and trigger it again—getting more than you paid for.
Example: The infamous DAO hack was an Ethereum contract in 2016 that was drained of ~$60M because an attacker exploited a reentrancy bug and repeatedly withdrew funds before the balance was updated.
function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount);
(bool sent, ) = msg.sender.call{value: _amount}("");
require(sent);
balances[msg.sender] -= _amount; // <-- should update before sending
}Mitigation: Use checks-effects-interactions pattern, ReentrancyGuard, or update state before external calls.
- Check conditions
- Update internal state
- Interact with external contracts (send funds, call other contracts)
Use reentrancyGuard (OpenZeppelin)
Use transfer() or call(value: ...)("") carefully
Watch: Reentrancy | Hack Solidity
Overflow & Underflow

What is it? Integer values wrap around on overflow/underflow, leading to unexpected results.
This happens when numbers go beyond the allowed limit for their data (like uint8, uint256...):
Overflow:
- When a number goes above its maximum value and wraps around to the minimum.
- Example with uint8 (which holds values from 0 to 255):
uint8 x = 255;
x += 1; // x becomes 0 (overflow)Underflow:
This happens when a number goes below its minimum value and wraps around the maximum.
uint8 y = 0;
y = y - 1; // y becomes 255 (under)If not protected, this can allow attackers to use Trick Logic (like making balances huge by underflowing), draining tokens or ETH, manipulating counters or loops
Mitigation: Use Solidity >=0.8 (has built-in checks), or use SafeMath for older versions.
//Using SafeMath for uint256; //uint256 a = 1;
uint256 b = a.sub(2); // This will revert instead of underflowingWatch: Arithmetic Overflow and Underflow | Hack Solidity
Watch: Overflow and Underflow Errors
Forceful Ether Send (selfdestruct)

What is it? Normally, if a contract doesn't have a payable fallback/receive function, you can't send it Ether using .transfer() or .call(value:...) - it reverts.
Ether can be sent to any contract forcibly using selfdestruct, even if the contract has no payable functions.
How it works:
A contract can call: selfdestruct(payable(address));
selfdestruct(payable(targetAddress));When it does:
- The contract is destroyed.
- Any remaining Ether in its balacne is forcefully sent to the target address.
- This bypasses all checks and doesn't trigger any fallback or receive functions.

It will still receive the Ether.
Why is this important?
This technique is use in CTF challenges, exploit research, and testing scenarios.
Key Security Implications:
- Balance checks are unreliable: A contract's balance can increase without it expecting it.
require(address(this).balance == 0); // Can be false after a force send- Contracts shouldn't rely on
address(this).balancefor security logic.
Mitigation: Always check address(this).balance instead of assuming zero balance means no Ether was sent.
How to Protect Against It?
- You can't prevent forceful Ether via selfdestruct, but you can:
- Avoid using balance as a trust metric.
- Design contracts to ignore unexpected Ether.
- Add a withdrawal mechanism that requires explicit user intent.
Watch: Forcefully Send Ether with selfdestruct | Hack Solidity
Accessing Private Data

What is it? In Solidity, marking a variable as private means it cannot be accessed by other contracts or externally through Soilidty functions. However, contract data is visible on-chain, even if marked private because everything stored on the Ethereum blockchain is publicly accessible.
What Private Actually Means
- Private in Solidity limits access in code, not visibility on chain.
- Anyone can read the storage slots using tools like:
- Etherscan
- Web3.js / Viem / ethers.js
- cast storage (Foundry)
- web3.eth.getStorageAt()
Example:
// Solidity contract Secret ( uint256 private secretNumber = 42; ) // JavaScript const slot = 0; // first declared state variable const value = await provider.getStorageAt(contractAddress, slot);Mitigation: If you want truly private data, you'll need off-chain storage or zero-knolwege proofs. Never store secrets or sensitive data on-chain.
Watch: Accessing Private Data | Hack Solidity
Unsafe Delegatecall

What is it? delegatecall is a low-level function in Solidity that lets one contract run code from another contract, but using its own storage, msg.sender, and msg.value.
What Makes it Unsafe?
Because the called contract's code executes in the context of the calling contract, it can:
- be exploited if the callee is malicious or untrusted.
- Change teh storage of the calling contract
- Use msg.sender and msg.value as if it were the original caller
- Accidentally (or maliciously) corrupt the calling contract's data
Example:
Imagine giving your house keys to someone to do repairs, but they actually live in your house, use your furniture, and mess with your thermostat while pretending to be you. That's delegatecall.
⚠️ Real Risks
- Storage Collision
- Code Execution Hijack
- Drain funds
- Modify state
- Permanently break logic
- Proxy Pattern Risks
If storage layout doesn't match, variables can get overwritten.
// Caller contract address public owner; // slot 0 // Callee contract uint256 public someValue; // slot 0 // delegatecall will overwrite `owner` with `someValue`If you delegatecall untrusted code, that code can:
Many upgradeable contracts use delegatecall (e.g., proxies calling implementation logic). If not tightly controlled, a malicious implementation can brick or drain your contract.
Mitigation:
- NEVER use
delegatecallwith untrusted contracts. Use only with trusted, immutable code. - Avoid user-controlled addresses.
- Use OpenZeppelin's Transparent Proxy Pattern
- Match storage layouts exactly between proxy and implementation.
- Lock critical variables in a fixed position (e.g., EIP-1967).
- Use immutable variables carefully - they're stored in bytecode, not state.
🚫 Bad Example
contract Dangerous {
function attack() public {
selfdestruct(payable(msg.sender));
}
}
// In another contract:
(bool success, ) = address(dangerous).delegatecall(abi.encodeWithSignature("attack()"));
// Can destroy your contract by mistake!
Watch: Unsafe Delegatecall | Hack Solidity (part 1 & 2)
Insecure Randomness

What is it? Insecure randomness refers to generating 'random' values in a smart contract using predictable on-chain variales, such as:
- block.timestamp
- block.difficulty
- block.number
- blockhash
- block.coinbase (the miner's address)
These values can be influenced or predicted by miners, users, or attackers - making them unsuitable for randomness in anything that involves rewards, lotteries, games, or security.
⚠️ Why It's Dangerous
Attackers or miners can:
- Predict the outcome or random logic.
- Manipulate block variables slightly (e.g. timestamp or miner address) to win lotteries or games.
- Exploit systems based on these random values for unfair gains.
🚫 Bad Example
uint256 random = uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender))) % 100;
// Looks random... but block.timestamp is set by the miner and predictable in the short term. This is not secure.Mitigation: Do not use on-chain variables alone for randomness in contracts that involve money, trust, or fairness. They are not truly random and can be maniuplated.
Use secure randomness sources like Chainlink VRF.
Chainlink VRF (Verifiable Random Function):
A secure, verifiable way to get randomness in smart contracts.
RANDAQ + Commit-Reveal Schemes:
More complex but can be used in decentralized games and protocols.
🔍 Randomness Comparison Chart
| Feature | ❌ Insecure Randomness | ✅ Secure Randomness (e.g., Chainlink VRF) |
|---|---|---|
| Source Examples | block.timestamp, blockhash, block.difficulty | Chainlink VRF, commit-reveal schemes |
| Predictable? | Yes, partially or fully | No — cryptographically secure |
| Miner/User Manipulable? | Yes | No |
| Suitable for Lotteries? | ❌ No | ✅ Yes |
| Gas Cost | Low | Moderate |
| Requires Off-chain Oracle? | No | Yes (e.g., Chainlink node) |
| Tamper Resistance | Weak | Strong (verifiable proof of randomness) |
| Best Use Cases | Non-critical logic, timestamps | Games, NFTs, lotteries, security-critical randomness |
Watch: Insecure Source of Randomness | Hack Solidity
Denial of Service (DoS)

What is it? A Denial of Service attack occurs when a contract is manipulated so that:
- Attackers can block contract functionality (e.g., by blocking withdrawals or consuming all gas).
- It Reverts for other users.
- It gets stuck in an unstable state.
This can block access to features, lock funds, or hault contract functionality.
Types include:
- Gas Limit Attacks (e.g. unbounded loops)
- Revert-based DoS (one address causes a failure for all)
- Blocklist Locking (permanent or unremovable blocking)

Mitigation:
- Avoid unbounded loops
- Problem: Loops that grow with user input (like looping over a dynamic array) can hit the gas limit and fail.
- Solution:
- Use mapping instead of arrays when possible.
- Split logic across multiple transactions.
- Use pull over push for payments, and design for graceful failure.
- Use the "Pull" Payment Pattern
- Problem: Pushing Ether to users can fail if a receipt is a contract that reverts or uses too much gas.
- Solution:
- Let usrs claim their funds themselves.
- Example:
- Cap Iterations or Queue Work
- Problem: One transaction doing too much work can fail or block others.
- Solution:
- Add limits to loop lengths.
- Use a job queue pattern, processing a few items for call.
- Reentrency Protection
- Problem: Some DoS attacks exploit reentrancy bugs.
- Solution:
- Use OpenZeppelin's ReentrancyGuard.
- Follow checks-effects-interactions.
- Never Depend on External Contract Success
- Problem: If your logic relies on a call to another contract (which could revert), a malicious actor could block functionality.
- Solution:
- Treat external calls as optional.
- Log failures or defer handling (instead of reverting everything).
- Timeouts and Escapes
- Problem: Contracts that wait for actions (like voting or auction claims) can get stuck.
- Solution:
- Add timeouts or fallback conditions.
- Provide admin emergency functions for stuck scenarios.
mapping(address => uint256) public balances;
function withdraw() external {
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}Summary Table
| DoS Cause | Mitigation Strategy |
|---|---|
| Unbounded Loops | Avoid dynamic arrays, use mappings |
| Failing Ether Transfers | Use pull payments (withdraw pattern) |
| Malicious External Calls | Never depend on success, handle with fallback |
| High Gas Usage | Cap iterations, queue jobs |
| Reentrancy | Use ReentrancyGuard, update state before calls |
| Lock-ins (e.g., blocklists) | Use timeouts or manual escape hatches |
Watch: Denial of Service | Hack Solidity
Phishing with tx.origin

What is it? Using tx.origin for authentication can allow phishing attacks.
require(tx.origin == owner); // Vulnerable!Mitigation: Use msg.sender for authentication.
Watch: tx.origin Phishing Attacks - Solidity, Attack

Hiding Malicious Code
What is it? Attackers can hide malicious logic in complex contracts or use misleading variable/function names.
Mitigation: Conduct thorough code reviews and use automated analysis tools.
Watch: Hiding Malicious Code | Hack Solidity
Honeypot
What is it? Contracts that appear vulnerable but are designed to trap attackers.
Mitigation: Always test on testnets and review code before interacting with unknown contracts.
Watch: Honeypot | Hack Solidity

Front Running
What is it? Attackers observe pending transactions and submit their own with higher gas to exploit information.
Mitigation: Use commit-reveal schemes and design for MEV resistance.

Watch: Front Running | Hack Solidity

Block Timestamp Manipulation
What is it? Miners can manipulate block.timestamp within a small range.
Mitigation: Do not use timestamps for critical logic or randomness.
Watch: Block Timestamp Manipulation | Hack Solidity

Signature Replay
What is it? A valid signature can be reused on another chain or contract.
Mitigation: Include chain ID and contract address in signed messages.
Watch: Signature Replay | Hack Solidity

Contract With Zero Code Size
What is it? Contracts can be destroyed, leaving an address with zero code size, which can break assumptions in other contracts.
Mitigation: Always check extcodesize and handle edge cases.
Watch: Contract With Zero Code Size | Hack Solidity

Read Only Reentrancy
What is it? Reentrancy attacks that exploit view/pure functions to manipulate state indirectly.
Mitigation: Be cautious with external calls in view functions and use reentrancy guards where needed.
Watch: Read Only Reentrancy | Hack Solidity

Vault Inflation Attack
What is it? Manipulating share calculations in vaults to steal value.
Mitigation: Use time-weighted average price (TWAP) oracles and audit share logic.
Watch: Vault Inflation Attack | Hack Solidity
WETH Permit
What is it? Exploiting permit signatures to steal tokens.
Mitigation: Always verify signature validity and use nonces.
Watch: WETH Permit | Hack Solidity
Front Run ERC20 Approval
What is it? Attackers front run approval transactions to spend tokens before the intended contract.
Mitigation: Use increaseAllowance and decreaseAllowance patterns.
Watch: Front Run ERC20 Approval | Hack Solidity
63/64 Gas Rule Attack
What is it? Subtle gas forwarding rules can break contract logic, especially with transfer and call.
Mitigation: Use call with explicit gas and handle failures gracefully.
Watch: 63/64 Gas Rule Attack | Hack Solidity
Conclusion
Blockchain security is a constantly evolving field. By understanding common vulnerabilities and following best practices, you can protect your smart contracts and users. Always audit your code, use automated tools, and stay up to date with the latest research and exploits.