height 23745 of 23745 on the network. 9 peer(s) connected, 77 in its peer directory; 1041 block(s) followed and 865 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.
ZK13Token for your fork
- Status
- a draft, not deployed nothing here is on any chain; never send value to a draft
- Profile
- ZK13 fungible, the reference token
- Holds
- its own token ledger
- The property that matters
- supply cannot be created; the owner cannot reach a holder’s balance
- 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
ZK13Token- Compiler
areslang-1.3.0- Bytecode SHA-256
afee3ff841e5ca1700cdfe2a0b058397b369213e81a8898cf01e280cabf8236c- Bytecode
- 2,239 bytes
- Source SHA-256
1adee85062a215f6a6cfdb7c24a86126e7b9a93dabd2cddbe6a5ecc3bc77d8a8- The catalogue text?
- Yes, byte for byte: matches the published vector
- Ready to deploy?
- not yet a deployment refuses while these remain: tokenName = "Example ZK13 Token", tokenSymbol = "EZK"
- 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 zk13-token > zk13-token.aresand edit the marked constants yourself. - Dry-run it on your machine.
strblk wallet deploy zk13-token.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 zk13-token.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,144 |
| transfer | 835 |
| burn | 774 |
| acceptOwnership | 612 |
| setMinter | 471 |
| closeMinting | 470 |
| proposeOwner | 412 |
| setPaused | 363 |
- Declare at least
- 11,440 ten times the most expensive call
Functions
| Kind | Name | Inputs | Outputs | Mutability |
|---|---|---|---|---|
| function | name | string | view | |
| function | symbol | string | view | |
| function | decimals | uint | view | |
| function | totalSupply | uint | view | |
| function | maxSupply | uint | view | |
| function | lifetimeIssued | uint | view | |
| function | balanceOf | account: address | uint | view |
| function | owner | address | view | |
| function | minter | address | view | |
| function | paused | bool | view | |
| function | pendingOwnerIs | address | view | |
| function | transfer | to: address, amount: uint | bool | nonpayable |
| function | burn | amount: uint | bool | nonpayable |
| function | mint | to: address, amount: 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, amount: 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/zk13-token.ares, as shipped
// =========================================================================================
// ZK13Token — the reference fungible token for SourceLess.
//
// WHAT IT IS FOR
// A fungible asset whose ledger lives in this contract's own storage, committed through
// Sindri into the chain state root. This is the reference implementation of
// docs/ZK13_TOKEN_STANDARD.md: every view, every mutating function, every event and every
// `require` message in that document is here, spelled exactly as the standard spells it,
// so a wallet can recognise a refusal without parsing prose.
//
// WHAT IT GUARANTEES
// * Supply moves only through `mint` and `burn`. `transfer` moves it by exactly zero.
// * `maxSupply` is set at deployment and no function raises it. It bounds LIFETIME
// ISSUANCE — `lifetimeIssued()`, which only ever goes up — and NOT the circulating
// `totalSupply`, which `burn` lowers. Those are different promises and only the first one
// is keepable: a cap checked against a burnable number is defeated by mint, burn, mint,
// which issues without limit against a cap that never appears to be breached.
// `maxSupply() == 0` means UNCAPPED, which is what this template ships as.
// * Minting closes permanently: `closeMinting` is one-way and `setMinter` refuses to
// reopen it, so "fixed supply" is a promise the code keeps rather than a claim.
// * The owner can pause and can hand over ownership in two steps. The owner CANNOT move
// anybody else's balance: there is no `setBalance`, no `adjust`, no `transferFrom`.
// * A revert leaves the ledger byte-identical. Storage commits only on success.
// * A self-transfer is refused, so the classic debit-then-credit inflation bug cannot be
// reached at all.
//
// HOW IT IS TYPICALLY MISUSED
// * Treating "holds a balance" as "is a verified user". Receiving a ZK13 token is NOT
// gated by STR.domain and cannot be — there is no domain accessor in AresLang. Airdrop
// eligibility, voting weight and revenue shares computed from `balanceOf` are computed
// over an unauthenticated set. Check the registry off chain.
// * Sending to a contract address. A contract cannot be `caller` and there are no
// cross-contract calls, so a balance recorded at a contract address can never move.
// THAT IS A PERMANENT LOSS. This token has no recovery path and cannot be given one.
// * Deploying and then forgetting to `closeMinting`, while telling holders the supply is
// fixed. `minter()` is in the ABI; a holder will read it.
// * 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: mint 1,144, transfer 835, burn 774,
// proposeOwner 412, acceptOwnership 612, setPaused 363, setMinter 471, closeMinting 470.
// Declare a gas limit of at least 12,000.
//
// WHAT IT DELIBERATELY DOES NOT HAVE
// No `approve` and no `transferFrom`. An allowance exists so another CONTRACT can spend on
// a holder's behalf, and AresLang has no cross-contract calls. An allowance nothing can
// spend is a stock of forgotten approvals waiting to become live on the day a call opcode
// ships. See ZK13_TOKEN_STANDARD.md §4.2.
// No upgrade path and no proxy. Bytecode is in this contract's address preimage; there is
// no `setCode` anywhere in Sindri. What you deploy is what holders get, forever.
//
// TO MAKE THIS YOUR TOKEN
// Edit the four constants below and nothing else. Changing them changes the bytecode and
// therefore the deployed address, which is correct: a different token is a different
// contract. Supply starts at zero; the deployer is owner and minter, so the first call
// after deployment is `mint(founderAddress, amount)`.
// =========================================================================================
contract ZK13Token {
// ---- edit these four ----------------------------------------------------------------
state tokenName: string = "Example ZK13 Token";
state tokenSymbol: string = "EZK";
// 13, like all seven native ledgers. A wallet showing two assets at different precision is
// where somebody sends 10^5 times what they meant to.
state tokenDecimals: uint = 13;
// A ceiling on LIFETIME ISSUANCE, not on circulating supply — see the header. 0 means
// uncapped, and must be declared as 0 rather than as a very large number that looks like a
// cap. Immutable once deployed.
//
// SHIPPED AT 0, WHICH IS UNCAPPED. That is deliberate rather than lazy: a template cannot
// invent a cap for a token it knows nothing about, and a plausible-looking placeholder is
// worse than an honest zero because it reads as a decision somebody made. If your holders
// are to be told the supply is bounded, either set this to the real number before you
// deploy — the address changes with it, which is correct — or `closeMinting()` once the
// whole supply is issued, which proves the bound instead of promising it.
state supplyCap: uint = 0;
// -------------------------------------------------------------------------------------
state supply: uint = 0;
// Everything `mint` has ever created, and it never goes down. This is what `supplyCap`
// bounds. `supply` cannot be, because `burn` decrements it: with the cap tested against a
// burnable number, mint -> burn -> mint issues without limit against a fixed cap, and the
// arithmetic never complains. MEASURED on this template before the fix: 4,000 issued
// against a cap of 1,000, in four calls, on a chain that stayed valid.
state issuedEver: uint = 0;
// `caller` inside a state initializer is the DEPLOYER: Sindri runs the compiler-generated
// initializer with the authenticated deployer as caller. That is why this token 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;
state balances: map<address, uint>;
// Transfer is the SINGLE record of every balance movement and there is no second one:
// a mint is Transfer("", to, amount) and a burn is Transfer(from, "", amount). An indexer
// summing Transfer must arrive at exactly the balances in storage.
event Transfer(from: address, to: address, amount: 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);
// ---- views ----------------------------------------------------------------------------
view fn name() -> string {
return tokenName;
}
view fn symbol() -> string {
return tokenSymbol;
}
view fn decimals() -> uint {
return tokenDecimals;
}
view fn totalSupply() -> uint {
return supply;
}
view fn maxSupply() -> uint {
return supplyCap;
}
// Everything ever minted, burned or not. `totalSupply` answers "how much exists"; this
// answers "how much was ever issued", and it is the one the cap is enforced against. A
// holder comparing the two learns exactly how much has been burned.
view fn lifetimeIssued() -> uint {
return issuedEver;
}
view fn balanceOf(account: address) -> uint {
return balances[account];
}
view fn owner() -> address {
return contractOwner;
}
// "" means minting is closed permanently. Required even though this token does mint: a
// holder must be able to ask "who can mint this?" and get an answer from the ABI.
view fn minter() -> address {
return mintAuthority;
}
// Required even though a token might never pause. A missing function is indistinguishable,
// to a wallet, from a token whose answer is inconvenient.
view fn paused() -> bool {
return frozen;
}
view fn pendingOwnerIs() -> address {
return pendingOwner;
}
// ---- movement -------------------------------------------------------------------------
fn transfer(to: address, amount: uint) -> bool {
require(frozen == false, "paused");
// Unreachable through a call record — the ABI check refuses a malformed `address`
// argument before the first opcode runs — and kept because it is the standard's rule and
// because a future caller path that does not run `validateArgs` would need it.
require(len(to) > 0, "empty recipient");
// Measured, not assumed. On this VM the two-statement form below is ALREADY safe for
// to == caller: MSTORE writes through to the working storage, so the credit reads the
// already-debited value and 1000 - 1000 + 1000 is 1000. The inflation appears the moment
// somebody caches both balances in locals before writing either — `let from =
// balances[caller]; let to = balances[to];` — because then the credit overwrites the
// debit and the holder gains `amount` from nothing, callable in a loop, by anyone.
// The guard costs 8 gas and makes that edit harmless. Keep it.
require(to != caller, "self transfer");
// The header documents that a balance recorded at a contract address can never move, per
// standard section 7.3. This is the one case of it the token can cheaply refuse outright.
require(to != self, "self is not payable");
require(balances[caller] >= amount, "insufficient balance");
// uint - uint compiles to the CHECKED subtract, which reverts rather than producing a
// negative that every later comparison would read as "plenty".
balances[caller] = balances[caller] - amount;
balances[to] = balances[to] + amount;
// A zero-amount transfer is permitted and still emits: refusing it breaks batch payers
// that send a list including zeros.
emit Transfer(caller, to, amount);
return true;
}
// The caller burns its OWN balance and no other. With no allowance mechanism there is no
// authority by which anyone could burn somebody else's, and inventing one is confiscation.
fn burn(amount: uint) -> bool {
require(frozen == false, "paused");
require(balances[caller] >= amount, "insufficient balance");
balances[caller] = balances[caller] - amount;
supply = supply - amount;
emit Transfer(caller, "", amount);
return true;
}
fn mint(to: address, amount: uint) -> bool {
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");
// Refuses at the cap rather than truncating. A truncated mint gives the minter less than
// they asked for and tells nobody.
//
// The cap is tested against LIFETIME ISSUANCE and never against `supply`, which `burn`
// can lower. A cap that burning reopens is not a cap: burn what you just minted and the
// room comes back, so a "fixed supply of 1,000" mints 1,000 as many times as the minter
// has patience for.
if (supplyCap > 0) {
require(issuedEver + amount <= supplyCap, "supply cap");
}
issuedEver = issuedEver + amount;
supply = supply + amount;
balances[to] = balances[to] + amount;
emit Transfer("", to, amount);
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");
// Minting, once closed, stays closed. A fixed supply that can be un-fixed is not one.
require(len(mintAuthority) > 0, "mint closed");
emit MinterChanged(mintAuthority, next);
mintAuthority = next;
}
// One-way. This is the function that lets a token PROVE a fixed supply rather than promise
// one. 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 = "";
}
}