Skip to content
LitVM AcademyStart
Builder track

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:

  1. 01

    The chain

    Stores an owner for each id, and a tokenURI function. That is all it guarantees.

  2. 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.

  3. 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.

  1. 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.

  2. 02

    Compile with 0.8.20 or newer

    First compile takes a few seconds longer while Remix downloads the OpenZeppelin sources.

  3. 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.

  4. 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.

  5. 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:

FieldType thisWhat it is
name_LitVM Academy ProofCollection name a wallet shows. Spaces are fine.
symbol_LVAPShort 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.

  • ownerOf with 1 returns your address. That single answer is what ownership is on this chain.
  • balanceOf with your address returns 1 — the function still exists on ERC-721, it just counts tokens instead of amounts.
  • nextTokenId reads 2, because 1 is taken. Press the orange mint button and you get token 2, in a second transaction this time.
  • ownerOf with 2 reverts 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:

  1. 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.

  2. 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.

  3. 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);
    }
}
Open RemixCreate a new file named SimpleNFT.sol, paste in the contract above, then compile it with Solidity 0.8.20 or newer and deploy it from Remix.Open

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.