16 — NFT in 5 Lines

V0.9 introduces the nft construct — a top-level keyword that auto-synthesizes the entire ERC-721 surface from a metadata-only declaration. Where Solidity needs ~150 lines and a base contract import, Covenant needs five lines.

nft CoolApes {
    name: "Cool Apes"
    symbol: "APE"
    base_uri: "https://api.example.com/"
}

That’s the entire contract. It compiles to deployable EVM bytecode.

What gets synthesized

From those five lines the compiler emits:

SurfaceMembers
Fieldsowners, balances, token_approvals, operator_approvals
Viewsname, symbol, tokenURI, balanceOf, ownerOf, getApproved, isApprovedForAll
Actionsapprove, setApprovalForAll, transferFrom, mint, burn
EventsTransfer, Approval, ApprovalForAll (all properly indexed per ERC-721)
ErrorsNotTokenOwner, TokenAlreadyMinted, TokenDoesNotExist, NotApprovedOrOwner, InvalidReceiver

Every member matches the ERC-721 normative spec. The bytecode is a drop-in replacement for an OpenZeppelin-based ERC-721 — wallets, marketplaces, and indexers see the same interface.

What to notice

  • No inheritance. Covenant has no inheritance system. The nft keyword itself does the auto-synthesis at compile time. There is no parent contract to import or override.
  • Metadata is constant by default. name, symbol, and base_uri become immutable values baked into bytecode. To make them mutable, declare them as field instead.
  • Mint is open by default. Add only deployer to the synthesized mint action if you want only the deployer to mint:
    nft CoolApes {
        name: "Cool Apes"
        symbol: "APE"
        base_uri: "https://api.example.com/"
        mint: only deployer    -- or: only(some_minter_role)
    }
  • tokenURI(id) returns base_uri verbatim in V0.9.0. ID-concatenation (base_uri + id.toString()) lands in V0.9.x alongside text-interpolation primitives. For now, point base_uri at an API that handles the concatenation server-side.
  • safeTransferFrom (with the receiver-hook callback) is deferred to V0.9.x — included in the synthesizer’s reserved name list but not emitted yet, to keep the V0.9.0 audit surface minimal.
  • Authorization is enforced (V0.9.2). transferFrom and the synthesized burn require the caller to be the token owner, the per-token approved address, or an approved operator (OpenZeppelin _isAuthorized semantics), and transferFrom reverts on to == address(0) with InvalidReceiver. Earlier (≤ V0.9.1) compiles left these unchecked — see the CHANGELOG. Contracts already deployed under V0.9.0/V0.9.1 keep their original bytecode; only new compiles get the gate.

Custom logic on top

If you need custom mint logic, additional fields, or extra actions, just write them inline. The nft keyword synthesizes the ERC-721 baseline; you add whatever else you need:

nft CoolApes {
    name: "Cool Apes"
    symbol: "APE"
    base_uri: "https://api.example.com/"

    field mint_price: amount
    field minted_count: u256

    action public_mint() {
        given block.value >= mint_price
        given minted_count < 10000
        mint(caller, minted_count + 1)
        minted_count = minted_count + 1
    }
}

Try it

Open this in the playground — it compiles, deploys to MockChain, and lets you mint a token in three clicks.

What’s next