COURSE · 6 LESSONS · 100% FREE
⛓️

Blockchain & Web3

Decentralized future — 6 lessons covering smart contracts, Solidity, DeFi protocols, DApp development, and security.

0Lessons
0Code Examples
BasicProgramming
0%
You've completed 0 of 6 lessons
J/ Next
K/ Prev
Esc Collapse
/ Search
Foundations

What Is a Blockchain?

A blockchain is an immutable, distributed ledger where data is grouped into blocks, each containing a cryptographic hash of the previous block. This creates a chain that is tamper-evident — altering one block invalidates every subsequent block.

Core Properties

  • Decentralization — No single point of failure; thousands of nodes hold copies of the ledger
  • Immutability — Once confirmed, data cannot be retroactively changed without network consensus
  • Transparency — All transactions are publicly verifiable on public chains
  • Censorship resistance — No single entity can block or reverse transactions

Hash Functions

A cryptographic hash function maps arbitrary-length input to a fixed-length output. Blockchain uses SHA-256 (Bitcoin) and Keccak-256 (Ethereum) to link blocks and verify data integrity.

JavaScript
const crypto = require('crypto');

// SHA-256: Bitcoin's hash function
const block = { index: 1, data: "Alice sends 5 BTC to Bob", timestamp: 1700000000 };
const hash = crypto.createHash('sha256').update(JSON.stringify(block)).digest('hex');
console.log(hash);
// => "a3f2b8c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1"

// Keccak-256: Ethereum's hash function
const { keccak256 } = require('@noble/hashes/sha3');
const data = ethers.utils.toUtf8Bytes("Hello, Ethereum!");
const ethHash = ethers.utils.hexlify(keccak256(data));
console.log(ethHash);

Merkle Trees

Merkle trees allow efficient verification of large datasets. Each leaf is a transaction hash; the Merkle root is embedded in the block header. Verifying a single transaction requires only O(log n) hashes instead of O(n).

JavaScript
const { keccak256 } = require('@noble/hashes/sha3');

function merkleRoot(transactions) {
  if (transactions.length === 0) return '0x' + '0'.repeat(64);
  if (transactions.length === 1) return transactions[0];

  let level = transactions.map(tx =>
    ethers.utils.hexlify(keccak256(ethers.utils.toUtf8Bytes(tx)))
  );

  while (level.length > 1) {
    const next = [];
    for (let i = 0; i < level.length; i += 2) {
      const left = level[i];
      const right = i + 1 < level.length ? level[i + 1] : left;
      const combined = ethers.utils.concat([left, right]);
      next.push(ethers.utils.hexlify(keccak256(combined)));
    }
    level = next;
  }
  return level[0];
}

const txs = ["Alice->Bob:5", "Bob->Carol:3", "Carol->Dave:2", "Dave->Alice:1"];
console.log("Merkle Root:", merkleRoot(txs));

Proof of Work vs Proof of Stake

PoW (Bitcoin, pre-merge Ethereum)

Miners compete to find a nonce that produces a hash below a difficulty target. The first to solve it broadcasts the block and earns a reward.

JavaScript
const crypto = require('crypto');

function proofOfWork(data, difficulty) {
  let nonce = 0;
  const prefix = '0'.repeat(difficulty);
  while (true) {
    const hash = crypto.createHash('sha256')
      .update(data + nonce)
      .digest('hex');
    if (hash.startsWith(prefix)) {
      return { nonce, hash };
    }
    nonce++;
  }
}

const result = proofOfWork("block #1000", 4);
console.log(`Nonce: ${result.nonce}, Hash: ${result.hash}`);
PoS (Ethereum post-merge)

Validators stake ETH as collateral. The protocol pseudo-randomly selects a validator to propose each block. Misbehavior results in slashing — partial or total loss of staked ETH. PoS uses ~99.95% less energy than PoW.

Consensus in Detail

Consensus mechanisms solve the Byzantine Generals Problem — how to reach agreement among distributed, potentially malicious nodes. Key algorithms:

  • Nakamoto Consensus — Longest chain wins (Bitcoin)
  • Gasper — PoS + Casper FFG (Ethereum)
  • PBFT — Practical Byzantine Fault Tolerance (Hyperledger)
  • Tendermint — BFT with instant finality (Cosmos)
💡
Key InsightHash functions provide data integrity and block linking. Merkle trees enable efficient transaction verification in O(log n) time.

Key Takeaways

  • Blockchains are cryptographically secured distributed ledgers
  • Hash functions provide data integrity and block linking
  • Merkle trees enable efficient transaction verification
  • PoW trades energy for security; PoS trades energy cost for stake-based security
  • Consensus is the mechanism that keeps all nodes in agreement
🧪 Quick Check
What does a Merkle root enable?

The Ethereum Virtual Machine (EVM)

The EVM is a deterministic, sandboxed runtime that executes smart contract bytecode. Every Ethereum node runs the same code against the same state, producing identical results. It is a stack-based machine with 256-bit word size.

Key EVM Concepts

  • Gas — Every operation costs gas. Gas limit caps total computation; gas price (in Gwei) determines cost in ETH.
  • Storage vs Memory vs Stack — Storage is persistent (expensive), memory is temporary (cheap), stack is execution-only.
  • Opcodes — ~140 opcodes like ADD, SSTORE, CALL, CREATE2. Each has a fixed gas cost.

