web3.path

PHASE 06 Solidity · ~6 hours

Smart Contracts

Solidity is "a strongly-typed JavaScript that compiles to EVM bytecode and has to be right the first time". There is no hotfix: once deployed, code is immutable.

Goal — write, test, and deploy an ERC-20 and a small NFT. Know storage vs memory vs calldata, modifiers, events, and the security-minded patterns you'll deepen in Phase 10.

1. Contract anatomy

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract Counter {
    address public owner;          // storage slot 0
    uint256 public count;          // storage slot 1
    event Bumped(address by, uint256 newCount);
    error NotOwner();

    constructor() { owner = msg.sender; }

    modifier onlyOwner() {
        if (msg.sender != owner) revert NotOwner();
        _;
    }

    function bump() external {
        count += 1;                 // SSTORE update: 2,900 gas
        emit Bumped(msg.sender, count);
    }

    function reset() external onlyOwner { count = 0; }
}

2. Data locations (this trips everyone up)

LocationLifetimeMutable?Gas
storagePersistent on-chainYesVery expensive
memoryFunction callYesCheap
calldataFunction call, read-onlyNoCheapest (no copy)
stackSingle expressionYesFree (256-bit slots)
Analogystorage is Postgres (survives forever, slow writes), memory is a request-scoped heap (like a Node.js request object), calldata is the raw HTTP body — immutable, read where it lies.
// Prefer calldata for external read-only params:
function sum(uint256[] calldata xs) external pure returns (uint256 s) {
    for (uint i; i < xs.length; ++i) s += xs[i];
}

3. msg.sender, tx.origin, address(this)

4. Visibility & state mutability

VisibilityCallable by
externalOutside only (cheapest for big calldata)
publicOutside + this contract
internalThis contract + derived
privateThis contract only (still visible on-chain!)

Mutability: view (reads state, no writes), pure (no reads/writes), payable (can receive ETH).

Gotchaprivate is a compiler access modifier, not encryption. Anyone can read any storage slot. Do not store secrets on-chain.

5. Events — your on-chain console.log

event Transfer(address indexed from, address indexed to, uint256 value);
emit Transfer(msg.sender, to, amt);

indexed fields become topics (queryable); non-indexed go into the data blob. Your backend (Phase 8) will index these. Events are the API surface between the chain and off-chain services.

6. ERC-20 — the canonical token

Don't write from scratch in production. Use OpenZeppelin.

// npm i @openzeppelin/contracts
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract MyToken is ERC20, Ownable {
    constructor() ERC20("MyToken", "MTK") Ownable(msg.sender) {
        _mint(msg.sender, 1_000_000 * 1e18);
    }
    function mint(address to, uint256 amt) external onlyOwner {
        _mint(to, amt);
    }
}

The ERC-20 interface is 6 functions: totalSupply, balanceOf, transfer, approve, allowance, transferFrom, plus Transfer/Approval events. That's the entire standard.

7. A tiny NFT (ERC-721)

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract Kitty is ERC721 {
    uint256 public nextId;
    constructor() ERC721("Kitty", "KTY") {}
    function mint() external { _safeMint(msg.sender, nextId++); }
}

NFTs differ from ERC-20 in one word: unique. Each tokenId has an owner (not a balance). The tokenURI typically points to IPFS/HTTPS metadata.

8. Testing with Hardhat (Chai + ethers)

const { expect } = require("chai");
describe("MyToken", () => {
  it("mints initial supply to deployer", async () => {
    const [owner] = await ethers.getSigners();
    const T = await ethers.getContractFactory("MyToken");
    const t = await T.deploy();
    expect(await t.balanceOf(owner.address)).to.equal(10n**24n);
  });
  it("blocks non-owners from mint", async () => {
    const [,alice] = await ethers.getSigners();
    const t = await (await ethers.getContractFactory("MyToken")).deploy();
    await expect(t.connect(alice).mint(alice.address, 1)).to.be.reverted;
  });
});
Fast feedback — Hardhat runs an in-memory EVM per test. You can fuzz with 10,000 cases in seconds using forge test (Foundry) — recommended once you're comfortable.

9. Gas-conscious patterns

10. Project

Deliverable — an ERC-20 with a capped supply, an owner-mint, and a burn. Plus an ERC-721 that mints for a fixed ETH price. Full test coverage. Deploy to Hardhat's local chain.

Quiz

Q. You store a password hash in a private storage variable. Is it hidden from other users?
All contract storage is public. private is access control in the compiler, not on the chain.
← Phase 5Phase 7: Web3 Integration →