Security Fundamentals

Back

Loading concept...

🛡️ 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:

  1. Put in $1
  2. Get candy
  3. Quickly reach in again before the machine updates
  4. 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:

  1. Check: Do you have enough money?
  2. Effect: Mark the order as paid
  3. 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

  1. Reentrancy: Update state BEFORE external calls
  2. Guards: Lock the door while working
  3. Arithmetic: Use Solidity 0.8+ or SafeMath
  4. Ether Transfers: Use call with success check
  5. tx.origin: Never use it! Use msg.sender
  6. Access Control: Limit who can do what
  7. Roles: Multiple keys for multiple people
  8. 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! 🛡️

Loading story...

Story - Premium Content

Please sign in to view this story and start learning.

Upgrade to Premium to unlock full access to all stories.

Stay Tuned!

Story is coming soon.

Story Preview

Story - Premium Content

Please sign in to view this concept and start learning.

Upgrade to Premium to unlock full access to all content.