Accounts & Transactions

JavaScript
const { ethers } = require('ethers');

const provider = new ethers.JsonRpcProvider("https://mainnet.infura.io/v3/YOUR_KEY");

async function inspectAccount(address) {
  const code = await provider.getCode(address);
  const balance = await provider.getBalance(address);
  const nonce = await provider.getTransactionCount(address);

  console.log(`Address: ${address}`);
  console.log(`Balance: ${ethers.formatEther(balance)} ETH`);
  console.log(`Nonce: ${nonce}`);
  console.log(`Is Contract: ${code !== '0x'}`);
}

inspectAccount("0xdAC17F958D2ee523a2206206994597C13D831ec7"); // USDT (contract)
inspectAccount("0x0000000000000000000000000000000000000000");  // Burn address

Gas Mechanics

JavaScript
const { ethers } = require('ethers');

const provider = new ethers.JsonRpcProvider("https://mainnet.infura.io/v3/YOUR_KEY");

async function estimateGasCost() {
  const feeData = await provider.getFeeData();
  const gasLimit = 21000n;
  const gasPrice = feeData.gasPrice;
  const totalCostWei = gasLimit * gasPrice;
  const totalCostEth = ethers.formatEther(totalCostWei);

  console.log(`Gas Limit: ${gasLimit}`);
  console.log(`Gas Price: ${ethers.formatUnits(gasPrice, 'gwei')} Gwei`);
  console.log(`Max Fee: ${totalCostEth} ETH`);
  console.log(`Max Fee (USD): $${(parseFloat(totalCostEth) * 3500).toFixed(2)}`);
}

estimateGasCost();

// EIP-1559: Base fee + Priority fee
async function eip1559Cost() {
  const feeData = await provider.getFeeData();
  console.log(`Base Fee: ${ethers.formatUnits(feeData.gasPrice, 'gwei')} Gwei`);
  console.log(`Max Priority Fee: ${ethers.formatUnits(feeData.maxPriorityFeePerGas, 'gwei')} Gwei`);
  console.log(`Max Fee Per Gas: ${ethers.formatUnits(feeData.maxFeePerGas, 'gwei')} Gwei`);
}
eip1559Cost();

First Smart Contract — Solidity Basics

Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Counter {
    uint256 public count;
    address public owner;

    event CountChanged(uint256 newCount, address indexed caller);

    modifier onlyOwner() {
        require(msg.sender == owner, "Not the owner");
        _;
    }

    constructor() {
        owner = msg.sender;
        count = 0;
    }

    function increment() public {
        count += 1;
        emit CountChanged(count, msg.sender);
    }

    function decrement() public {
        require(count > 0, "Count is already zero");
        count -= 1;
        emit CountChanged(count, msg.sender);
    }

    function getCount() public view returns (uint256) {
        return count;
    }

    function reset() public onlyOwner {
        count = 0;
        emit CountChanged(count, msg.sender);
    }
}

Deploying with ethers.js

JavaScript
const { ethers } = require('ethers');

async function deployCounter() {
  const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
  const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);

  const abi = [
    "function count() view returns (uint256)",
    "function owner() view returns (address)",
    "function increment()",
    "function decrement()",
    "function getCount() view returns (uint256)",
    "function reset()"
  ];

  const bytecode = "0x6080604052..."; // Compiled Solidity bytecode

  const factory = new ethers.ContractFactory(abi, bytecode, wallet);
  const contract = await factory.deploy();
  await contract.waitForDeployment();

  console.log(`Deployed to: ${await contract.getAddress()}`);

  await contract.increment();
  console.log(`Count: ${await contract.count()}`);
}

deployCounter();

Transaction Lifecycle

  1. EOA signs a transaction with their private key
  2. Transaction is broadcast to the mempool
  3. A validator includes it in a block
  4. EVM executes the transaction (applies state changes)
  5. Gas is consumed; unused gas is refunded
  6. Event logs are emitted for off-chain listeners
⚠️
Gas is fuelEvery operation on Ethereum costs gas. Always estimate gas before sending transactions to avoid failed txns and wasted ETH.

Key Takeaways

  • The EVM is the world computer executing smart contracts
  • Gas prevents infinite loops and compensates validators
  • EOAs initiate transactions; contracts respond to calls
  • Solidity is the primary language for Ethereum smart contracts
  • Events are cheap storage for off-chain indexing
🧪 Quick Check
What happens when gas runs out during execution?
← PrevBlockchain Fundamentals
Intermediate

Data Types & Variables

Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract SolidityTypes {
    // Value types
    uint256 public uintVar = 1 ether;
    int256 public intVar = -42;
    bool public boolVar = true;
    address public addrVar = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
    bytes32 public bytes32Var = keccak256("hello");

    // Reference types
    string public stringVar = "Hello, Solidity";
    bytes public bytesVar = abi.encodePacked("raw bytes");

    // Arrays
    uint256[] public dynamicArray;
    uint256[5] public fixedArray;

    // Mappings (hash tables)
    mapping(address => uint256) public balances;
    mapping(address => mapping(address => uint256)) public allowances;

    // Structs
    struct Proposal {
        uint256 id;
        string description;
        uint256 voteCount;
        bool executed;
        mapping(address => bool) hasVoted;
    }

    Proposal[] public proposals;

    // Enums
    enum Status { Pending, Active, Closed, Cancelled }
    Status public currentStatus;

    function createProposal(string calldata _desc) external {
        proposals.push(Proposal({
            id: proposals.length,
            description: _desc,
            voteCount: 0,
            executed: false
        }));
    }

    // Storage vs Memory vs Calldata
    function processArray(uint256[] calldata input) external pure returns (uint256) {
        uint256 sum = 0;
        for (uint256 i = 0; i < input.length; i++) {
            sum += input[i];
        }
        return sum;
    }
}

