Hashtag Web3 Logo

Smart Contract Security 101

9 min
advanced

Review state changes and permissions

Public contracts can be inspected and called by untrusted parties. Review which accounts can move assets, change configuration, or invoke external code, and test those boundaries explicitly.

Include failure cases in development and review rather than relying only on successful examples.

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;
 }
}

The external call occurs before the balance update, leaving an interval in which a callback can see stale state.

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.

Continue with a test exercise

Write tests for an unauthorized call, a reverted transfer, and a callback during withdrawal. Record which properties each test checks and which assumptions remain untested.

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?