SourceLessExplorer · SourceLess AsgardSourceLess Asgard · chain 1313
LiveHeight23,745Head age6 minLast interval4 minFinality23,734Behind−11Pending0 / 8,192Tx in head0
XPlorer following the chain as its own read-only node

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 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

Edit the draft

Set the constants the template marks for editing, and name a deployer to see where it would land.

string, shipped as "Example Allowlist Token"

string, shipped as "EAL"

uint, shipped as 13

uint, shipped as 0

The form asks this explorer to compile your draft and show it back. It signs nothing, stores nothing and reaches no node.

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.

  1. Save the draft. Download it above, or start from the catalogue text with strblk contract template allowlist-token > allowlist-token.ares and edit the marked constants yourself.
  2. 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.
  3. Find your fork's RPC. It is the rpc (Iris, wallets) line of the fork's banner, and the rpcUrl of each node in <data root>/fork.json.
  4. 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 pointGas
mint1,209
transfer965
setAllowed775
burn774
setCompliance412
Declare at least
12,090 ten times the most expensive call

Functions

KindNameInputsOutputsMutability
functionstandardstringview
functionnamestringview
functionsymbolstringview
functiondecimalsuintview
functiontotalSupplyuintview
functionmaxSupplyuintview
functionlifetimeIssueduintview
functionbalanceOfaccount: addressuintview
functionisAllowedaccount: addressboolview
functionallowedCountuintview
functioncomplianceOfficeraddressview
functionowneraddressview
functionminteraddressview
functionpausedboolview
functionpendingOwnerIsaddressview
functionsetAllowedaccount: address, allow: boolnonpayable
functiontransferto: address, amount: uintboolnonpayable
functionburnamount: uintboolnonpayable
functionmintto: address, amount: uintboolnonpayable
functionsetCompliancenext: addressnonpayable
functionproposeOwnernext: addressnonpayable
functionacceptOwnershipnonpayable
functionsetPausednext: boolnonpayable
functionsetMinternext: addressnonpayable
functioncloseMintingnonpayable
eventTransferfrom: address, to: address, amount: uint
eventAllowedChangedaccount: address, allow: bool, by: address
eventComplianceChangedprevious: address, next: address
eventOwnerProposedcurrent: address, pending: address
eventOwnerChangedprevious: address, next: address
eventPausedChangedpaused: bool, by: address
eventMinterChangedprevious: 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 = "";
  }
}