height 23717 of 23717 on the network. 9 peer(s) connected, 77 in its peer directory; 1013 block(s) followed and 842 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.
NFTCollection for your fork
- Status
- a draft, not deployed nothing here is on any chain; never send value to a draft
- Profile
- NFT collection
- Holds
- its own ownership map
- The property that matters
- only the holder of a token can move it
- On Asgard
- the same template in the catalogue
Edit the draft
Set the constants the template marks for editing, and name a deployer to see where it would land.
The draft as shipped
- Contract
NFTCollection- Compiler
areslang-1.3.0- Bytecode SHA-256
1b891ed5ebdcfc068929f09855b52f5671ad161dd6dfee6c87dedc0b9fb94afa- Bytecode
- 2,327 bytes
- Source SHA-256
4ea6f77f21aaf07b8dd0c2c2fe9bde21456c6f57b7735ba5b6d5ca8bfe0bd2f6- The catalogue text?
- Yes, byte for byte: matches the published vector
- Ready to deploy?
- not yet a deployment refuses while these remain: collectionName = "Example Collection", collectionSymbol = "EXC"
- Would land at
- name a deployer to see it
Deploy it to your fork
Only the last step signs, and only it reaches a node.
- Save the draft. Download it above, or start from the catalogue text with
strblk contract template nft-collection > nft-collection.aresand edit the marked constants yourself. - Dry-run it on your machine.
strblk wallet deploy nft-collection.ares --dry-run --address <your address on the fork>compiles it locally and prints its digest and address. They must match this page; if they do not, the file is not this draft. - Find your fork's RPC. It is the
rpc (Iris, wallets)line of the fork's banner, and therpcUrlof each node in<data root>/fork.json. - Deploy it.
strblk wallet deploy nft-collection.ares --node <your fork's RPC>, from a wallet whose account holds a proved STR.domain on your fork. Before it signs, the wallet prints the network: the realm reads unregistered and the genesis is your fork's hash. Check that hash; it is what the signature binds.
Measured gas
Charged by the STARW VM under areslang-1.3.0, the same on your fork. Measured, not estimated.
| Entry point | Gas |
|---|---|
| mint | 1,223 |
| transferToken | 1,110 |
| burnToken | 804 |
| setMinter | 471 |
| closeMinting | 470 |
| setPaused | 363 |
- Declare at least
- 12,230 ten times the most expensive call
Functions
| Kind | Name | Inputs | Outputs | Mutability |
|---|---|---|---|---|
| function | mint | to: address | 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 | ||
| function | name | string | view | |
| function | symbol | string | view | |
| function | ownerOf | tokenId: uint | address | view |
| function | balanceOf | account: address | uint | view |
| function | totalMinted | uint | view | |
| function | maxSupply | uint | view | |
| function | owner | address | view | |
| function | minter | address | view | |
| function | paused | bool | view | |
| event | Transfer | from: address, to: address, tokenId: uint | ||
| event | OwnerProposed | current: address, pending: address | ||
| event | OwnerChanged | previous: address, next: address | ||
| event | PausedChanged | paused: bool, by: address | ||
| event | MinterChanged | previous: address, next: address |
Your draft's source
templates/nft-collection.ares, as shipped
// =========================================================================================
// NFTCollection — a non-fungible collection whose tokens are owned by an address.
//
// THIS IS NOT A DOMAIN WNFT, AND IT CANNOT BE MADE INTO ONE
// Read this before you name your collection. The native WNFT is one per STR.domain, minted
// by the domain registry, backed by 1000 STR locked, and NON-TRANSFERABLE — because a
// domain WNFT is bound to its holder and transferring it would transfer an identity.
// Nothing in this contract is any of that. It is a different object with a similar name,
// and a template that lets a user believe they are minting a domain WNFT is the most
// dangerous mislabelling available on this chain. Do not use "WNFT" in your collection
// name, symbol or marketing.
//
// A DOMAIN-BOUND NFT — one whose ownership is tied to a proved STR.domain rather than to a
// bare address — IS NOT WRITABLE TODAY. `AresHost` exposes selfAddress, blockTimestampMs,
// blockHeight, balanceOf and transfer, and no domain accessor at all: a contract cannot ask
// whether an address holds a proved domain or which domain it holds. Storing a domain
// string in the token and treating it as proof verifies nothing — a string is a string, and
// case folding (STR.Treasury beside STR.treasury) is an impersonation primitive the
// contract cannot even detect, because `normalizeStrDomain` is not reachable from AresLang.
// This template therefore stores no domain at all. That is the honest version.
//
// 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 and no operator or approval
// mechanism, so there is no second authority that could move a token.
// * An id is minted at most once. Ids are assigned by the contract, in sequence, and no
// function writes an owner for an id that was not minted.
// * The supply cap is fixed at deployment and no function raises it.
// * Minting closes permanently with `closeMinting`, one-way, so "the collection is
// complete" can be a fact and not a promise.
// * The contract owner cannot take a token from a holder. There is no such function; the
// owner can only pass the minter role, close minting, pause, and hand over ownership.
// * There is an emergency stop, and it is declared. `setPaused` freezes mint, transferToken
// and burnToken; it freezes no view. A pause moves nothing and destroys nothing — every
// token stays with its holder, readable in `ownerOf`, and moves again when the owner
// unpauses. `paused()` is in the ABI so a buyer can read the state before committing.
//
// 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, so they would be stored
// permissions nothing could use — waiting to become live on the day a call opcode ships.
// No enumeration of a holder's tokens. That would be iteration over caller-controlled data,
// which is how a contract becomes permanently uncallable at a size discovered in
// production. `ownedCount` is a counter; an indexer builds the list from Transfer events.
//
// HOW IT IS TYPICALLY MISUSED
// * Transferring a token to a contract address. A contract cannot be `caller`, so a token
// owned by a contract can never move again. PERMANENT LOSS, with no recovery path, and
// this contract cannot detect the case to refuse it.
// * Treating `ownerOf` as a person. Holding is not identity — identity is the WNFT, is
// non-transferable, and is minted only by the domain registry.
// * Ignoring who holds the pause. It is the contract owner, there is no `renounceOwnership`,
// and the code is immutable — so an owner who pauses and then loses the key leaves a
// collection nobody can ever move again. Nothing is confiscated and nothing is destroyed,
// but a permanently paused collection is a permanently unsellable one. Hand ownership to
// a key you will still hold in five years, and read `owner()` before you buy.
// * Under-declaring the gas limit. MEASURED: mint 1,223, transferToken 1,110,
// burnToken 804, setPaused 363, setMinter 471, closeMinting 470. Declare at least 13,000.
// =========================================================================================
contract NFTCollection {
// ---- edit these before deploying -------------------------------------------------------
state collectionName: string = "Example Collection";
state collectionSymbol: string = "EXC";
// Fixed at deployment. 0 would mean uncapped; declare a real cap unless you mean uncapped.
state maxTokens: uint = 10000;
// -------------------------------------------------------------------------------------
state contractOwner: address = caller;
state pendingOwner: address = "";
state mintAuthority: address = caller;
state frozen: bool = false;
state mintedCount: uint = 0;
state tokenOwner: map<uint, address>;
state ownedCount: map<address, uint>;
// A mint is Transfer("", to, id) and a move is Transfer(from, to, id). One event, every
// movement, so an indexer summing Transfer arrives at exactly the owners in storage.
event Transfer(from: address, to: address, tokenId: uint);
event OwnerProposed(current: address, pending: address);
event OwnerChanged(previous: address, next: address);
event PausedChanged(paused: bool, by: address);
event MinterChanged(previous: address, next: address);
fn mint(to: address) -> uint {
require(frozen == false, "paused");
require(len(mintAuthority) > 0, "mint closed");
require(caller == mintAuthority, "not minter");
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");
if (maxTokens > 0) {
require(mintedCount < maxTokens, "supply cap");
}
let id = mintedCount + 1;
mintedCount = id;
tokenOwner[id] = to;
ownedCount[to] = ownedCount[to] + 1;
emit Transfer("", to, id);
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");
tokenOwner[tokenId] = to;
ownedCount[caller] = ownedCount[caller] - 1;
ownedCount[to] = ownedCount[to] + 1;
emit Transfer(caller, to, tokenId);
return true;
}
// The holder destroys their own token and nobody else's.
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] = "";
ownedCount[caller] = ownedCount[caller] - 1;
emit Transfer(caller, "", tokenId);
return true;
}
fn proposeOwner(next: address) {
require(caller == contractOwner, "not owner");
pendingOwner = next;
emit OwnerProposed(contractOwner, next);
}
fn acceptOwnership() {
require(caller == pendingOwner, "not the pending owner");
emit OwnerChanged(contractOwner, caller);
contractOwner = caller;
pendingOwner = "";
}
// The emergency stop the other two asset templates have and this one did not. It covers
// mint, transferToken and burnToken — every function that writes a token — and no view,
// because a stopped collection must stay auditable at exactly the moment somebody needs to
// audit it. It takes nothing from a holder and destroys nothing: a paused token is still
// theirs, in `ownerOf`, and moves again the moment the owner unpauses.
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;
}
fn closeMinting() {
require(caller == contractOwner, "not owner");
require(len(mintAuthority) > 0, "mint closed");
emit MinterChanged(mintAuthority, "");
mintAuthority = "";
}
view fn name() -> string {
return collectionName;
}
view fn symbol() -> string {
return collectionSymbol;
}
// "" means the id was never minted, or was burned.
view fn ownerOf(tokenId: uint) -> address {
return tokenOwner[tokenId];
}
view fn balanceOf(account: address) -> uint {
return ownedCount[account];
}
view fn totalMinted() -> uint {
return mintedCount;
}
view fn maxSupply() -> uint {
return maxTokens;
}
view fn owner() -> address {
return contractOwner;
}
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;
}
}