Modifiers & Events

Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract AccessControl {
    address public admin;
    mapping(address => bool) public moderators;
    mapping(address => uint256) public lastAction;

    // Custom errors (cheaper than require strings)
    error NotAdmin();
    error NotModerator();
    error CooldownActive(uint256 remaining);

    event AdminChanged(address indexed oldAdmin, address indexed newAdmin);
    event ModeratorAdded(address indexed account);
    event ActionRecorded(address indexed user, uint256 timestamp);

    modifier onlyAdmin() {
        if (msg.sender != admin) revert NotAdmin();
        _;
    }

    modifier onlyModerator() {
        if (!moderators[msg.sender]) revert NotModerator();
        _;
    }

    modifier cooldown(uint256 _seconds) {
        uint256 elapsed = block.timestamp - lastAction[msg.sender];
        if (elapsed < _seconds) {
            revert CooldownActive(_seconds - elapsed);
        }
        _;
    }

    constructor() {
        admin = msg.sender;
    }

    function addModerator(address _account) external onlyAdmin {
        moderators[_account] = true;
        emit ModeratorAdded(_account);
    }

    function changeAdmin(address _newAdmin) external onlyAdmin {
        emit AdminChanged(admin, _newAdmin);
        admin = _newAdmin;
    }

    function recordAction() external cooldown(1 hours) {
        lastAction[msg.sender] = block.timestamp;
        emit ActionRecorded(msg.sender, block.timestamp);
    }
}
Gas OptimizationCustom errors are cheaper than require strings. Use revert NotAdmin() instead of require(msg.sender == admin, "Not admin").

Inheritance & Interfaces

Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

abstract contract Ownable {
    address public owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    constructor(address _owner) {
        owner = _owner;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    function transferOwnership(address _newOwner) external onlyOwner {
        emit OwnershipTransferred(owner, _newOwner);
        owner = _newOwner;
    }
}

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

contract Pausable is Ownable {
    bool public paused;

    event Paused(address account);
    event Unpaused(address account);

    modifier whenNotPaused() {
        require(!paused, "Contract is paused");
        _;
    }

    modifier whenPaused() {
        require(paused, "Contract is not paused");
        _;
    }

    constructor(address _owner) Ownable(_owner) {}

    function pause() external onlyOwner {
        paused = true;
        emit Paused(msg.sender);
    }

    function unpause() external onlyOwner {
        paused = false;
        emit Unpaused(msg.sender);
    }
}

contract TokenVault is Pausable {
    IERC20 public immutable token;
    mapping(address => uint256) public deposits;

    event Deposited(address indexed user, uint256 amount);
    event Withdrawn(address indexed user, uint256 amount);

    constructor(address _token) Pausable(msg.sender) {
        token = IERC20(_token);
    }

    function deposit(uint256 _amount) external whenNotPaused {
        token.transferFrom(msg.sender, address(this), _amount);
        deposits[msg.sender] += _amount;
        emit Deposited(msg.sender, _amount);
    }

    function withdraw(uint256 _amount) external whenNotPaused {
        require(deposits[msg.sender] >= _amount, "Insufficient balance");
        deposits[msg.sender] -= _amount;
        token.transfer(msg.sender, _amount);
        emit Withdrawn(msg.sender, _amount);
    }
}

Libraries

Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

library SafeMath {
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a, "SafeMath: overflow");
        return c;
    }

    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b <= a, "SafeMath: underflow");
        return a - b;
    }

    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        if (a == 0) return 0;
        uint256 c = a * b;
        require(c / a == b, "SafeMath: overflow");
        return c;
    }
}

contract UsingSafeMath {
    using SafeMath for uint256;
    uint256 public total;

    function addToTotal(uint256 _amount) external {
        total = total.add(_amount);
    }
}

Key Takeaways

  • Value types live on stack; reference types can be storage/memory/calldata
  • Custom errors are cheaper than require strings
  • Interfaces define the contract ABI for external calls
  • Abstract contracts provide partial implementations
  • Libraries enable reusable, gas-efficient code
← PrevEthereum & Smart Contracts

ERC-20 Token Standard

Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

