March 28, 2024

Blockchain Security: Common Vulnerabilities and Attacks

18 min readSecurity
Blockchain Security

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

Reentrancy Attack Diagram

What is it? An attacker repeatedly calls a vulnerable contract before the first invocation finishes, often draining funds.

    This happens when:
  1. A contract calls an external contract.
  2. The external contract calls back into the original contract.
  3. The original contract wasn't finished updating its state yet.
  4. 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.

  1. Check conditions
  2. Update internal state
  3. Interact with external contracts (send funds, call other contracts)

Use reentrancyGuard (OpenZeppelin)

Use transfer() or call(value: ...)("") carefully

Watch: Reentrancy | Hack Solidity

Overflow & Underflow

Arithmetic Overflow and 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 underflowing

Watch: Arithmetic Overflow and Underflow | Hack Solidity

Watch: Overflow and Underflow Errors

Forceful Ether Send (selfdestruct)

Self Destruct

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.
Forcefull Ether

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).balance for 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

Private Data Access

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

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

  1. Storage Collision
  2. 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`
  3. Code Execution Hijack
  4. If you delegatecall untrusted code, that code can:

    • Drain funds
    • Modify state
    • Permanently break logic
  5. Proxy Pattern Risks
  6. 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 delegatecall with 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!
Delegate Call

Watch: Unsafe Delegatecall | Hack Solidity (part 1 & 2)

Insecure Randomness

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 Examplesblock.timestamp, blockhash, block.difficultyChainlink VRF, commit-reveal schemes
Predictable?Yes, partially or fullyNo — cryptographically secure
Miner/User Manipulable?YesNo
Suitable for Lotteries?❌ No✅ Yes
Gas CostLowModerate
Requires Off-chain Oracle?NoYes (e.g., Chainlink node)
Tamper ResistanceWeakStrong (verifiable proof of randomness)
Best Use CasesNon-critical logic, timestampsGames, NFTs, lotteries, security-critical randomness

Watch: Insecure Source of Randomness | Hack Solidity

Denial of Service (DoS)

Denial of Service

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)
Denial of Service

Mitigation:

  1. 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:
        • mapping(address => uint256) public balances;
          
          function withdraw() external {
              uint256 amount = balances[msg.sender];
              balances[msg.sender] = 0;
              payable(msg.sender).transfer(amount);
          }
    • 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.

      Summary Table

      DoS CauseMitigation Strategy
      Unbounded LoopsAvoid dynamic arrays, use mappings
      Failing Ether TransfersUse pull payments (withdraw pattern)
      Malicious External CallsNever depend on success, handle with fallback
      High Gas UsageCap iterations, queue jobs
      ReentrancyUse 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

    Phishing Attack

    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

    Malicious Code

    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

    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.

    Front Running

    Watch: Front Running | Hack Solidity

    Timestamp Manipulation

    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

    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

    Zero Code Size

    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

    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

    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.

    Comments

    Loading comments...

    Leave a Comment