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.
AllowlistToken for your fork
- Status
- a draft, not deployed nothing here is on any chain; never send value to a draft
- Profile
- ZK13 fungible, allowlisted
- Holds
- its own token ledger
- The property that matters
- both sides of every transfer are checked; removal is a declared freeze
- 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
AllowlistToken- Compiler
areslang-1.3.0- Bytecode SHA-256
32614bacfa594996baed82e1981b7d56dc5d6c73d26fe12c5a39fb343193e72d- Bytecode
- 3,014 bytes
- Source SHA-256
3a9ff6a09f8b302cbb8a71e4964ae3b5090dac6a0f9ddca5bf821cbcf6bc569f- The catalogue text?
- Yes, byte for byte: matches the published vector
- Ready to deploy?
- not yet a deployment refuses while these remain: tokenName = "Example Allowlist Token", tokenSymbol = "EAL"
- 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 allowlist-token > allowlist-token.aresand edit the marked constants yourself. - Dry-run it on your machine.
strblk wallet deploy allowlist-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 allowlist-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,209 |
| transfer | 965 |
| setAllowed | 775 |
| burn | 774 |
| setCompliance | 412 |
- Declare at least
- 12,090 ten times the most expensive call
Functions
| Kind | Name | Inputs | Outputs | Mutability |
|---|---|---|---|---|
| function | standard | string | view | |
| 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 | isAllowed | account: address | bool | view |
| function | allowedCount | uint | view | |
| function | complianceOfficer | address | view | |
| function | owner | address | view | |
| function | minter | address | view | |
| function | paused | bool | view | |
| function | pendingOwnerIs | address | view | |
| function | setAllowed | account: address, allow: bool | nonpayable | |
| function | transfer | to: address, amount: uint | bool | nonpayable |
| function | burn | amount: uint | bool | nonpayable |
| function | mint | to: address, amount: uint | bool | nonpayable |
| function | setCompliance | next: address | 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 | AllowedChanged | account: address, allow: bool, by: address | ||
| event | ComplianceChanged | previous: address, next: address | ||
| 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/allowlist-token.ares, as shipped
// =========================================================================================
// AllowlistToken — the ZK13 token standard's optional gated tier, and exactly what it is.
//
// WHAT IT IS FOR
// A token that may only be held by addresses an issuer has put on a list. Securities that
// may only be held by accredited investors, vouchers redeemable by named parties, an
// internal accounting unit that must not leave a known set of desks. This is the
// implementation of docs/ZK13_TOKEN_STANDARD.md §3.5 — the optional domain-gated tier — and
// the first thing this header has to do is correct that section's name.
//
// IT IS AN ISSUER'S LIST. IT IS NOT A DOMAIN PROOF, AND CANNOT BE MADE INTO ONE
// `AresHost` exposes selfAddress, blockTimestampMs, blockHeight, balanceOf and transfer, and
// no domain accessor of any kind. A contract CANNOT ask whether an address holds a proved
// STR.domain, so "domain-gated" is not writable today and nothing in this file attempts it.
// What this contract enforces is that an address appears in a map that one named key writes.
// That is a real and useful property — it is what a transfer-restricted security needs — but
// it verifies the ISSUER'S CLAIM about an address and nothing about the address itself. An
// issuer who puts every address on the list has an unrestricted token with extra gas.
//
// THE POWER THIS TOKEN DELIBERATELY HAS, DECLARED AS §6.5 REQUIRES
// **Removing an address from the list freezes it.** A removed holder can no longer send and
// can no longer receive; their balance stays theirs, readable in `balanceOf`, and moves again
// if they are re-listed. This is a SELECTIVE FREEZE — the one power the base standard forbids
// unless it is declared — and it is declared here, in the ABI (`setAllowed`, `isAllowed`) and
// in this header, because a token whose freeze is a surprise is a trap. If you do not need
// it, deploy `zk13-token.ares` or `fixed-supply-token.ares` instead; do not deploy this and
// leave the list open.
//
// What the compliance key CANNOT do, and there is no function for any of it: move a holder's
// balance, burn a holder's balance, mint to itself without also listing itself (which is
// visible), or change the supply cap.
//
// WHAT IT GUARANTEES
// * Every credit and every debit of a balance passes the list. `mint` requires the recipient
// listed; `transfer` requires BOTH parties listed. There is no path that moves a balance to
// or from an unlisted address.
// * `burn` is NOT gated by the list, deliberately. A holder may always destroy their own
// tokens: blocking it takes nothing from anyone else and leaves a frozen holder with no
// action at all, which is closer to confiscation than to compliance.
// * The list is written by ONE named key, the compliance officer, which the owner appoints
// and which is visible as `complianceOfficer()`. The owner may appoint itself; that is
// what the ABI says and a holder should read it.
// * `allowedCount()` is exact. It moves only when a flag actually changes, so listing an
// already-listed address does not inflate it.
// * The supply rules are the base standard's, unchanged: the cap bounds LIFETIME ISSUANCE
// (`lifetimeIssued()`), minting closes permanently with `closeMinting()`, and no function
// raises the cap.
// * A revert leaves the ledger and the list byte-identical. Storage commits only on success.
//
// HOW IT IS TYPICALLY MISUSED
// * Marketing the list as identity, KYC or accreditation verified on chain. Nothing is
// verified on chain. The issuer asserts; the contract records the assertion.
// * Forgetting to list the issuer's own address before `mint`. The mint reverts with
// `not allowed`, which is correct and surprises everybody the first time.
// * Removing an address to punish it and expecting the balance to come back. It does not.
// Removal freezes; nothing in this contract transfers a frozen balance anywhere, and no
// amendment to a deployed contract is possible — the bytecode is in the address preimage.
// * Losing the compliance key. Nobody can then be listed or unlisted again; the owner can
// appoint a new officer, and if the OWNER key is also lost the list is frozen as it stands,
// forever. Hold both keys somewhere you will still have them in five years.
// * 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: setAllowed 775 on a
// real change and 190 when the flag is already what was asked for, mint 1,209,
// transfer 965, burn 774, setCompliance 412, proposeOwner 412, acceptOwnership 612,
// setPaused 363, setMinter 471, closeMinting 470. Declare a gas limit of at least 13,000.
//
// TO MAKE THIS YOUR TOKEN
// Edit the four constants below and nothing else. The deployer is owner, minter AND
// compliance officer, so the sequence after deployment is `setAllowed(holder, true)` and then
// `mint(holder, amount)`.
// =========================================================================================
contract AllowlistToken {
// ---- edit these four ----------------------------------------------------------------
state tokenName: string = "Example Allowlist Token";
state tokenSymbol: string = "EAL";
// 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. 0 means uncapped, which is what
// this ships as — a template cannot invent a cap for a token it knows nothing about, and a
// plausible-looking placeholder reads as a decision somebody made.
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` lowers it and mint-burn-mint would then issue
// without limit against a cap that never appears to be breached.
state issuedEver: uint = 0;
// `caller` inside a state initializer is the DEPLOYER: Sindri runs the compiler-generated
// initializer with the authenticated deployer as caller, so there is no claim race on any of
// these three roles.
state contractOwner: address = caller;
state pendingOwner: address = "";
state mintAuthority: address = caller;
state complianceKey: address = caller;
state frozen: bool = false;
// The list. An address absent from it reads as `false`, which is the safe default: a map
// entry nobody wrote is the zero of its declared type, so a new address is unlisted rather
// than accidentally permitted.
state allowed: map<address, bool>;
state allowedTotal: uint = 0;
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).
event Transfer(from: address, to: address, amount: uint);
// Every change to the list, with the key that made it. This is the audit trail a restricted
// token is usually required to be able to produce.
event AllowedChanged(account: address, allow: bool, by: address);
event ComplianceChanged(previous: address, next: address);
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, and the profile of it. The `+allowlist`
// suffix is how a wallet learns, in one cheap read, that this token has a transfer
// restriction — before it quotes a price to somebody who cannot receive. A self-declaration
// that proves nothing; the check is `catalog(address)` and `isAllowed`.
view fn standard() -> string {
return "zk13-fungible/1+allowlist";
}
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;
}
view fn lifetimeIssued() -> uint {
return issuedEver;
}
view fn balanceOf(account: address) -> uint {
return balances[account];
}
// The whole restriction, in one view a buyer can call before they accept a payment. False
// means this address can neither send nor receive — including an address that already holds
// a balance.
view fn isAllowed(account: address) -> bool {
return allowed[account];
}
view fn allowedCount() -> uint {
return allowedTotal;
}
view fn complianceOfficer() -> address {
return complianceKey;
}
view fn owner() -> address {
return contractOwner;
}
// "" means minting is closed permanently.
view fn minter() -> address {
return mintAuthority;
}
view fn paused() -> bool {
return frozen;
}
view fn pendingOwnerIs() -> address {
return pendingOwner;
}
// ---- the list ---------------------------------------------------------------------------
// Lists or unlists one address. Unlisting is the declared selective freeze; see the header.
fn setAllowed(account: address, allow: bool) {
require(frozen == false, "paused");
require(caller == complianceKey, "not allowed");
require(len(account) > 0, "empty recipient");
// The count moves only on a real change, which is what makes `allowedCount()` exact rather
// than "the number of times somebody called this".
if (allowed[account] != allow) {
allowed[account] = allow;
if (allow) {
allowedTotal = allowedTotal + 1;
} else {
allowedTotal = allowedTotal - 1;
}
emit AllowedChanged(account, allow, caller);
}
}
// ---- movement ---------------------------------------------------------------------------
fn transfer(to: address, amount: uint) -> bool {
require(frozen == false, "paused");
// Unreachable through a call record — `validateArgs` checksum-validates every `address`
// argument before the first opcode — and kept because it is the standard's rule.
require(len(to) > 0, "empty recipient");
// BOTH sides. Checking only the recipient would let a de-listed holder empty their position
// into a listed one, which is the restriction defeated in a single call.
require(allowed[caller], "not allowed");
require(allowed[to], "not allowed");
require(to != caller, "self transfer");
// A balance at this contract's own address could never move again: a contract cannot be
// `caller`. The list would not stop it either, since the owner could list this address.
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;
emit Transfer(caller, to, amount);
return true;
}
// Not gated by the list, deliberately — see the header. The caller burns its OWN balance and
// no other; there is no authority here by which anyone could burn somebody else's.
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");
// A mint to an unlisted address would create a balance that cannot move, which is the
// restriction producing a stranded position rather than enforcing anything.
require(allowed[to], "not allowed");
// Refuses at the cap rather than truncating, and the cap is tested against LIFETIME
// issuance and never against `supply`, which `burn` can lower.
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 setCompliance(next: address) {
require(caller == contractOwner, "not owner");
emit ComplianceChanged(complianceKey, next);
complianceKey = next;
}
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 = "";
}
// Covers mint, transfer, burn and setAllowed — every write in the contract — and no view, so
// a stopped token stays auditable at the moment somebody needs to audit it.
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. Lets the token PROVE a fixed supply rather than promise one. It does not close the
// LIST — a restricted token still needs its restriction after issuance ends.
fn closeMinting() {
require(caller == contractOwner, "not owner");
require(len(mintAuthority) > 0, "mint closed");
emit MinterChanged(mintAuthority, "");
mintAuthority = "";
}
}