Smart Contract Security 101 — Reentrancy & Common AI‑Generated Mistakes

Published: (January 12, 2026 at 01:32 AM EST)
5 min read
Source: Dev.to

Source: Dev.to

By day 30 of this Web3 journey, “security” stopped being a scary audit word and started feeling very real.

Because the first time an “innocent” withdraw function drained an entire contract, it didn’t look like a hack at all. It looked like… a normal payment going through.

  1. The user clicked Withdraw.
  2. The contract sent them some ETH.
  3. Everything looked fine—except the payment never really ended. It kept calling back. And back. And back again.

By the time the contract “realized” what happened, its balance was gone.

That’s reentrancy—and with AI helping us write Solidity faster than ever, it’s now dangerously easy to ship this bug by accident.

What Reentrancy Really Is

Think of a contract like a vending machine:

  1. You insert a coin.
  2. The machine checks your balance.
  3. Then it updates the balance and drops your snack.

Now imagine a bug where, while the snack door is still open, you can hit the “dispense” button again and again before the machine updates your balance. You keep grabbing snacks while the machine still thinks you’ve only taken one.

That’s reentrancy in Web3 terms:

  • A smart contract sends ETH or tokens to another address/contract.
  • While that transfer is happening, the receiver runs code.
  • That code calls back into the original contract’s function before the balance or state is updated.
  • The attacker repeats this loop, draining funds.

Key idea: the contract trusts that the external call is “just a transfer.” But in Ethereum, that recipient can be a contract with its own logic.

The Classic Reentrancy Trap (And Why AI Loves It)

Here’s the vulnerable pattern AI tools often generate when you ask for a “simple withdraw function”:

// 1️⃣ Check if msg.sender has enough balance
require(balances[msg.sender] >= amount, "Insufficient balance");

// 2️⃣ Send ETH
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "Transfer failed");

// 3️⃣ Update balance
balances[msg.sender] = 0;

This looks perfectly logical in plain English, but the order of operations is deadly:

  1. External call goes out (to an attacker contract).
  2. Attacker’s fallback function runs and calls withdraw() again.
  3. The original contract still thinks the attacker has a balance, because it hasn’t set it to 0 yet.
  4. Funds keep flowing until the contract is empty.

AI models often:

  • Use call{value: …}("") without mentioning reentrancy risks.
  • Forget to use a reentrancy guard or the checks‑effects‑interactions pattern.
  • Prioritize a “working example” over a “secure example,” especially when you ask for “simple,” “minimal,” or “gas‑efficient” code.

The code compiles. The tests pass. Mainnet doesn’t forgive.

Checks‑Effects‑Interactions: Your First Shield

To fight reentrancy, flip the vending‑machine logic:

  • Instead of: “Send snack → then update balance”
  • Do: “Update balance → then send snack”

In Solidity this is called checks‑effects‑interactions:

PhaseWhat you do
ChecksValidate conditions (require statements).
EffectsUpdate internal state (balances, mappings).
InteractionsCall external contracts or send ETH last.

Why it works: When the attacker tries to re‑enter, your contract’s state already says “You have no balance.” So even if they call back in, the require fails.

Any time you see call, delegatecall, or transfer followed by a state change, your “reentrancy radar” should beep.

Reentrancy Guards & Safe Patterns

Modern Solidity provides extra safety nets you should almost always use:

  • ReentrancyGuard – a simple “locked” flag that blocks a function from being entered again before it finishes.
  • Pull‑over‑push payments – let users withdraw in a controlled, minimal function instead of sending ETH automatically.
  • Minimize external calls – the fewer external calls, the fewer reentrancy surfaces.

Think of ReentrancyGuard like putting a “Busy” sign on the vending‑machine door: while one snack is being dispensed, no one can press buttons—including the same person trying to spam the button through a backdoor.

As a beginner (and especially when using AI):

  • Default to using ReentrancyGuard on any function that sends ETH or calls untrusted contracts.
  • Only remove it when you fully understand why it’s safe without it.

Common AI‑Generated Security Mistakes (Beyond Reentrancy)

Reentrancy is just one category. AI tends to repeat a few dangerous patterns:

MistakeWhy it’s risky
Missing access control – admin‑only functions (setPrice, pause, withdrawAll) left as public.Anyone can call them.
Blind trust in msg.sender – no role checks (onlyOwner, AccessControl).No protection against unauthorized actors.
Unbounded loops over arrays – “simple” loops over large user lists that can run out of gas.Perfect for griefing or DoS.
Ignoring return values – not checking if token transfer or call actually succeeded.Silent failures that leave funds stuck.

All of these look reasonable at first glance. That’s what makes them dangerous: they fail not in the compiler, but in production.

Your job as a beginner isn’t to write “perfect” code. It’s to notice when something could go wrong, and slow down there.

Why This Matters for Developers

Reentrancy is not “just another bug.” It’s the kind of bug that:

  • Empties treasuries.
  • Breaks user trust.
  • Follows your name around on‑chain forever.

Think About This (Mini Challenge)

Next time you ask an AI tool:

“Write a simple Solidity contract that lets users deposit and withdraw ETH.”

Do this:

  1. Scan for external calls (call, transfer, send).
  2. Check the order: Does the function update state before or after sending ETH?
  3. Ask yourself: “What happens if the receiver is a contract that calls back right here?”

If you feel even a tiny bit of doubt, good—that’s your security instinct waking up.

Key Takeaway

Reentrancy isn’t magic. It’s just “I trusted external code before I locked my own house.”

As AI starts writing more of our smart‑contract code, make sure you always:

  • Apply checks‑effects‑interactions.
  • Use ReentrancyGuard (or an equivalent).
  • Review every external call carefully.

Stay safe, stay curious, and keep building responsibly.

Solidity, your edge won’t be typing faster.
It will be spotting the trapdoors AI leaves behind.

Don’t aim to be the smartest auditor in the room.
Aim to be the developer who never ships the obvious bug.

Because in Web3, one “innocent” withdraw function can be the difference between:

  • “Nice little dApp you deployed.”
  • “Remember that contract that got drained? Yeah… that was mine.”

Resources to Go Deeper

Back to Blog

Related posts

Read more »

Web3 store

Overview I wrote and deployed a demo Web3 store using Solidity and ethers.js as a learning exercise. The original tools recommended by an older book web3.js, T...