hoelee c8e59061de
CI / Foundry project (push) Has been cancelled
docs: rewrite README as learning note; archive old cheat-sheet
- README.md: comprehensive learning-oriented writeup (contracts, structure,
  setup, concepts, gotchas) matching the actual code/Makefile.
- notes/OLD-README-cheatsheet.md: verbatim archive of the original raw
  command scratch-pad + base64 outputs.
2026-08-19 19:42:03 +08:00
2024-08-18 06:19:00 +08:00
2024-08-19 13:34:51 +08:00
2024-08-19 15:43:51 +08:00
2024-08-18 17:10:03 +08:00
2024-08-18 17:10:03 +08:00
2024-08-19 15:43:51 +08:00
2024-08-19 13:34:51 +08:00
2024-08-19 13:34:51 +08:00
2024-08-18 17:10:03 +08:00
2024-08-18 13:19:30 +08:00
2024-08-19 15:43:51 +08:00
2024-08-18 17:10:03 +08:00
2024-08-18 17:10:03 +08:00
2024-08-18 17:10:03 +08:00
2024-08-18 17:10:03 +08:00
2024-08-18 17:10:03 +08:00

Foundry NFT — Learning Project

A hands-on Solidity / Foundry learning project for building and deploying ERC-721 NFTs. It walks through the full lifecycle — writing the contract, embedding an image fully on-chain, storing metadata on IPFS, deploying to a local Anvil node or Sepolia testnet, and minting/viewing the result in MetaMask.

This is a learning repo, not production code. The contract and tests are deliberately kept small and heavily commented so each concept is easy to follow. See Concepts covered for the study notes.


What this project does

It implements two NFT contracts and the scripts + tests to drive them:

Contract Description
BasicNft.sol A minimal ERC-721 where mintNft(tokenUri) stores an arbitrary URI (usually an IPFS URI) per token.
MoodNft.sol A "dynamic" / fully-on-chain SVG NFT. The artwork is encoded in the contract itself and can be flipped between Happy and Sad states by the owner.

Both inherit OpenZeppelin's ERC721 — so most of the heavy lifting (transfer, approval, safeMint, balanceOf, ownerOf) comes for free.


Project structure

├── src/
│   ├── BasicNft.sol          # Minimal ERC-721 with per-token URI
│   └── MoodNft.sol           # On-chain SVG NFT with flipMood()
├── script/
│   ├── DeployBasicNft.s.sol  # Deploys BasicNft
│   ├── DeployMoodNft.s.sol   # Reads img/*.svg, base64-encodes them, deploys MoodNft
│   └── Interactions.s.sol    # MintBasicNft / MintMoodNft (uses DevOpsTools to find latest deployment)
├── test/
│   ├── unit/                 # Isolated unit tests (DeployMoodNftTest, MoodNftTest)
│   └── interactions/         # Integration tests (BasicNftTest, MoodNftIntegrationTest)
├── img/                      # Source SVG art + example metadata JSONs for IPFS
├── lib/                      # Submodules: forge-std, openzeppelin-contracts, foundry-devops
├── foundry.toml              # Foundry config (build, remappings, rpc/etherscan)
├── Makefile                  # Convenience targets (deploy, mint, test, anvil...)
├── sample1-deploy.txt        # Reference log: Sepolia deploy of BasicNft
└── sample2-mint.txt          # Reference log: Sepolia mint of BasicNft

Getting started

1. Install Foundry

curl -L https://foundry.paradigm.xyz | bash
foundryup

2. Install dependencies (git submodules)

git submodule update --init --recursive

3. Build & test

forge build
forge test

The test suite runs 6 tests across 4 suites and should pass green.


Run against a local Anvil node

# Terminal 1 — start a local EVM
make anvil

# Terminal 2 — deploy and mint
make deploy
make mint

make anvil starts anvil with a known test mnemonic and 1-second block time. The default deployer key is Anvil's first account (0xac09...), already configured in the Makefile.