contract MyToken is IERC20 {
    string public name;
    string public symbol;
    uint8 public decimals = 18;
    uint256 private _totalSupply;

    mapping(address => uint256) private _balances;
    mapping(address => mapping(address => uint256)) private _allowances;

    constructor(string memory _name, string memory _symbol, uint256 _initialSupply) {
        name = _name;
        symbol = _symbol;
        _mint(msg.sender, _initialSupply * 10 ** decimals);
    }

    function totalSupply() external view override returns (uint256) {
        return _totalSupply;
    }

    function balanceOf(address account) external view override returns (uint256) {
        return _balances[account];
    }

    function transfer(address to, uint256 amount) external override returns (bool) {
        _transfer(msg.sender, to, amount);
        return true;
    }

    function approve(address spender, uint256 amount) external override returns (bool) {
        _allowances[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) external override returns (bool) {
        uint256 currentAllowance = _allowances[from][msg.sender];
        require(currentAllowance >= amount, "ERC20: insufficient allowance");
        unchecked {
            _allowances[from][msg.sender] = currentAllowance - amount;
        }
        _transfer(from, to, amount);
        return true;
    }

    function _transfer(address from, address to, uint256 amount) internal {
        require(from != address(0), "ERC20: transfer from zero");
        require(to != address(0), "ERC20: transfer to zero");
        require(_balances[from] >= amount, "ERC20: insufficient balance");
        unchecked {
            _balances[from] -= amount;
        }
        _balances[to] += amount;
        emit Transfer(from, to, amount);
    }

    function _mint(address to, uint256 amount) internal {
        require(to != address(0), "ERC20: mint to zero");
        _totalSupply += amount;
        _balances[to] += amount;
        emit Transfer(address(0), to, amount);
    }
}

ERC-721 NFT Standard

Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IERC721 {
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);

    function balanceOf(address owner) external view returns (uint256);
    function ownerOf(uint256 tokenId) external view returns (address);
    function approve(address to, uint256 tokenId) external;
    function getApproved(uint256 tokenId) external view returns (address);
    function transferFrom(address from, address to, uint256 tokenId) external;
}

contract MyNFT is IERC721 {
    string private _name;
    string private _symbol;
    uint256 private _tokenIdCounter;
    uint256 public mintPrice = 0.01 ether;

    mapping(uint256 => address) private _owners;
    mapping(address => uint256) private _balances;
    mapping(uint256 => address) private _tokenApprovals;
    mapping(address => mapping(address => bool)) private _operatorApprovals;

    constructor(string memory name_, string memory symbol_) {
        _name = name_;
        _symbol = symbol_;
    }

    function name() external view returns (string memory) { return _name; }
    function symbol() external view returns (string memory) { return _symbol; }

    function balanceOf(address owner) external view override returns (uint256) {
        require(owner != address(0), "Zero address");
        return _balances[owner];
    }

    function ownerOf(uint256 tokenId) public view override returns (address) {
        address owner = _owners[tokenId];
        require(owner != address(0), "Token does not exist");
        return owner;
    }

    function mint(address to) external payable {
        require(msg.value >= mintPrice, "Insufficient payment");
        uint256 tokenId = _tokenIdCounter++;
        _owners[tokenId] = to;
        _balances[to] += 1;
        emit Transfer(address(0), to, tokenId);
    }

    function approve(address to, uint256 tokenId) external override {
        address owner = ownerOf(tokenId);
        require(msg.sender == owner, "Not owner");
        _tokenApprovals[tokenId] = to;
        emit Approval(owner, to, tokenId);
    }

    function getApproved(uint256 tokenId) external view override returns (address) {
        return _tokenApprovals[tokenId];
    }

    function transferFrom(address from, address to, uint256 tokenId) external override {
        address owner = ownerOf(tokenId);
        require(
            msg.sender == owner ||
            msg.sender == _tokenApprovals[tokenId] ||
            isApprovedForAll(owner, msg.sender),
            "Not authorized"
        );
        _tokenApprovals[tokenId] = address(0);
        _balances[from] -= 1;
        _balances[to] += 1;
        _owners[tokenId] = to;
        emit Transfer(from, to, tokenId);
    }

    function setApprovalForAll(address operator, bool approved) external {
        _operatorApprovals[msg.sender][operator] = approved;
    }

    function isApprovedForAll(address owner, address operator) public view returns (bool) {
        return _operatorApprovals[owner][operator];
    }
}
💡
Token StandardsERC-20 is for fungible tokens (currencies, utility tokens). ERC-721 is for non-fungible tokens (NFTs, unique digital assets).

Automated Market Maker (AMM)

Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract SimpleAMM {
    using SafeERC20 for IERC20;

    IERC20 public immutable token0;
    IERC20 public immutable token1;
    uint256 public reserve0;
    uint256 public reserve1;
    uint256 public totalLP;
    mapping(address => uint256) public lpBalances;

    event LiquidityAdded(address indexed provider, uint256 amount0, uint256 amount1, uint256 lpTokens);
    event Swapped(address indexed user, bool zeroForOne, uint256 amountIn, uint256 amountOut);

    constructor(address _token0, address _token1) {
        token0 = IERC20(_token0);
        token1 = IERC20(_token1);
    }

    function addLiquidity(uint256 amount0, uint256 amount1) external returns (uint256 lpTokens) {
        token0.safeTransferFrom(msg.sender, address(this), amount0);
        token1.safeTransferFrom(msg.sender, address(this), amount1);

        if (totalLP == 0) {
            lpTokens = sqrt(amount0 * amount1);
        } else {
            lpTokens = min(
                (amount0 * totalLP) / reserve0,
                (amount1 * totalLP) / reserve1
            );
        }

        totalLP += lpTokens;
        lpBalances[msg.sender] += lpTokens;
        reserve0 += amount0;
        reserve1 += amount1;

        emit LiquidityAdded(msg.sender, amount0, amount1, lpTokens);
    }

    function swap(bool zeroForOne, uint256 amountIn) external returns (uint256 amountOut) {
        uint256 amountInWithFee = amountIn * 997;
        uint256 reserveIn = zeroForOne ? reserve0 : reserve1;
        uint256 reserveOut = zeroForOne ? reserve1 : reserve0;

        amountOut = (amountInWithFee * reserveOut) / (reserveIn * 1000 + amountInWithFee);
        require(amountOut > 0, "Insufficient output");

        if (zeroForOne) {
            token0.safeTransferFrom(msg.sender, address(this), amountIn);
            token1.transfer(msg.sender, amountOut);
            reserve0 += amountIn;
            reserve1 -= amountOut;
        } else {
            token1.safeTransferFrom(msg.sender, address(this), amountIn);
            token0.transfer(msg.sender, amountOut);
            reserve1 += amountIn;
            reserve0 -= amountOut;
        }

        emit Swapped(msg.sender, zeroForOne, amountIn, amountOut);
    }

    function getAmountOut(bool zeroForOne, uint256 amountIn) external view returns (uint256) {
        uint256 amountInWithFee = amountIn * 997;
        uint256 reserveIn = zeroForOne ? reserve0 : reserve1;
        uint256 reserveOut = zeroForOne ? reserve1 : reserve0;
        return (amountInWithFee * reserveOut) / (reserveIn * 1000 + amountInWithFee);
    }

    function sqrt(uint256 x) internal pure returns (uint256) {
        if (x == 0) return 0;
        uint256 z = (x + 1) / 2;
        uint256 y = x;
        while (z < y) { y = z; z = (x / z + z) / 2; }
        return y;
    }

    function min(uint256 a, uint256 b) internal pure returns (uint256) {
        return a < b ? a : b;
    }
}

