height 23776 of 23776 on the network. 9 peer(s) connected, 77 in its peer directory; 1072 block(s) followed and 894 pulled in by sync since this follower started. Caught up: this follower holds every block any peer has claimed, so every panel below is the whole chain. Genesis 1cf8655125cc1bbb…, producers genesis.
What this follower is, locally
Following 7 seed(s) — their addresses are not published here, for the reason the peer table gives. Its store is held under its own Cerberus lease — no other node’s store is opened, touched or stopped.
ZK13NFT
- Status
- template — not deployed A template is a draft, not a deployed contract. Nothing here is on the chain: it has no address until somebody deploys it, and where it lands depends on who deploys it and with which salt. Never send value to a template.
- File
templates/zk13-nft.ares- Profile
- ZK13 non-fungible, the reference collection
- Holds
- its own ownership map
- The property that matters
- metadata is written at mint and no function changes it
- Compiler in force
areslang-1.3.0- Bytecode
- 3,453 bytes
- Bytecode SHA-256
eec491a197be1282ade029e393f078e67efb2e7d6132c98673714b0403b44611- Source SHA-256
43250b04b64b10b33d5a96ee9c8dfcf860ff137811bde9e1b35687de5e5772ae- Published vector
- matches the published vector spec/conformance-vectors.json pins eec491a197be1282… under areslang-1.3.0
- Catalogue
- every template
Before you deploy
The header marks these for editing before you deploy. The values below are the examples it ships with; deployed unedited, they are what every holder sees for the life of the contract.
| Constant | Type | Shipped as |
|---|---|---|
collectionName | string | "Example ZK13 Collection" |
collectionSymbol | string | "EZC" |
collectionUri | string | "" |
maxTokens | uint | 10000 |
maxUriLength | uint | 512 |
Where your deployment would land
Name a deployer to see where its deployment of this template, unedited, would land.
The chain derives a contract’s address from its name, its bytecode, the deployer and the salt. This is that function over the bytecode above, so it holds only for the template exactly as shown and only while the chain runs the same compiler. Edit one constant and the address moves, which is correct: different code is a different contract. Nothing is at this address until somebody deploys there.
Measured gas
What the STARW VM charged for each call on a real chain, under areslang-1.3.0, and asserted exactly by the catalogue test: a figure that moved would fail the build. Measured, not estimated. A call that stores a string costs more for a longer one; those were measured with ipfs://QmExampleTokenMetadataDocument01.
| Entry point | Gas |
|---|---|
| transferToken | 2,373 |
| burnToken | 2,081 |
| mint | 2,063 |
| setMinter | 471 |
| closeMinting | 470 |
| setPaused | 363 |
- Declare at least
- 23,730 ten times the most expensive call; post-mining covers only a tenth of the declared limit
ABI
| Kind | Name | Inputs | Outputs | Mutability |
|---|---|---|---|---|
| function | standard | string | view | |
| function | name | string | view | |
| function | symbol | string | view | |
| function | contractUri | string | view | |
| function | ownerOf | tokenId: uint | address | view |
| function | tokenUri | tokenId: uint | string | view |
| function | balanceOf | account: address | uint | view |
| function | tokenOfOwnerByIndex | account: address, index: uint | uint | view |
| function | totalMinted | uint | view | |
| function | totalBurned | uint | view | |
| function | totalSupply | uint | view | |
| function | maxSupply | uint | view | |
| function | exists | tokenId: uint | bool | view |
| function | owner | address | view | |
| function | minter | address | view | |
| function | paused | bool | view | |
| function | pendingOwnerIs | address | view | |
| function | mint | to: address, uri: string | uint | nonpayable |
| function | transferToken | to: address, tokenId: uint | bool | nonpayable |
| function | burnToken | tokenId: uint | bool | nonpayable |
| function | proposeOwner | next: address | nonpayable | |
| function | acceptOwnership | nonpayable | ||
| function | setPaused | next: bool | nonpayable | |
| function | setMinter | next: address | nonpayable | |
| function | closeMinting | nonpayable | ||
| event | Transfer | from: address, to: address, tokenId: uint | ||
| event | TokenUri | tokenId: uint, uri: string | ||
| event | OwnerProposed | current: address, pending: address | ||
| event | OwnerChanged | previous: address, next: address | ||
| event | PausedChanged | paused: bool, by: address | ||
| event | MinterChanged | previous: address, next: address |
From the command line
Print the source to a file, edit the marked constants, dry-run it to see the digest and your address, then deploy. Only the last command signs, and it is the only one that needs your key and a node.
strblk contract template zk13-nft > zk13-nft.ares strblk contract template zk13-nft --info strblk wallet deploy zk13-nft.ares --dry-run --address <your address> strblk wallet deploy zk13-nft.ares --node <your node's RPC>
Source
templates/zk13-nft.ares, as embedded in this build
// =========================================================================================
// ZK13NFT — the reference non-fungible collection for SourceLess.
//
// WHAT IT IS FOR
// One contract, many distinct tokens, each owned by exactly one address, each carrying its
// own metadata pointer. This is the reference implementation of docs/ZK13_NFT_STANDARD.md:
// every view, every mutating function, every event and every `require` message in that
// document is here, spelled the way the standard spells it, so a wallet can recognise a
// refusal without parsing prose.
//
// HOW IT DIFFERS FROM nft-collection.ares, WHICH IS ALSO IN THIS CATALOGUE
// `nft-collection.ares` is the minimal object: ownership and a count, no metadata and no
// enumeration. This one is the STANDARD: it adds a per-token metadata pointer fixed at mint,
// and it adds `tokenOfOwnerByIndex`, so a holder's tokens can be listed from chain state
// rather than reconstructed from events by an indexer. Both are conforming; this one costs
// more gas per move (MEASURED below) and answers more questions. Pick the minimal one if
// your tokens have no metadata and nobody needs to enumerate a holder.
//
// THIS IS NOT A DOMAIN WNFT, AND IT CANNOT BE MADE INTO ONE
// The native WNFT is one per STR.domain, minted by the domain registry, backed by 1000 STR
// locked, and NON-TRANSFERABLE, because transferring it would transfer an identity. Nothing
// here is any of that. A DOMAIN-BOUND NFT IS NOT WRITABLE TODAY: `AresHost` exposes
// selfAddress, blockTimestampMs, blockHeight, balanceOf and transfer and no domain accessor,
// so a contract cannot ask whether an address holds a proved STR.domain. Storing a domain
// string and calling it a binding verifies nothing — and case folding (STR.Treasury beside
// STR.treasury) is an impersonation the contract cannot even detect, because
// `normalizeStrDomain` is not reachable from AresLang. This template therefore stores no
// domain at all. Do not use "WNFT" in your collection name, symbol or marketing.
//
// WHAT IT GUARANTEES
// * Only the holder of a token can move it. `transferToken` reads `tokenOwner[id]` and
// compares it to `caller`; there is no `from` argument, no approval and no operator, so
// there is no second authority that could move a token.
// * An id is minted at most once, and ids are assigned by the contract in sequence. No
// function writes an owner for an id that was never minted.
// * A token's metadata pointer is written once, at mint, and NO FUNCTION CHANGES IT. There
// is no `setTokenUri` here, and adding one is the most common way a collection becomes a
// rug: the art is replaced after the sale. What this cannot promise is that the bytes the
// pointer names stay the same — that is a property of whatever serves the URI, not of this
// chain. An immutable pointer to mutable content is still mutable content; point it at a
// content-addressed location if you want the content fixed too.
// * `tokenOfOwnerByIndex` is exact and costs the same at any collection size. The holder's
// list is maintained by swapping the last entry into the vacated slot, which is a fixed
// number of map writes — it never iterates, so no holder can grow a position into one that
// is too expensive to move.
// * The owner cannot take a token from a holder. There is no such function. The owner can
// pass the minter role, close minting, pause, and hand over ownership in two steps.
// * An emergency stop that freezes every write and no view, so a stopped collection stays
// auditable at the moment somebody needs to audit it.
// * A revert leaves the collection byte-identical. Storage commits only on success.
//
// WHAT IT DELIBERATELY DOES NOT HAVE
// No approve, no operator, no transferFrom. Those exist so another CONTRACT can move a
// token on your behalf, and AresLang has no cross-contract calls: they would be stored
// permissions nothing could use, waiting to become live on the day a call opcode ships.
// No royalty declaration. A royalty is enforced by a marketplace, a marketplace is another
// contract, and this chain has no cross-contract calls — so a `royaltyBps()` here would be a
// number nothing reads and nothing obeys. The standard says that in its own words rather
// than shipping a field that looks like a right.
// No `tokenByIndex`. Ids ARE the index: they run 1..totalMinted() with no gaps, because
// `mint` assigns them in sequence and `burnToken` does not renumber. `ownerOf(id) == ""` is
// how a burned id reads.
//
// HOW IT IS TYPICALLY MISUSED
// * Sending a token to a contract address. A contract cannot be `caller`, so a token owned
// by a contract can never move again — PERMANENT LOSS. This template refuses the one case
// an address check can detect (`to != self`) and cannot detect any other, because an
// address gives no hint of what is behind it.
// * Treating `ownerOf` as a person, or as a verified user. Holding is not identity.
// * Expecting the gas meter to stop a long URI. It will not, and this is the one number in
// this file that surprises people: the VM prices a storage write by the SLOT NAME it has
// to build, not by the value it stores, so minting with a 60,000-character URI costs the
// same 2,054 gas as minting with a 41-character one — while adding 60,000 characters to
// state that every node keeps forever. `maxUriLength` is therefore a policy bound the
// contract enforces because the meter does not. 512 is a decision, not a machine limit;
// the machine's limit is 65,535 UTF-16 code units.
// * Under-declaring the gas limit on the call record. Post-mining covers only
// min(gasUsed, gasLimit / 10), so the network silently absorbs the rest, forever, on every
// call. MEASURED, on the v2 gas table, with 53-character addresses and a 38-character
// metadata URI, whose length the mint cost rises with: mint 2,063,
// transferToken 2,373, burnToken 2,081, proposeOwner 412, acceptOwnership 612,
// setPaused 363, setMinter 471, closeMinting 470. Declare a gas limit of at least 24,000.
//
// TO MAKE THIS YOUR COLLECTION
// Edit the five constants below and nothing else. Changing them changes the bytecode and
// therefore the deployed address, which is correct: a different collection is a different
// contract. The deployer is owner and minter, so the first call after deployment is
// `mint(holder, "https://...")`.
// =========================================================================================
contract ZK13NFT {
// ---- edit these five -----------------------------------------------------------------
state collectionName: string = "Example ZK13 Collection";
state collectionSymbol: string = "EZC";
// Collection-level metadata, for a wallet rendering the collection itself rather than one
// token. Set once here; there is no setter, for the same reason there is no `setTokenUri`.
state collectionUri: string = "";
// 0 means uncapped. A cap is the only way "there will only ever be N of these" is a fact
// rather than a promise, because `closeMinting` depends on the owner still being reachable
// and a cap does not.
state maxTokens: uint = 10000;
// A policy bound on the metadata pointer, in UTF-16 code units. Not a machine limit — see
// the header. Every URI is stored forever in state every node keeps.
state maxUriLength: uint = 512;
// -------------------------------------------------------------------------------------
// `caller` inside a state initializer is the DEPLOYER: Sindri runs the compiler-generated
// initializer with the authenticated deployer as caller. That is why this collection needs
// no `claimOwnership` call and has no window in which a stranger could claim it.
state contractOwner: address = caller;
state pendingOwner: address = "";
state mintAuthority: address = caller;
state frozen: bool = false;
// Ids are handed out from here and never reused. `mintedCount` is therefore both the count
// of ids ever issued and the highest id in existence, which is what makes the bounds check
// in every mutating function a single comparison.
state mintedCount: uint = 0;
state burnedCount: uint = 0;
state tokenOwner: map<uint, address>;
// Written once, at mint. Survives a burn deliberately: the record of what an id WAS is
// worth keeping, and `ownerOf(id) == ""` is the authoritative answer to whether it exists.
state tokenUriOf: map<uint, string>;
// The holder's list, and the inverse of it. `ownedAt[holder][i]` is the i-th token that
// holder owns, for i in 0..ownedCount[holder]-1; `slotOf[id]` is the i at which `id` sits in
// its current owner's list. The pair is what makes removal O(1): the last entry is swapped
// into the vacated slot and the tail is cleared, so no function here ever walks a list whose
// length a holder controls. A contract that iterated instead would become uncallable at a
// size discovered in production, and the code is immutable.
state ownedCount: map<address, uint>;
state ownedAt: map<address, map<uint, uint>>;
state slotOf: map<uint, uint>;
// Transfer is the SINGLE record of every ownership movement and there is no second one: a
// mint is Transfer("", to, id) and a burn is Transfer(from, "", id). An indexer replaying
// Transfer must arrive at exactly the owners in storage.
event Transfer(from: address, to: address, tokenId: uint);
// Metadata, not movement, so it double-counts nothing for an indexer summing Transfer.
// Emitted once, at mint, because the pointer is written once and never changes.
event TokenUri(tokenId: uint, uri: string);
event OwnerProposed(current: address, pending: address);
event OwnerChanged(previous: address, next: address);
event PausedChanged(paused: bool, by: address);
event MinterChanged(previous: address, next: address);
// ---- views ----------------------------------------------------------------------------
// Which standard this contract was written against. It is a SELF-DECLARATION and proves
// nothing — any contract can return any string — but it tells a client which set of rules
// the issuer INTENDED, which the function catalogue alone cannot when two standards' required
// sets overlap. A client that needs certainty checks `catalog(address)` against the
// standard's required surface; this is the cheap first question, not the answer.
view fn standard() -> string {
return "zk13-nft/1";
}
view fn name() -> string {
return collectionName;
}
view fn symbol() -> string {
return collectionSymbol;
}
view fn contractUri() -> string {
return collectionUri;
}
// "" means the id was never minted, or was burned. Those two are distinguishable: an id
// above `totalMinted()` was never minted, one at or below it was burned.
view fn ownerOf(tokenId: uint) -> address {
return tokenOwner[tokenId];
}
view fn tokenUri(tokenId: uint) -> string {
return tokenUriOf[tokenId];
}
view fn balanceOf(account: address) -> uint {
return ownedCount[account];
}
// The i-th token of a holder, in no promised order: a transfer or a burn moves the last
// entry into the vacated slot, so positions shift. What IS promised is that the indices
// 0..balanceOf(account)-1 enumerate exactly that holder's tokens, each once, at any instant.
// A client paging through them while the holder is moving tokens can see one twice or miss
// one, exactly as it can on any chain; read them within one block or accept that.
view fn tokenOfOwnerByIndex(account: address, index: uint) -> uint {
require(index < ownedCount[account], "no such index");
return ownedAt[account][index];
}
view fn totalMinted() -> uint {
return mintedCount;
}
view fn totalBurned() -> uint {
return burnedCount;
}
// How many exist right now. `totalMinted` never falls, so this is the one to show a buyer.
view fn totalSupply() -> uint {
return mintedCount - burnedCount;
}
view fn maxSupply() -> uint {
return maxTokens;
}
view fn exists(tokenId: uint) -> bool {
return len(tokenOwner[tokenId]) > 0;
}
view fn owner() -> address {
return contractOwner;
}
// "" means minting is closed permanently.
view fn minter() -> address {
return mintAuthority;
}
// Required even on a collection that never pauses. A missing function is indistinguishable,
// to a wallet, from a contract whose answer is inconvenient.
view fn paused() -> bool {
return frozen;
}
view fn pendingOwnerIs() -> address {
return pendingOwner;
}
// ---- movement ---------------------------------------------------------------------------
fn mint(to: address, uri: string) -> uint {
require(frozen == false, "paused");
require(len(mintAuthority) > 0, "mint closed");
require(caller == mintAuthority, "not minter");
// Unreachable through a call record — `validateArgs` checksum-validates every `address`
// argument before the first opcode — and kept because it is the standard's rule and
// because a future caller path that does not run it would need the guard.
require(len(to) > 0, "empty recipient");
// A position at this contract's own address could never move again: a contract cannot be
// `caller`, and the value ledger refuses a contract that pays itself. Every `transfer` in
// this catalogue has refused this since it was written; the ISSUANCE path did not, so the
// guard sat on the call a holder makes and was absent from the call an operator makes.
// MEASURED 2026-10-07: minting here succeeded and produced a position nothing can spend.
require(to != self, "self is not payable");
require(len(uri) <= maxUriLength, "uri too long");
if (maxTokens > 0) {
require(mintedCount < maxTokens, "supply cap");
}
let id = mintedCount + 1;
mintedCount = id;
tokenOwner[id] = to;
tokenUriOf[id] = uri;
// Append to the recipient's list: the new token takes the slot one past the end.
let slot = ownedCount[to];
ownedAt[to][slot] = id;
slotOf[id] = slot;
ownedCount[to] = slot + 1;
emit Transfer("", to, id);
emit TokenUri(id, uri);
return id;
}
fn transferToken(to: address, tokenId: uint) -> bool {
require(frozen == false, "paused");
require(len(to) > 0, "empty recipient");
require(tokenId > 0 && tokenId <= mintedCount, "no such token");
// The whole security property of this contract is on this line.
require(tokenOwner[tokenId] == caller, "not the token owner");
require(to != caller, "self transfer");
// A token at this contract's own address could never move again, because a contract
// cannot be `caller`. It is the one permanent-loss case an address check can detect.
require(to != self, "self is not payable");
tokenOwner[tokenId] = to;
// Remove from the sender's list. The last entry is swapped into the vacated slot and the
// tail is cleared, which is correct in the degenerate case too: when the token being moved
// IS the last entry, `moved` is `tokenId`, the swap writes it back to its own slot, and
// the clear that follows removes it. Fixed cost, no iteration, no hole in the list.
let vacated = slotOf[tokenId];
let last = ownedCount[caller] - 1;
let moved = ownedAt[caller][last];
ownedAt[caller][vacated] = moved;
slotOf[moved] = vacated;
ownedAt[caller][last] = 0;
ownedCount[caller] = last;
// Append to the recipient's list. This has to come after the removal: a holder moving a
// token to themselves would have the two halves fight, which is why `to != caller` is
// refused above rather than handled here.
let slot = ownedCount[to];
ownedAt[to][slot] = tokenId;
slotOf[tokenId] = slot;
ownedCount[to] = slot + 1;
emit Transfer(caller, to, tokenId);
return true;
}
// The holder destroys their own token and nobody else's. There is no authority in this
// contract by which anyone could burn somebody else's, and inventing one is confiscation.
fn burnToken(tokenId: uint) -> bool {
require(frozen == false, "paused");
require(tokenId > 0 && tokenId <= mintedCount, "no such token");
require(tokenOwner[tokenId] == caller, "not the token owner");
tokenOwner[tokenId] = "";
let vacated = slotOf[tokenId];
let last = ownedCount[caller] - 1;
let moved = ownedAt[caller][last];
ownedAt[caller][vacated] = moved;
slotOf[moved] = vacated;
ownedAt[caller][last] = 0;
// After the swap, so that burning the last entry clears its own slot rather than the slot
// of whatever was moved on top of it.
slotOf[tokenId] = 0;
ownedCount[caller] = last;
burnedCount = burnedCount + 1;
emit Transfer(caller, "", tokenId);
return true;
}
// ---- powers the deployer keeps, all of them visible in the ABI --------------------------
fn proposeOwner(next: address) {
require(caller == contractOwner, "not owner");
pendingOwner = next;
emit OwnerProposed(contractOwner, next);
}
// Two steps, because a one-step handover to a mistyped address is permanent: there is no
// administrator anywhere in SourceLess who can reach into a contract's slots.
fn acceptOwnership() {
require(caller == pendingOwner, "not the pending owner");
emit OwnerChanged(contractOwner, caller);
contractOwner = caller;
pendingOwner = "";
}
fn setPaused(next: bool) {
require(caller == contractOwner, "not owner");
frozen = next;
emit PausedChanged(next, caller);
}
fn setMinter(next: address) {
require(caller == contractOwner, "not owner");
require(len(mintAuthority) > 0, "mint closed");
emit MinterChanged(mintAuthority, next);
mintAuthority = next;
}
// One-way. This is the function that lets a collection PROVE it is complete rather than
// promise it. There is no argument because an `address` argument cannot be "": the ABI
// check refuses it before the first opcode.
fn closeMinting() {
require(caller == contractOwner, "not owner");
require(len(mintAuthority) > 0, "mint closed");
emit MinterChanged(mintAuthority, "");
mintAuthority = "";
}
}