CI / Foundry project (push) Has been cancelled
- 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.
166 lines
6.8 KiB
Markdown
166 lines
6.8 KiB
Markdown
# 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](#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
|
|
|
|
```bash
|
|
curl -L https://foundry.paradigm.xyz | bash
|
|
foundryup
|
|
```
|
|
|
|
### 2. Install dependencies (git submodules)
|
|
|
|
```bash
|
|
git submodule update --init --recursive
|
|
```
|
|
|
|
### 3. Build & test
|
|
|
|
```bash
|
|
forge build
|
|
forge test
|
|
```
|
|
|
|
The test suite runs 6 tests across 4 suites and should pass green.
|
|
|
|
---
|
|
|
|
## Run against a local Anvil node
|
|
|
|
```bash
|
|
# 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):
|
|
|
|
```bash
|
|
SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/<YOUR_KEY>
|
|
ETHERSCAN_API_KEY=<YOUR_KEY>
|
|
SEPOLIA_PRIVATE_KEY=<YOUR_PRIVATE_KEY>
|
|
```
|
|
|
|
Then:
|
|
|
|
```bash
|
|
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:
|
|
```json
|
|
{
|
|
"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 `string`s in Solidity. Compare their hashes instead:
|
|
```solidity
|
|
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
|
|
|
|
- [ERC-721 standard (EIP-721)](https://eips.ethereum.org/EIPS/eip-721)
|
|
- [Foundry book](https://book.getfoundry.sh/)
|
|
- [OpenZeppelin Contracts](https://github.com/OpenZeppelin/openzeppelin-contracts)
|