Staking Contract

Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract Staking {
    IERC20 public immutable stakingToken;
    uint256 public rewardRate = 10;
    uint256 public lastClaimBlock;

    struct StakeInfo {
        uint256 amount;
        uint256 rewardDebt;
        uint256 stakedAt;
    }

    mapping(address => StakeInfo) public stakes;
    uint256 public totalStaked;

    event Staked(address indexed user, uint256 amount, uint256 blockNumber);
    event Withdrawn(address indexed user, uint256 amount);
    event RewardClaimed(address indexed user, uint256 reward);

    constructor(address _stakingToken) {
        stakingToken = IERC20(_stakingToken);
    }

    function stake(uint256 _amount) external {
        require(_amount > 0, "Zero amount");
        stakingToken.transferFrom(msg.sender, address(this), _amount);

        if (stakes[msg.sender].amount > 0) {
            _claimReward(msg.sender);
        }

        stakes[msg.sender].amount += _amount;
        stakes[msg.sender].stakedAt = block.number;
        totalStaked += _amount;

        emit Staked(msg.sender, _amount, block.number);
    }

    function withdraw(uint256 _amount) external {
        StakeInfo storage info = stakes[msg.sender];
        require(info.amount >= _amount, "Insufficient stake");

        _claimReward(msg.sender);
        info.amount -= _amount;
        totalStaked -= _amount;

        stakingToken.transfer(msg.sender, _amount);
        emit Withdrawn(msg.sender, _amount);
    }

    function claimReward() external {
        _claimReward(msg.sender);
    }

    function _claimReward(address _user) internal {
        uint256 reward = pendingReward(_user);
        if (reward > 0) {
            stakes[_user].rewardDebt = (stakes[_user].amount * (block.number - stakes[_user].stakedAt) * rewardRate) / totalStaked;
            stakingToken.transfer(_user, reward);
            emit RewardClaimed(_user, reward);
        }
    }

    function pendingReward(address _user) public view returns (uint256) {
        StakeInfo storage info = stakes[_user];
        if (info.amount == 0 || totalStaked == 0) return 0;
        return (info.amount * (block.number - info.stakedAt) * rewardRate) / totalStaked;
    }
}

Key Takeaways

  • ERC-20 is the fungible token standard; ERC-721 is for unique NFTs
  • AMMs use constant product formula (x*y=k) for permissionless swaps
  • Liquidity providers earn fees proportional to their share
  • Staking rewards are calculated per-block based on share of total staked
  • Always use SafeERC20 for token transfers to handle non-standard returns
← PrevSolidity Deep Dive
Advanced

Connecting MetaMask

JavaScript
async function connectWallet() {
  if (!window.ethereum) {
    alert("Please install MetaMask");
    return null;
  }

  try {
    const accounts = await window.ethereum.request({
      method: "eth_requestAccounts",
    });

    const provider = new ethers.BrowserProvider(window.ethereum);
    const signer = await provider.getSigner();
    const address = accounts[0];

    console.log(`Connected: ${address}`);

    window.ethereum.on("accountsChanged", (newAccounts) => {
      if (newAccounts.length === 0) {
        console.log("Disconnected");
      } else {
        console.log(`Switched to: ${newAccounts[0]}`);
        location.reload();
      }
    });

    window.ethereum.on("chainChanged", () => {
      location.reload();
    });

    return { provider, signer, address };
  } catch (err) {
    console.error("Connection failed:", err);
    return null;
  }
}

Switching Networks

JavaScript
const CHAINS = {
  ethereum: { chainId: "0x1", name: "Ethereum Mainnet" },
  sepolia: { chainId: "0xaa36a7", name: "Sepolia Testnet" },
  polygon: { chainId: "0x89", name: "Polygon Mainnet" },
  arbitrum: { chainId: "0xa4b1", name: "Arbitrum One" },
};

