# 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/ ETHERSCAN_API_KEY= SEPOLIA_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": "", "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, ``` - `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://` 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)