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.
AllowlistToken
- 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/allowlist-token.ares- 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
- Compiler in force
areslang-1.3.0- Bytecode
- 3,014 bytes
- Bytecode SHA-256
32614bacfa594996baed82e1981b7d56dc5d6c73d26fe12c5a39fb343193e72d- Source SHA-256
3a9ff6a09f8b302cbb8a71e4964ae3b5090dac6a0f9ddca5bf821cbcf6bc569f- Published vector
- matches the published vector spec/conformance-vectors.json pins 32614bacfa594996… 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 Allowlist Token" |
tokenSymbol | string | "EAL" |
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,209 |
| transfer | 965 |
| setAllowed | 775 |
| burn | 774 |
| setCompliance | 412 |
- Declare at least
- 12,090 ten times the most expensive call; post-mining covers only a tenth of the declared limit
ABI
| 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 |
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 allowlist-token > allowlist-token.ares strblk contract template allowlist-token --info strblk wallet deploy allowlist-token.ares --dry-run --address <your address> strblk wallet deploy allowlist-token.ares --node <your node's RPC>
Source
templates/allowlist-token.ares, as embedded in this build
// =========================================================================================
// 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 = "";
}
}