async function switchChain(chainKey) {
  const chain = CHAINS[chainKey];
  try {
    await window.ethereum.request({
      method: "wallet_switchEthereumChain",
      params: [{ chainId: chain.chainId }],
    });
  } catch (err) {
    if (err.code === 4902) {
      await window.ethereum.request({
        method: "wallet_addEthereumChain",
        params: [{
          chainId: chain.chainId,
          chainName: chain.name,
          nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
          rpcUrls: ["https://rpc.ankr.com/eth"],
          blockExplorerUrls: ["https://etherscan.io"],
        }],
      });
    }
  }
}

Reading Contract State

JavaScript
const TOKEN_ABI = [
  "function name() view returns (string)",
  "function symbol() view returns (string)",
  "function decimals() view returns (uint8)",
  "function totalSupply() view returns (uint256)",
  "function balanceOf(address) view returns (uint256)",
  "function transfer(address to, uint256 amount) returns (bool)",
  "function approve(address spender, uint256 amount) returns (bool)",
  "function allowance(address owner, address spender) view returns (uint256)",
];

async function readTokenInfo(tokenAddress) {
  const provider = new ethers.BrowserProvider(window.ethereum);
  const contract = new ethers.Contract(tokenAddress, TOKEN_ABI, provider);

  const [name, symbol, decimals, totalSupply] = await Promise.all([
    contract.name(),
    contract.symbol(),
    contract.decimals(),
    contract.totalSupply(),
  ]);

  console.log(`${name} (${symbol})`);
  console.log(`Decimals: ${decimals}`);
  console.log(`Total Supply: ${ethers.formatUnits(totalSupply, decimals)}`);
}

async function checkBalance(tokenAddress, walletAddress) {
  const provider = new ethers.BrowserProvider(window.ethereum);
  const contract = new ethers.Contract(tokenAddress, TOKEN_ABI, provider);

  const balance = await contract.balanceOf(walletAddress);
  const decimals = await contract.decimals();
  const formatted = ethers.formatUnits(balance, decimals);

  console.log(`Balance: ${formatted} ${await contract.symbol()}`);
  return { raw: balance, formatted };
}
View FunctionsUse view functions for reads (no gas) and transactions for writes. Estimate gas before sending to avoid failed transactions.

Writing to Contracts

JavaScript
async function transferTokens(tokenAddress, toAddress, amountEth) {
  const provider = new ethers.BrowserProvider(window.ethereum);
  const signer = await provider.getSigner();

  const contract = new ethers.Contract(tokenAddress, TOKEN_ABI, signer);

  const decimals = await contract.decimals();
  const amount = ethers.parseUnits(amountEth, decimals);

  const gasEstimate = await contract.transfer.estimateGas(toAddress, amount);
  const feeData = await provider.getFeeData();

  const tx = await contract.transfer(toAddress, amount, {
    gasLimit: gasEstimate * 120n / 100n,
    maxFeePerGas: feeData.maxFeePerGas,
    maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
  });

  console.log(`TX sent: ${tx.hash}`);
  const receipt = await tx.wait();
  console.log(`Confirmed in block ${receipt.blockNumber}`);
  console.log(`Gas used: ${receipt.gasUsed.toString()}`);

  return receipt;
}

Full DApp — React + ethers.js

JavaScript
import { useState, useEffect, useCallback } from "react";
import { ethers } from "ethers";

const CONTRACT_ADDRESS = "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4";
const ABI = [
  "function count() view returns (uint256)",
  "function increment()",
  "function decrement()",
  "event CountChanged(uint256 newCount, address indexed caller)",
];

export default function CounterDApp() {
  const [account, setAccount] = useState(null);
  const [count, setCount] = useState(0);
  const [loading, setLoading] = useState(false);
  const [contract, setContract] = useState(null);

  const connect = useCallback(async () => {
    if (!window.ethereum) return alert("Install MetaMask");

    const _provider = new ethers.BrowserProvider(window.ethereum);
    const _signer = await _provider.getSigner();
    const _contract = new ethers.Contract(CONTRACT_ADDRESS, ABI, _signer);
    const _account = await _signer.getAddress();

    setContract(_contract);
    setAccount(_account);

    const _count = await _contract.count();
    setCount(Number(_count));
  }, []);

  const increment = async () => {
    if (!contract) return;
    setLoading(true);
    try {
      const tx = await contract.increment();
      await tx.wait();
      const newCount = await contract.count();
      setCount(Number(newCount));
    } catch (err) {
      console.error(err);
    }
    setLoading(false);
  };

  const decrement = async () => {
    if (!contract) return;
    setLoading(true);
    try {
      const tx = await contract.decrement();
      await tx.wait();
      const newCount = await contract.count();
      setCount(Number(newCount));
    } catch (err) {
      console.error(err);
    }
    setLoading(false);
  };

  useEffect(() => {
    if (!contract) return;
    const handler = (newCount, caller) => {
      setCount(Number(newCount));
      console.log(`Count changed by ${caller}`);
    };
    contract.on("CountChanged", handler);
    return () => contract.off("CountChanged", handler);
  }, [contract]);

  if (!account) {
    return (
      

Counter DApp

); } return (

Counter DApp

Account: {account.slice(0, 6)}...{account.slice(-4)}

Count: {count}

{loading &&

Transaction pending...

}
); }