Deploy to Sepolia testnet

Set up your .env (ignored by git):

SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/<YOUR_KEY>
ETHERSCAN_API_KEY=<YOUR_KEY>
SEPOLIA_PRIVATE_KEY=<YOUR_PRIVATE_KEY>

Then:

make deploy ARGS="--network sepolia"
make mint ARGS="--network sepolia"

The make deploy/make mint targets auto-switch to the Sepolia RPC + auto-verify on Etherscan when --network sepolia is passed. See sample1-deploy.txt and sample2-mint.txt for what a successful run looks like.

Viewing the NFT in MetaMask: add the deployed contract + token ID (starts at 0) as a collectible. For BasicNft you'll see the IPFS image; for MoodNft the artwork is rendered from the on-chain base64 SVG.


Concepts covered

1. ERC-721 essentials (OpenZeppelin)

  • Inheriting ERC721 gives you the full token standard (ERC-165, transfers, approvals, metadata).
  • _safeMint(to, tokenId) — guarantees the recipient can actually receive ERC-721s (reverts for plain EOAs that would lose the token).
  • Custom errors (error BasicNft__TokenUriNotFound();) — modern, gas-cheaper alternative to require with string messages.

2. tokenURI and metadata

  • tokenURI(tokenId) returns the location of the token's metadata JSON — not necessarily the raw image.
  • The standard metadata shape:
    {
      "name": "...",
      "description": "...",
      "image": "<ipfs or data: URI>",
      "attributes": [ { "trait_type": "...", "value": "..." } ]
    }
    
    Store this JSON on IPFS, then point the contract at its ipfs:// CID. Example metadata JSONs live in img/*.json.

3. Fully on-chain SVG (MoodNft)

  • Instead of hosting art on IPFS, the SVG is base64-encoded and embedded in the contract:
    data:image/svg+xml;base64,<encoded>
    
  • DeployMoodNft uses vm.readFile("img/smile.svg") + Base64.encode(...) (OpenZeppelin) to build these image URIs at deploy time.
  • tokenURI then builds a JSON data:application/json;base64,... URI on the fly, so the entire NFT is self-contained on-chain.
  • flipMood(tokenId) mutates on-chain state (s_tokenIdToMood), so the NFT "changes expression" — a gentle intro to dynamic NFTs.

4. Foundry testing patterns

  • Unit tests (test/unit/) instantiate contracts directly with fixed inputs.
  • Integration tests (test/interactions/) run the actual deploy scripts (deployer.run()) to exercise the full path.
  • vm.prank(USER) — impersonate an address for the next call.
  • makeAddr("HOELEE") — deterministic fake address from a label.
  • String comparison gotcha: you can't == two strings in Solidity. Compare their hashes instead:
    keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b))
    

5. DevOpsTools (foundry-devops)

DevOpsTools.get_most_recent_deployment("BasicNft", block.chainid) reads the broadcast/ logs to find the latest deployed contract — so the mint script automatically targets the last deployment on the current chain instead of a hardcoded address.


Notes & gotchas

  • Line endings / SVG: the deploy script base64-encodes whatever bytes vm.readFile returns. The repo pins img/*.svg to LF via .gitattributes so the on-chain bytes are deterministic regardless of platform (Windows/core.autocrlf would otherwise inject CRLF and silently change the encoded art).
  • remappings in foundry.toml maps @openzeppelin/contracts/... → lib/openzeppelin-contracts/contracts.
  • IPFS pinning: Interactions.s.sol contains both IPFS CIDs and Pinata gateway URLs as examples. Prefer the raw ipfs://<CID> form in production so the asset resolves independent of any single gateway.

Reference

S
Description
Learning project: ERC-721 NFTs built with Foundry - on-chain SVG Mood NFT + IPFS-backed Basic NFT
Readme
207 KiB
Languages
Makefile 100%