create-eth
Version:
Create a Scaffold-ETH-2 app
81 lines (57 loc) • 3.5 kB
Markdown
---
name: erc-721
description: "Add an ERC-721 NFT contract to a Scaffold-ETH 2 project. Use when the user wants to: create an NFT collection, deploy an ERC-721, add NFT minting, build an NFT gallery or transfer UI, or work with non-fungible tokens in SE-2."
---
# ERC-721 NFT Integration for Scaffold-ETH 2
## Prerequisites
Check if `./packages/nextjs/scaffold.config.ts` exists directly in the current working directory (do not search subdirectories). If it doesn't exist, this is not a Scaffold-ETH 2 project. Follow the instructions at https://docs.scaffoldeth.io/SKILL.md to scaffold it first. If it exists, continue directly with building.
Also read `.agents/skills/openzeppelin/SKILL.md` since ERC-721 contracts use OpenZeppelin, and that skill has critical guidance on reading the installed source for correct import syntax, override points, and constructor patterns.
## Gotchas
Key pitfalls and gotchas to watch for when working with ERC-721.
### 1. `_safeMint` Reentrancy
`_safeMint` and `safeTransferFrom` invoke `onERC721Received()` on the recipient if it's a contract. This is an external call **after** the token has been minted, creating a reentrancy vector.
**Real exploit (HypeBears, Feb 2022):** State updated after `_safeMint` allowed attacker to re-enter and bypass per-address minting limits.
```solidity
// VULNERABLE: state update after _safeMint
function mintNFT() public {
require(!addressMinted[msg.sender], "Already minted");
_safeMint(msg.sender, tokenId);
addressMinted[msg.sender] = true; // too late
}
// SAFE: state update before _safeMint
function mintNFT() public {
require(!addressMinted[msg.sender], "Already minted");
addressMinted[msg.sender] = true; // update first
_safeMint(msg.sender, tokenId);
}
```
### 2. On-Chain SVG Stack-Too-Deep
When generating SVG on-chain, the `tokenURI` or `generateSVG` function easily hits Solidity's 16-local-variable stack limit. Split SVG generation into multiple helper functions (e.g., `_svgBackground`, `_svgShapes`, `_svgText`) rather than building the entire SVG in one function.
### 3. Marketplace Metadata `attributes` Array
The `attributes` array in NFT metadata JSON is not in the ERC-721 EIP but is the de facto standard used by OpenSea, Blur, and every marketplace. Without it, traits won't display:
```json
{
"name": "My NFT #1",
"description": "...",
"image": "data:image/svg+xml;base64,...",
"attributes": [
{ "trait_type": "Color", "value": "Blue" },
{ "trait_type": "Rarity", "value": "Rare" }
]
}
```
### 4. Required Overrides with ERC721Enumerable
When combining `ERC721` + `ERC721Enumerable`, both define `_update` and `_increaseBalance`. You must explicitly override them or the contract won't compile:
```solidity
function _update(address to, uint256 tokenId, address auth) internal override(ERC721, ERC721Enumerable) returns (address) {
return super._update(to, tokenId, auth);
}
function _increaseBalance(address account, uint128 amount) internal override(ERC721, ERC721Enumerable) {
super._increaseBalance(account, amount);
}
function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721Enumerable) returns (bool) {
return super.supportsInterface(interfaceId);
}
```
### 5. IPFS Base URI Trailing Slash
OpenZeppelin's `tokenURI()` concatenates `_baseURI() + tokenId.toString()`. If the base URI is `ipfs://QmCID` without a trailing slash, token 42 becomes `ipfs://QmCID42` instead of `ipfs://QmCID/42`.