🛡️ Contract Security: The Castle Defense Guide
Imagine you’re building a magical castle that holds treasure. Bad guys want to steal it. Let’s learn how to protect your castle!
🎯 The Big Picture
Smart contracts are like treasure vaults that work automatically. Once you set the rules, they follow them forever. But here’s the scary part: bad guys know the rules too!
If there’s even a tiny crack in your vault, thieves will find it. That’s why we need to learn about security — the art of building unbreakable vaults.
🔄 Reentrancy Attacks
The Sneaky Thief Trick
Imagine a candy machine. You put in $1, it gives you candy. Simple, right?
But what if a sneaky kid figured out that the machine gives candy BEFORE it remembers you already took some? They could:
- Put in $1
- Get candy
- Quickly reach in again before the machine updates
- Get MORE candy with the same $1!
That’s a reentrancy attack!
How It Works in Code
// BAD: Vulnerable to reentrancy!
function withdraw() public {
uint balance = balances[msg.sender];
// Sends money FIRST
(bool success,) = msg.sender.call{value: balance}("");
// Updates balance AFTER (too late!)
balances[msg.sender] = 0;
}
The thief’s contract can call back into withdraw() before the balance becomes zero!
Real Example: The DAO Hack
In 2016, a thief stole $60 million using this exact trick. It was so bad that Ethereum itself had to be changed to undo the damage!
🔒 Reentrancy Guards
The Bouncer Solution
What if your candy machine had a bouncer? Before giving candy, the bouncer checks: “Is someone already taking candy?” If yes, they say “WAIT YOUR TURN!”
// GOOD: Protected with a guard!
bool private locked;
modifier noReentrancy() {
require(!locked, "Wait your turn!");
locked = true; // Lock the door
_;
locked = false; // Unlock after
}
function withdraw() public noReentrancy {
uint balance = balances[msg.sender];
balances[msg.sender] = 0; // Update FIRST!
(bool success,) = msg.sender.call{value: balance}("");
}
The CEI Pattern
Checks → Effects → Interactions
Think of it like ordering pizza:
- Check: Do you have enough money?
- Effect: Mark the order as paid
- Interact: Only THEN does the pizza get delivered
Always update your records BEFORE sending money out!
🧮 Arithmetic Safety
The Overflow Problem
Imagine a car odometer that only shows 3 digits (000-999). What happens when it goes from 999 to 1000?
It wraps around to 000! 🤯
Old smart contracts had this problem with numbers:
// Old Solidity (before 0.8)
uint8 number = 255;
number = number + 1; // Becomes 0! Not 256!
The Fix
Solidity 0.8+ automatically stops this! The transaction just fails instead of giving wrong numbers.
// Modern Solidity (0.8+)
uint8 number = 255;
number = number + 1; // FAILS! Transaction reverts
For older code, use SafeMath:
using SafeMath for uint256;
result = a.add(b); // Safe addition
💰 Ether Transfer Methods
Three Ways to Send Money
Think of sending money like delivering a letter. You have three options:
| Method | Gas Limit | Returns | Best For |
|---|---|---|---|
transfer |
2300 | Throws error | Simple sends |
send |
2300 | true/false | Check success |
call |
All gas | true/false | Recommended |
The Modern Way
// RECOMMENDED approach
(bool success,) = recipient.call{value: amount}("");
require(success, "Transfer failed");
Why call? Because transfer and send can break when gas costs change!
Quick Summary
graph TD A["Send Ether"] --> B{Which Method?} B --> C["transfer: Reverts on fail"] B --> D["send: Returns bool"] B --> E["call: Most flexible ✅"] E --> F["Check return value!"]
👤 tx.origin vs msg.sender
The Puppet Attack
Imagine Alice asks Bob to deliver a letter to Charlie.
- msg.sender = Bob (the direct sender)
- tx.origin = Alice (the original person)
If Charlie only checks tx.origin, a trick can happen!
// DANGEROUS: Don't do this!
function withdraw() public {
require(tx.origin == owner); // BAD!
// Attacker's contract can call this
// while the real owner is doing something else!
}
The Safe Way
// SAFE: Always use msg.sender
function withdraw() public {
require(msg.sender == owner); // GOOD!
// Only direct calls from owner work
}
Remember This Rule
tx.origin = The human who started everything
msg.sender = The contract/address that called you directly
Always prefer msg.sender!
🚪 Access Control Patterns
Who Can Open Which Door?
Your smart contract is like a building with different rooms. Some doors should be open to everyone. Others? Only the boss can enter!
The Simple Lock: onlyOwner
address public owner;
modifier onlyOwner() {
require(msg.sender == owner, "Not the boss!");
_;
}
function changeSettings() public onlyOwner {
// Only owner can do this
}
Be Careful!
What if the owner loses their keys? Add a way to transfer ownership:
function transferOwnership(address newOwner)
public onlyOwner
{
require(newOwner != address(0));
owner = newOwner;
}
🎭 Role-Based Access Control
The Team Approach
What if your castle needs more than just a king? You might need:
- 👑 Admin: Can do everything
- 💰 Treasurer: Manages money
- 🔧 Operator: Runs daily tasks
Creating Roles
bytes32 public constant ADMIN = keccak256("ADMIN");
bytes32 public constant TREASURER = keccak256("TREASURER");
mapping(bytes32 => mapping(address => bool))
private roles;
modifier onlyRole(bytes32 role) {
require(roles[role][msg.sender], "Wrong role!");
_;
}
function grantRole(bytes32 role, address account)
public onlyRole(ADMIN)
{
roles[role][account] = true;
}
OpenZeppelin Makes It Easy
import "@openzeppelin/contracts/access/AccessControl.sol";
contract MyContract is AccessControl {
bytes32 public constant MINTER_ROLE =
keccak256("MINTER_ROLE");
function mint(address to)
public onlyRole(MINTER_ROLE)
{
// Only minters can call this
}
}
⏸️ Pausable Contracts
The Emergency Stop Button
Every good machine has a big red STOP button. Smart contracts should too!
When to Pause?
- 🐛 You found a bug
- 🚨 Someone is attacking
- 🔧 You need to upgrade
The Pattern
bool public paused;
modifier whenNotPaused() {
require(!paused, "Contract is paused!");
_;
}
modifier whenPaused() {
require(paused, "Contract is not paused!");
_;
}
function pause() public onlyOwner {
paused = true;
}
function unpause() public onlyOwner {
paused = false;
}
function transfer(address to, uint amount)
public whenNotPaused
{
// Only works when not paused
}
With OpenZeppelin
import "@openzeppelin/contracts/security/Pausable.sol";
contract MyToken is Pausable {
function transfer() public whenNotPaused {
// Protected by pause
}
function pause() public onlyOwner {
_pause();
}
}
🎯 Quick Reference
graph TD A["Contract Security"] --> B["Reentrancy"] A --> C["Arithmetic"] A --> D["Ether Transfer"] A --> E["Access Control"] B --> B1["Use Guards"] B --> B2["CEI Pattern"] C --> C1["Use Solidity 0.8+"] D --> D1["Use call with check"] E --> E1["msg.sender not tx.origin"] E --> E2["Roles for teams"] E --> E3["Pausable for emergencies"]
🏆 Key Takeaways
- Reentrancy: Update state BEFORE external calls
- Guards: Lock the door while working
- Arithmetic: Use Solidity 0.8+ or SafeMath
- Ether Transfers: Use
callwith success check - tx.origin: Never use it! Use msg.sender
- Access Control: Limit who can do what
- Roles: Multiple keys for multiple people
- Pausable: Always have an emergency stop
Remember: In smart contracts, there are no second chances. Test everything. Audit your code. And always think like a thief trying to break in! 🛡️
