// ========================================================================================= // 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; state ownedCount: map; // 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; } }