Hashtag Web3 Logo

Smart Contract Security 101

9 min
advanced

The stakes are high

When you deploy a smart contract, the code is public and immutable. If there is a bug, anyone in the world can analyze the code, find the flaw, and exploit it. Because contracts hold real financial value, the incentives for hackers are massive.

Security must be the primary focus of every Solidity developer.

Top Smart Contract Vulnerabilities Reentrancy External call before state update Fix: CEI pattern Access Control Missing onlyOwner on admin functions Fix: OpenZeppelin Integer Overflow Math wraps around to max/min values Fix: Solidity 0.8+ Front-Running Bots see pending TX and trade ahead Fix: Slippage checks

1. The Reentrancy Attack

This is the most famous vulnerability in Ethereum history (it caused the 2016 DAO hack that split the Ethereum network).

The Flawed Code

Imagine a contract where users can deposit and withdraw ETH.

contract VulnerableBank {
 mapping(address => uint256) public balances;

 // 1. User requests withdrawal
 function withdraw() public {
 uint256 amount = balances[msg.sender];
 require(amount > 0, "No balance");

 // 2. Contract sends the ETH to the user
 (bool success, ) = msg.sender.call{value: amount}("");
 require(success, "Transfer failed");

 // 3. Contract updates the user's balance to 0
 balances[msg.sender] = 0;
 }
}

This looks logical, but it is fatally flawed.

When a smart contract sends ETH to another smart contract, the receiving contract's receive() function is automatically triggered. A malicious contract can receive the ETH, and in its receive() function, immediately call withdraw() again.

Because VulnerableBank hasn't reached step 3 yet, the hacker's balance is still greater than 0. The contract sends the ETH again. This loops until the bank is empty.

The Fix: Checks-Effects-Interactions

Always update your state before interacting with the outside world.

contract SafeBank {
 mapping(address => uint256) public balances;

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

 // EFFECTS (Update state FIRST)
 balances[msg.sender] = 0;

 // INTERACTIONS (Send ETH LAST)
 (bool success, ) = msg.sender.call{value: amount}("");
 require(success, "Transfer failed");
 }
}

Now, if the hacker tries to re-enter, the amount will be 0 on the second loop, and the attack fails. Developers also commonly use OpenZeppelin's nonReentrant modifier to lock functions during execution.

2. Access Control Flaws

Access control defines who is allowed to call certain functions. A classic mistake is leaving a sensitive administrative function as public without checking the caller.

// VULNERABLE
function changeOwner(address newOwner) public {
 owner = newOwner; // Anyone can call this and take over the contract!
}

// SAFE
function changeOwner(address newOwner) public {
 require(msg.sender == owner, "Not authorized");
 owner = newOwner;
}

Best Practice: Do not write custom ownership logic. Use OpenZeppelin's Ownable contract, which provides a heavily audited onlyOwner modifier.

3. Integer Overflow and Underflow

Before Solidity 0.8.0, if you subtracted 1 from a uint256 that was equal to 0, it would "underflow" and wrap around to the maximum possible number. Hackers used this to give themselves near-infinite balances.

The Fix: If you are using Solidity 0.8.0 or higher, the compiler automatically checks for math overflows and underflows and will revert the transaction if they happen. If you are reading older code, you will see the SafeMath library being used to prevent this.

4. Front-Running

Because all transactions sit in a public "mempool" before being processed, miners and bots can see what you are trying to do. If you submit a transaction to buy a token on Uniswap, a bot can see your transaction, pay a slightly higher gas fee to buy the token before you, let your purchase drive the price up, and then immediately sell the token to you for a profit. This is called MEV (Maximal Extractable Value).

The Fix: Smart contracts handling trades must include "slippage tolerance" checks—requiring the transaction to fail if the execution price is worse than the user expected.

Key takeaways

  • Use the Checks-Effects-Interactions pattern to prevent reentrancy.
  • Never trust external calls. Always assume any external contract is malicious.
  • Use heavily audited libraries (like OpenZeppelin) instead of writing your own security logic.
  • Ensure strict access control on all administrative functions.

Congratulations

You have completed the Smart Contract Development track. You now understand Solidity syntax, basic state management, token standards, and core security vulnerabilities.

Start Building: The best way to learn is to write code. Head to Remix IDE and deploy your first contract to a testnet!

Quiz: Smart Contract Security 101

1 / 5

What is a reentrancy attack?