Transaction Status Tracking

JavaScript
async function sendAndWait(txPromise) {
  const tx = await txPromise;
  console.log(`Pending: ${tx.hash}`);

  const receipt = await tx.wait(1);

  if (receipt.status === 1) {
    console.log(`Success in block ${receipt.blockNumber}`);
    console.log(`Gas used: ${receipt.gasUsed}`);
    console.log(`Events:`, receipt.logs.map(log => ({
      topic: log.topics[0],
      data: log.data,
    })));
  } else {
    console.error("Transaction reverted");
  }

  return receipt;
}

async function batchReads(provider, contract, addresses) {
  const calls = addresses.map(addr =>
    contract.balanceOf(addr).then(bal => ({ address: addr, balance: bal }))
  );
  return Promise.all(calls);
}

Key Takeaways

  • MetaMask injects window.ethereum for browser-based wallet interaction
  • Always handle account/chain changes with event listeners
  • Use view functions for reads (no gas) and transactions for writes
  • Estimate gas before sending to avoid failed transactions
  • Event listeners enable real-time UI updates without polling
← PrevDeFi Protocols

Reentrancy Attack

The most devastating Solidity vulnerability. A malicious contract calls back into the vulnerable contract before the first execution finishes, draining funds.

Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// VULNERABLE — DO NOT USE
contract VulnerableVault {
    mapping(address => uint256) public balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() external {
        uint256 balance = balances[msg.sender];
        require(balance > 0, "No balance");

        (bool success, ) = msg.sender.call{value: balance}("");
        require(success, "Transfer failed");

        balances[msg.sender] = 0;
    }
}

// Attack contract
contract ReentrancyAttacker {
    VulnerableVault public vault;

    constructor(address _vault) {
        vault = VulnerableVault(_vault);
    }

    function attack() external payable {
        vault.deposit{value: msg.value}();
        vault.withdraw();
    }

    receive() external payable {
        if (address(vault).balance >= 1 ether) {
            vault.withdraw();
        }
    }
}
🚫
NEVER deploy vulnerable contractsThe reentrancy pattern above is for education only. Always use Checks-Effects-Interactions and ReentrancyGuard.

Reentrancy Guard (Fix)

Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

abstract contract ReentrancyGuard {
    uint256 private _locked = 1;

    modifier nonReentrant() {
        require(_locked == 1, "ReentrancyGuard: reentrant call");
        _locked = 2;
        _;
        _locked = 1;
    }
}

contract SecureVault is ReentrancyGuard {
    mapping(address => uint256) public balances;

    event Deposit(address indexed user, uint256 amount);
    event Withdrawal(address indexed user, uint256 amount);

    function deposit() external payable {
        balances[msg.sender] += msg.value;
        emit Deposit(msg.sender, msg.value);
    }

    function withdraw() external nonReentrant {
        uint256 balance = balances[msg.sender];
        require(balance > 0, "No balance");

        balances[msg.sender] = 0;
        emit Withdrawal(msg.sender, balance);

        (bool success, ) = msg.sender.call{value: balance}("");
        require(success, "Transfer failed");
    }
}

Integer Overflow

Solidity
contract OverflowExample {
    uint8 public counter = 255;

    function safeIncrement() external {
        counter += 1; // Reverts on overflow (Solidity 0.8+)
    }

    function unsafeIncrement() external {
        unchecked {
            counter += 1; // Wraps to 0!
        }
    }
}

Access Control Vulnerabilities

Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract GoodOwnership {
    address public owner;
    bool private _initialized;

    constructor() {
        require(!_initialized, "Already initialized");
        owner = msg.sender;
        _initialized = true;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    address public pendingOwner;

    function transferOwnership(address newOwner) external onlyOwner {
        pendingOwner = newOwner;
    }

    function acceptOwnership() external {
        require(msg.sender == pendingOwner, "Not pending owner");
        owner = pendingOwner;
        pendingOwner = address(0);
    }
}

Front-Running Protection

Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract CommitReveal {
    mapping(address => bytes32) public commitments;
    mapping(address => uint256) public commitmentBlocks;

    event Committed(address indexed user, bytes32 hash);
    event Revealed(address indexed user, uint256 value);

    function commit(bytes32 _hash) external {
        commitments[msg.sender] = _hash;
        commitmentBlocks[msg.sender] = block.number;
        emit Committed(msg.sender, _hash);
    }

    function reveal(uint256 _value, bytes32 _salt) external {
        require(
            block.number > commitmentBlocks[msg.sender] + 2,
            "Wait 3 blocks"
        );
        bytes32 hash = keccak256(abi.encodePacked(_value, _salt, msg.sender));
        require(hash == commitments[msg.sender], "Invalid reveal");

        delete commitments[msg.sender];
        emit Revealed(msg.sender, _value);
    }
}
⚠️
Front-running is realMiners/validators can see pending transactions and reorder them. Use commit-reveal schemes or Flashbots Protect for sensitive operations.

Hardhat Project Setup

JavaScript
// hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");

module.exports = {
  solidity: {
    version: "0.8.20",
    settings: {
      optimizer: { enabled: true, runs: 200 },
    },
  },
  networks: {
    sepolia: {
      url: process.env.SEPOLIA_RPC_URL,
      accounts: [process.env.PRIVATE_KEY],
    },
  },
  etherscan: {
    apiKey: process.env.ETHERSCAN_API_KEY,
  },
};
JavaScript
// test/Counter.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("Counter", function () {
  let counter, owner, addr1;

  beforeEach(async function () {
    [owner, addr1] = await ethers.getSigners();
    const Counter = await ethers.getContractFactory("Counter");
    counter = await Counter.deploy();
  });

  it("should start at zero", async function () {
    expect(await counter.count()).to.equal(0);
  });

  it("should increment", async function () {
    await counter.increment();
    expect(await counter.count()).to.equal(1);
  });

  it("should decrement from positive", async function () {
    await counter.increment();
    await counter.increment();
    await counter.decrement();
    expect(await counter.count()).to.equal(1);
  });

  it("should revert when decrementing from zero", async function () {
    await expect(counter.decrement()).to.be.revertedWith("Count is already zero");
  });

  it("should emit CountChanged event", async function () {
    await expect(counter.increment())
      .to.emit(counter, "CountChanged")
      .withArgs(1, owner.address);
  });

  it("only owner can reset", async function () {
    await counter.increment();
    await counter.reset();
    expect(await counter.count()).to.equal(0);

    await expect(counter.connect(addr1).reset()).to.be.revertedWith("Not the owner");
  });
});

