- 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.
6.8 KiB
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. ForBasicNftyou'll see the IPFS image; forMoodNftthe artwork is rendered from the on-chain base64 SVG.
Concepts covered
1. ERC-721 essentials (OpenZeppelin)
- Inheriting
ERC721gives 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 torequirewith 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:
Store this JSON on IPFS, then point the contract at its
{ "name": "...", "description": "...", "image": "<ipfs or data: URI>", "attributes": [ { "trait_type": "...", "value": "..." } ] }ipfs://CID. Example metadata JSONs live inimg/*.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> DeployMoodNftusesvm.readFile("img/smile.svg")+Base64.encode(...)(OpenZeppelin) to build these image URIs at deploy time.tokenURIthen builds a JSONdata: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
==twostrings 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.readFilereturns. The repo pinsimg/*.svgto LF via.gitattributesso the on-chain bytes are deterministic regardless of platform (Windows/core.autocrlfwould otherwise inject CRLF and silently change the encoded art). remappingsinfoundry.tomlmaps@openzeppelin/contracts/...→lib/openzeppelin-contracts/contracts.- IPFS pinning:
Interactions.s.solcontains both IPFS CIDs and Pinata gateway URLs as examples. Prefer the rawipfs://<CID>form in production so the asset resolves independent of any single gateway.