Builder 04 of 06
Your First NFT
Deploy an ERC-721 and mint one to yourself. Ownership is read back off the chain, not taken on trust.
13 min read, then one transaction
An ERC-20 answers "how much does this address have". An ERC-721 answers "who owns this particular one". That single change — from a quantity to an identity — is the whole difference.
// ERC-20
mapping(address => uint256) balanceOf; // address -> amount
// ERC-721
mapping(uint256 => address) ownerOf; // token id -> address
The arrow points the other way. Everything else follows from that.
Non-fungible means the id matters
Fungible means interchangeable. Any 10 of your tokens are the same as any other 10, so a balance is enough to describe what you hold.
Non-fungible means each unit is distinct, so a balance tells you almost nothing.
Knowing that an address holds three tokens from a collection is useless without
knowing which three. That is why the ownership mapping is keyed by token id,
and why transferFrom on an ERC-721 takes an id rather than an amount.
ERC-20
- balanceOf(address) -> amount
- Transfers move a quantity
- Units are interchangeable
- approve grants an amount
ERC-721
- ownerOf(id) -> address
- Transfers move one specific id
- Every unit is distinct
- approve grants one id, or all of them
The image is not on the chain
This surprises people who expect an NFT to be a picture. Here is the entire link between a token and its artwork:
function tokenURI(uint256 tokenId) external view returns (string memory);
A function that returns a string. That string points at a JSON document, and
that JSON has an image field pointing somewhere else again:
- 01
The chain
Stores an owner for each id, and a tokenURI function. That is all it guarantees.
- 02
The metadata
A JSON file with name, description, image, and attributes. Usually on IPFS, sometimes on an ordinary web server, occasionally encoded into the URI itself.
- 03
The image
Another URL inside that JSON. A marketplace follows the whole chain to show you a picture.
So "owning an NFT" means the chain agrees you own a number, and that number resolves — through two hops the chain does not control — to a picture. If either hop is an HTTP URL on somebody's server, the artwork lasts exactly as long as that server does.
The contract you are about to deploy has no artwork at all. It inherits
OpenZeppelin's ERC721 and never sets a base URI, so tokenURI returns an
empty string: the token is real, the ownership is real, and there is simply
nothing for a marketplace to draw. That is legal, and it is the honest starting
point — an ERC-721 is a register of owners, and the picture is a convention
layered on top of it.
ERC-165: asking a contract what it is
The verification for this module calls one function on your contract:
supportsInterface(0x80ac58cd) // -> true if this is an ERC-721
That magic number is the XOR of the function selectors that make up the ERC-721 interface. It is not registered anywhere and not chosen by anyone — it falls out of the standard's own function signatures, so any correct implementation computes the same value.
This is how one contract can safely ask another what it supports before calling
it. A marketplace uses it to decide whether to treat your contract as ERC-721 or
ERC-1155, and it is why inheriting a correct base implementation matters: get
supportsInterface wrong and your token is invisible to everything.
Safe minting, and why it exists
The contract below uses _safeMint rather than _mint. The difference matters:
_mint writes ownership and stops. If the recipient is a contract with no way
to transfer an ERC-721 back out, the token is stuck there forever.
_safeMint calls onERC721Received on a contract recipient and reverts unless
it answers correctly. It is a handshake: prove you know what you are receiving,
or the transfer does not happen.
Deploy it, and own it
The constructor mints token 1 to whoever deploys, so one transaction is the whole exercise: when it confirms, you own an NFT.
- 01
Open SimpleNFT.sol in Remix
Use the button below to open Remix, then create a new file named SimpleNFT.sol and paste in the source above. It imports OpenZeppelin's ERC721, which Remix fetches from npm on its own — the import needs no setup.
- 02
Compile with 0.8.20 or newer
First compile takes a few seconds longer while Remix downloads the OpenZeppelin sources.
- 03
Fill the two constructor fields
Remix shows one input per argument under the Deploy button: name_ and symbol_. Type the values from the table below into them, plain text, no quotes.
- 04
Deploy on chain 4441
Confirm the wallet says LitVM LiteForge, press Deploy and sign. Copy the deployment transaction hash — that is what the task at the end of this lesson asks for.
- 05
You already own token 1
The same transaction that created the contract ran its constructor, and the constructor minted. There is no second step to forget.
The two constructor fields:
| Field | Type this | What it is |
|---|---|---|
name_ | LitVM Academy Proof | Collection name a wallet shows. Spaces are fine. |
symbol_ | LVAP | Short ticker for the collection, by convention 3-5 uppercase letters. |
Neither name is reserved, so use your own if you like.
Where your NFT is
Nothing to press yet — you own token 1 already. Confirm it in the deployed
contract panel in Remix, on the blue buttons. They are view calls: free, and
no signature.
ownerOfwith1returns your address. That single answer is what ownership is on this chain.balanceOfwith your address returns 1 — the function still exists on ERC-721, it just counts tokens instead of amounts.nextTokenIdreads 2, because 1 is taken. Press the orangemintbutton and you get token 2, in a second transaction this time.ownerOfwith2reverts until you do. Token 2 does not exist, and the standard requires a revert rather than the zero address.
Then look at it outside Remix, where the rest of the world sees it:
- 01
The explorer's token page
liteforge.explorer.caldera.xyz/token/<your contract address> lists the collection, its supply and every holder. The address page you were on before shows the contract; this one shows the token.
- 02
The Tokens tab of your own address
Open your address in the explorer and switch to Tokens. Your NFT is listed there against the collection name you chose.
- 03
Your wallet, via Import NFT
Contract address plus token id 1. It appears under the collection name, with an empty picture, because this contract publishes no metadata — that is the contract being honest rather than the wallet failing.
Open the deployment transaction in the explorer and read its log. There is a
Transfer event, emitted by a contract that did not exist when the transaction
started, with from set to the zero address — the same convention the ERC-20
lesson used for minting. Creation, on both standards, is a transfer from nobody.
Send it to somebody
An ERC-721 moves one specific id, so the transfer takes three arguments rather than two. In Remix, on your deployed contract:
safeTransferFrom(from, to, tokenId)
from is your address, to is theirs, tokenId is 1. Remix lists
safeTransferFrom twice, because the standard defines it with and without a
trailing data argument — use the three-field one.
Once it lands, ownerOf(1) returns their address. You created the token, your
address stays in the deployment transaction for good, and none of that gives you
a claim on it any more: ownership is the current row in the mapping, nothing
else.
What to remember
- 01ERC-721 inverts the ERC-20 mapping: token id to owner, rather than owner to amount.
- 02The chain stores an owner and a tokenURI function; by default the artwork is two hops away and outside the chain's guarantees.
- 03A token with no metadata is still a valid token: tokenURI may return nothing, and ownership is unaffected.
- 04supportsInterface lets contracts ask each other what they are, and 0x80ac58cd falls out of the ERC-721 signatures rather than being assigned.
- 05_safeMint refuses to strand a token in a contract that cannot move it, at the cost of handing that contract control mid-transaction.
- 06Minting is a Transfer event from the zero address, on both standards.
Primary sources
SimpleNFT.sol
54 lines, ready to paste · raw file
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// Remix resolves this from npm automatically. Unlike the ERC-20 lesson, this
// one inherits rather than reimplements: ERC-721 has enough edge cases around
// safe transfers and receiver checks that hand-rolling it teaches the wrong
// lesson. Read the standard, inherit the implementation.
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
/// @title SimpleNFT
/// @notice A minimal ERC-721 that mints its first token while it is being
/// deployed, so one transaction leaves you owning an NFT rather than
/// an empty collection.
///
/// Deploy it with a name and a ticker. The address that sends the deployment
/// owns token 1 the moment it confirms. `mint` is there for the second one.
///
/// Deployed in "Your First NFT" at
/// https://litvm-academy.pro/build/your-first-nft
contract SimpleNFT is ERC721 {
/// @notice Id given to the next token minted. Starts at 1, because 0 reads
/// as "no token" in far too much downstream code.
uint256 public nextTokenId = 1;
/// @param name_ Collection name, shown by wallets and explorers.
/// @param symbol_ Short ticker for the collection.
constructor(string memory name_, string memory symbol_)
ERC721(name_, symbol_)
{
_mintNext(msg.sender);
}
/// @notice Mint another token to yourself.
/// @dev No access control at all, so anyone can mint. That is deliberate
/// for a teaching contract and would be a critical bug in a real one.
function mint() external returns (uint256 tokenId) {
return _mintNext(msg.sender);
}
function _mintNext(address to) private returns (uint256 tokenId) {
tokenId = nextTokenId;
// The counter moves before the mint, never after: `_safeMint` hands
// control to a contract recipient, and a half-updated counter is how
// the classic NFT drop exploit gets its second token for free.
nextTokenId = tokenId + 1;
// `_safeMint` rather than `_mint`: it checks that a contract recipient
// knows how to receive an ERC-721, so tokens cannot be sent into a
// contract that has no way to move them again. Deploying from a normal
// wallet always passes that check.
_safeMint(to, tokenId);
}
}
Your first NFT
Paste the hash of the transaction that deployed your ERC-721. The check reads the transaction straight from LiteForge, so it has to be one your own wallet sent.