Static Analysis with Slither

Shell
pip install slither-analyzer

slither contracts/Vault.sol
slither contracts/Vault.sol --filter-paths node_modules

# Common detectors:
# reentrancy-eth — Reentrancy with ETH transfers
# reentrancy-no-eth — Reentrancy with contract calls
# unchecked-transfer — ERC20 transfer return value ignored
# locked-ether — Contract receives ETH but cannot withdraw
# arbitrary-send — Unrestricted ETH withdrawal

Fuzz Testing with Foundry

Shell
forge init my-project
cd my-project
forge build
forge test

# Fuzz test
forge test --match-test testFuzzStake -vvvv
Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import "../src/Staking.sol";

contract StakingTest is Test {
    Staking public staking;
    MockERC20 public token;

    function setUp() public {
        token = new MockERC20("StakeToken", "STK", 1000 ether);
        staking = new Staking(address(token));

        token.mint(address(this), 1000 ether);
        token.approve(address(staking), type(uint256).max);
    }

    function testStake() public {
        staking.stake(100 ether);
        assertEq(staking.totalStaked(), 100 ether);
        assertEq(staking.stakes(address(this)).amount, 100 ether);
    }

    function testWithdraw() public {
        staking.stake(100 ether);
        staking.withdraw(50 ether);
        assertEq(staking.totalStaked(), 50 ether);
    }

    function testWithdrawRevertsOnInsufficientBalance() public {
        staking.stake(100 ether);
        vm.expectRevert("Insufficient stake");
        staking.withdraw(200 ether);
    }

    function testFuzzStake(uint256 amount) public {
        vm.assume(amount > 0 && amount <= 1000 ether);
        staking.stake(amount);
        assertEq(staking.totalStaked(), amount);
    }
}

contract MockERC20 {
    string public name;
    string public symbol;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;

    constructor(string memory _name, string memory _symbol, uint256 _initialSupply) {
        name = _name;
        symbol = _symbol;
        _mint(msg.sender, _initialSupply);
    }

    function mint(address to, uint256 amount) external {
        totalSupply += amount;
        balanceOf[to] += amount;
    }

    function approve(address spender, uint256 amount) external returns (bool) {
        return true;
    }

    function transfer(address to, uint256 amount) external returns (bool) {
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) external returns (bool) {
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        return true;
    }
}

Security Checklist

  • Reentrancy — Use Checks-Effects-Interactions + ReentrancyGuard
  • Overflow — Use Solidity 0.8+ (built-in checks) or SafeMath
  • Access control — Verify msg.sender for every state-changing function
  • Front-running — Use commit-reveal or Flashbots Protect
  • Oracle manipulation — Use TWAP oracles, not spot prices
  • Flash loan attacks — Check for multi-block state or external oracle dependency
  • Unchecked return values — Always check ERC20 transfer/approve returns
  • Frontrunning minting — Use limits per wallet, time locks

Key Takeaways

  • Reentrancy is the #1 Solidity vulnerability — always use CEI + nonReentrant
  • Custom errors save gas and provide better debugging
  • Hardhat + Chai provides comprehensive testing for Solidity contracts
  • Slither catches common bugs before deployment
  • Foundry's fuzz testing finds edge cases humans miss
  • Never deploy without a security audit for production contracts
← PrevDApp Development

📚 Resources & Further Learning

📖
Solidity Docs
The official Solidity programming language documentation.
docs.soliditylang.org →
📘
Ethereum.org
Learn about Ethereum, smart contracts, and the decentralized web.
ethereum.org →
🎯
OpenZeppelin
Battle-tested smart contract libraries and security standards.
openzeppelin.com →
🧪
Hardhat Docs
Ethereum development environment for compiling, testing, and deploying.
hardhat.org →
Foundry Book
Blazing fast, portable, modular toolkit for Ethereum development.
getfoundry.sh →
📦
Ethers.js
Complete Ethereum library for browser and Node.js.
docs.ethers.org →
AI
Blockchain Tutor
ZenMux · GLM 4.7 Flash
Ask me anything about Blockchain & Web3! I can help with Solidity, smart contracts, DeFi, DApp development, security, or explain any concept from the lessons above.