ERC-721 NFT Deployment
ERC-721 (finalized 2018 by Entriken, Shirley, Evans, and Sachs) is the Ethereum standard for non-fungible tokens — uniquely identified, individually owned on-chain assets. It ships in three pieces: the core IERC721 (ownership, transfer, approval), the optional IERC721Metadata extension (name, symbol, tokenURI), and the optional IERC721Enumerable extension (iterate the full collection on-chain). It is the interface that lets any wallet or marketplace integrate any NFT contract without custom code.
What actually lives on-chain
A common misconception is that NFTs store the artwork on the blockchain. They almost never do. The contract stores ownership and a tokenURI string; that URI points to a JSON metadata file, which in turn points to an image. Both files live off-chain — on IPFS, Arweave, or a plain web server. If those files disappear, the NFT still exists on-chain but renders no picture. A typical OpenSea-schema metadata file:
{
"name": "Sample NFT #1",
"description": "An example NFT.",
"image": "ipfs://QmYourImageCID/1.png",
"external_url": "https://yoursite.com/nft/1",
"attributes": [ { "trait_type": "Background", "value": "Blue" } ]
}EIP-721 does not forbid mutable or non-permanent URIs, so a self-hosted metadata endpoint is valid when you want to update the image arbitrarily (at the cost of permanence).
The standard workflow
- Generate the contract with the OpenZeppelin Contracts Wizard: pick
ERC721, set name/symbol, and enable features like Mintable + Auto-increment IDs, URI Storage, Enumerable, and Ownable access control. The Wizard emits audited Solidity you can open directly in Remix. - Compile and deploy in Remix IDE (browser-based Solidity IDE). Choose the compiler matching the contract’s
pragma, then Deploy & Run Transactions → Environment: Injected Web3 so your browser wallet (e.g. MetaMask) signs the deployment transaction. - Test on a testnet first — Sepolia (or the deprecated Goerli). Get test ETH from a faucet, deploy, and exercise the contract for free before spending real gas on mainnet.
- Pin metadata to IPFS (via Pinata, NFT.Storage, web3.storage, Filebase) or host it yourself; note the
ipfs://<cid>orhttps://URI. - Mint via the contract’s
safeMint(to, uri)(ormint(address to, uint256 tokenId, string uri)depending on options) from Remix’s Deployed Contracts panel — passing the recipient address and the metadata URI. Confirm withtokenURI(tokenId). - View/transfer on a marketplace (OpenSea / testnets.opensea.io) which picks up minted collections automatically, or import into a wallet by contract address + token ID.
A canonical Wizard-style contract skeleton:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract NFTAvatar is ERC721, ERC721Enumerable, ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIdCounter;
constructor() ERC721("NFTAvatar", "NFTAVATAR") {}
function safeMint(address to, string memory uri) public onlyOwner {
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(to, tokenId);
_setTokenURI(tokenId, uri);
}
// ...required overrides: _beforeTokenTransfer, _burn, tokenURI, supportsInterface
}safeMint takes two arguments — the recipient address and the metadata URI (Remix’s chevron expands them into separate fields). tokenURI(tokenId) returns the metadata URI for an existing token.
Toolchain and environment gotchas
- Wallet/browser coupling to Remix is fragile. Remix’s deploy path depends on the injected wallet provider; some browser+wallet combinations compile fine but fail at deploy time with an opaque “Transaction Failed” (noted with Safari + certain extension wallets, and under iOS/iPadOS lockdown mode). A Chromium-based browser with a built-in or well-supported wallet (e.g. Brave Wallet, MetaMask) is the reliable deployment path — even if every other step works fine elsewhere.
- Lockdown mode (iPadOS/macOS) must be disabled for the Remix site to compile.
- Testnet vs mainnet: Goerli was the historical testnet (with Alchemy’s goerlifaucet); it has been deprecated in favor of Sepolia — the workflow is identical, only the network and faucet change.
- Record deployment metadata — Solidity compiler version, deploying wallet address, and contract address — so you can reconnect to the contract in Remix later to mint more tokens.
- Wallet display lag: freshly minted NFTs can take a long time to appear in wallet UIs (e.g. MetaMask) even though they show up on marketplaces immediately; transfers can be done from the marketplace interface.
- Cost: a typical ERC-721 deploy on Ethereum mainnet runs roughly 200 (contract size + gas price dependent), each mint 50; Layer-2s (Base, Arbitrum) drop both to cents; testnets are free.
Adjacent pieces
- ENS and multisig wallets (e.g. Safe) can hold the deploying identity and avatar records; an avatar URL set at the ENS level may not surface consistently across all consoles.
- The next standards up are ERC-1155 (multi-token: fungible + non-fungible in one contract) and ERC-20 (fungible tokens) — all use the same Remix/OpenZeppelin workflow.