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.
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)
| Location | Lifetime | Mutable? | Gas |
|---|---|---|---|
storage | Persistent on-chain | Yes | Very expensive |
memory | Function call | Yes | Cheap |
calldata | Function call, read-only | No | Cheapest (no copy) |
stack | Single expression | Yes | Free (256-bit slots) |
storage 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)
msg.sender— immediate caller (EOA or contract).tx.origin— the EOA that started the whole tx. Never use for auth (Phase 10 explains why).address(this)— the running contract.
4. Visibility & state mutability
| Visibility | Callable by |
|---|---|
external | Outside only (cheapest for big calldata) |
public | Outside + this contract |
internal | This contract + derived |
private | This contract only (still visible on-chain!) |
Mutability: view (reads state, no writes), pure (no reads/writes), payable (can receive ETH).
private 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;
});
});
forge test (Foundry) — recommended once you're comfortable.9. Gas-conscious patterns
- Pack tight structs (
uint128 + uint128→ 1 slot). - Use
++iinstead ofi++(saves ~5 gas). - Prefer
calldatafor external arrays. - Cache storage reads into memory before loops.
- Use custom
errortypes (cheaper than string revert reasons). - Avoid unbounded loops over user-provided arrays — someone will DoS you.
10. Project
Quiz
private storage variable. Is it hidden from other users?- Yes, only your contract can read it
- No — anyone can read any slot via
eth_getStorageAt.privateonly blocks Solidity-level access - Only the block producer can read it
- Yes, unless gas is paid
private is access control in the compiler, not on the chain.