height 23722 of 23722 on the network. 9 peer(s) connected, 77 in its peer directory; 1018 block(s) followed and 847 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
- 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-token.ares- 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
- Compiler in force
areslang-1.3.0- Bytecode
- 2,239 bytes
- Bytecode SHA-256
afee3ff841e5ca1700cdfe2a0b058397b369213e81a8898cf01e280cabf8236c- Source SHA-256
1adee85062a215f6a6cfdb7c24a86126e7b9a93dabd2cddbe6a5ecc3bc77d8a8- Published vector
- matches the published vector spec/conformance-vectors.json pins afee3ff841e5ca17… 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 |
|---|---|---|
tokenName | string | "Example ZK13 Token" |
tokenSymbol | string | "EZK" |
tokenDecimals | uint | 13 |
supplyCap | uint | 0 |
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 |
|---|---|
| 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; post-mining covers only a tenth of the declared limit
ABI
| 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 |
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-token > zk13-token.ares strblk contract template zk13-token --info strblk wallet deploy zk13-token.ares --dry-run --address <your address> strblk wallet deploy zk13-token.ares --node <your node's RPC>
Configure this token
Name it, give it a ticker and an owner wallet, choose its features, and get a ready-to-deploy file instead of a template to edit.
This generates source and a manifest. It deploys nothing and asks for no key: the page is read-only, and the only step that signs is the deploy command it hands you, run on your machine.
Open the token wizard for every option this chain can build, with the ones outside the ZK13 standard behind a warning.
Source
templates/zk13-token.ares, as embedded in this build
// =========================================================================================
// 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 = "";
}
}