Solidity is fun… until your smart contract gets wrecked, drained, or exploited and it’s your fault. 😭
I’ve made my fair share of mistakes while building smart contracts, NFT platforms, and DeFi tools on Ethereum. Today, I’m sharing the security blunders I’ve made (so you don’t have to), the "why" behind them, and what to do instead.
** Mistake 1: Trusting msg.sender
Without Context**
function withdraw() public {
require(msg.sender == owner, "Not owner");
payable(owner).transfer(address(this).balance);
}
❌ What Went Wrong:
I thought only the owner could call this. But if this contract is called from another contract, msg.sender
might not be what I expect. Boom!!!....reentrancy or malicious forwarding.
✅ Fix It:
Use tx.origin
with care and add checks for contract calls when needed. Even better: use OpenZeppelin’s Ownable
and reentrancy guards.
Mistake 2: No Reentrancy Protection
function claimReward() public {
uint amount = rewards[msg.sender];
require(amount > 0, "No reward");
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "Failed");
rewards[msg.sender] = 0;
}
Classic. This function is re-entrancy prone.
Why It’s Bad:
Before zeroing out the user’s reward, it sends ETH giving malicious contracts a way to call claimReward()
again before the balance is reset. Result: they drain the contract.
✅ Fix It:
Use the checks-effects-interactions pattern or import ReentrancyGuard.
function claimReward() public nonReentrant {
uint amount = rewards[msg.sender];
require(amount > 0, "No reward");
rewards[msg.sender] = 0;
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "Failed");
}
🛡️ Add this modifier:
nonReentrant
from OpenZeppelin.
Mistake 3: Not Using immutable
or constant
I once stored variables like this:
address public owner;
uint256 public fee = 0.01 ether;
Big mistake.
Why It’s Bad:
Storage reads are expensive, especially for values that don’t change.
Use **immutable or constant:**
address public immutable owner;
uint256 public constant FEE = 0.01 ether;
constructor() {
owner = msg.sender;
}
Saves gas. Makes your contract cleaner.
Mistake 4: Poor Access Control
Ever accidentally leave a function open?
function setAdmin(address _new) public {
admin = _new;
}
What I Forgot:
Only authorized users should be able to make changes.
Use modifiers:
modifier onlyOwner() {
require(msg.sender == owner, "Not the owner");
_;
}
function setAdmin(address _new) public onlyOwner {
admin = _new;
}
Or better yet: use OpenZeppelin’s AccessControl for roles.
Mistake 5: Writing Gas Refund Loops Without Limits
function deleteAll() public {
for (uint i = 0; i < users.length; i++) {
delete users[i];
}
}
What’s the danger?
Unbounded loops can hit the block gas limit, causing the transaction to fail completely.
Fix It:
- Let users remove their own data.
- Use
mapping
instead of arrays. - Or do the work in batches across multiple transactions.
Solidity security is like locking your house in a tough neighborhood. P.S: it’s not optional.
Every little mistake can cost you real ETH, reputation, and peace of mind.
Start building secure habits early:
- Think like a hacker
- Read audits & exploit case studies
- Use OpenZeppelin, Slither, Foundry fuzzing
- Keep your contracts simple
💬 Your Turn!
Have you made any of these mistakes?
Got a juicy horror story from a smart contract gone wrong?
Drop it in the comments we learn faster together.👇
Until next time
STAY TUNED!!!
Great reminders here—reentrancy and access control bugs really are painful lessons. Thanks for sharing your experience!