SourceLessExplorer · SourceLess AsgardSourceLess Asgard · chain 1313
LiveHeight23,717Head age58 sLast interval1 minFinality23,706Behind−11Pending0 / 8,192Tx in head0
XPlorer following the chain as its own read-only node

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.

FixedSupplyToken for your fork

Status
a draft, not deployed nothing here is on any chain; never send value to a draft
Profile
ZK13 fungible, fixed supply
Holds
its own token ledger
The property that matters
one issuance, then no owner, no pause and no minter anywhere in the ABI

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 Fixed Supply Token"

string, shipped as "EFS"

uint, shipped as 13

uint, shipped as 10000000000000000000

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
FixedSupplyToken
Compiler
areslang-1.3.0
Bytecode SHA-256
c08fbc93fe1a81076740fa98e8d84bca78f6ec2ceccad7c8990698293a67040d
Bytecode
1,301 bytes
Source SHA-256
c7116c4d84b2ca73b00cd4b55b90265050d12e5441c2096fe55c57d4dcf41b62
The catalogue text?
Yes, byte for byte: matches the published vector
Ready to deploy?
not yet a deployment refuses while these remain: tokenName = "Example Fixed Supply Token", tokenSymbol = "EFS"
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 fixed-supply-token > fixed-supply-token.ares and edit the marked constants yourself.
  2. Dry-run it on your machine. strblk wallet deploy fixed-supply-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 fixed-supply-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
issue1,350
transfer778
burn717
Declare at least
13,500 ten times the most expensive call

Functions

KindNameInputsOutputsMutability
functionstandardstringview
functionnamestringview
functionsymbolstringview
functiondecimalsuintview
functiontotalSupplyuintview
functionmaxSupplyuintview
functionlifetimeIssueduintview
functionbalanceOfaccount: addressuintview
functionowneraddressview
functionminteraddressview
functionpausedboolview
functionissuanceClosedboolview
functionissueboolnonpayable
functiontransferto: address, amount: uintboolnonpayable
functionburnamount: uintboolnonpayable
eventTransferfrom: address, to: address, amount: uint
eventMinterChangedprevious: address, next: address

Your draft's source

templates/fixed-supply-token.ares, as shipped

// =========================================================================================
// FixedSupplyToken — a ZK13 token with one issuance, no owner, and no switches.
//
// WHAT IT IS FOR
//   The token you deploy when the supply is the whole point: a fixed number of units, issued
//   once, after which NOBODY — including the deployer — can mint, pause, freeze, confiscate or
//   change anything. It conforms to docs/ZK13_TOKEN_STANDARD.md exactly as the reference
//   `zk13-token.ares` does; it is the profile of that standard with every owner power deleted
//   rather than merely promised unused.
//
// HOW IT DIFFERS FROM zk13-token.ares
//   `zk13-token.ares` keeps an owner who may pause, appoint a minter, and close minting. This
//   one has no owner after issuance, no pause, no minter role and no `setPaused`, `setMinter`,
//   `proposeOwner` or `acceptOwnership` ANYWHERE IN THE ABI. A holder does not have to trust
//   that the owner will not use a power: the power is not there to use, and `owner()` returns
//   "" to say so. That is the entire difference, and it is a large one.
//
//   The price of it: no emergency stop. If a holder's key is stolen the token cannot be frozen
//   while it is sorted out, because nothing here can freeze anything. That is the trade, and a
//   token that claimed both properties would be claiming one it does not have.
//
// WHY THERE IS AN `issue()` CALL RATHER THAN A BALANCE IN THE INITIALIZER
//   Because a map cannot be initialized at deployment. A state initializer runs before any
//   entry exists and AresLang has no map literal — deliberately, since an initializer that
//   enumerated entries would be unbounded work on a deployment path. So the supply cannot be
//   placed in anybody's balance by the deployment itself, and a token that wants a fixed supply
//   must issue it in one call afterwards. `issue()` is that call: it mints the whole declared
//   supply to the issuer and closes issuance permanently IN THE SAME CALL, so there is no
//   state in which a partial supply exists alongside the power to mint more.
//
// WHAT IT GUARANTEES
//   * `issue()` succeeds at most once, ever. The second call reverts with `mint closed`, and
//     there is no other function anywhere that writes `supply` upward.
//   * After `issue()`, `totalSupply() == lifetimeIssued() == maxSupply()` and `minter()` and
//     `owner()` are both "". Those five readings together are the proof a holder needs: they
//     can be checked from the ABI, in one block, by anyone.
//   * `totalSupply()` can fall — `burn` is the only way, and only a holder can burn what is
//     their own. `maxSupply()` and `lifetimeIssued()` never move after issuance, which is what
//     makes "how much was destroyed" answerable: `maxSupply() - totalSupply()`.
//   * No function moves a balance the caller does not hold. There is no `transferFrom`, no
//     allowance, no owner, no recovery path, and no way to add one: the bytecode is in the
//     address preimage and there is no `setCode` anywhere in Sindri.
//   * A revert leaves the ledger byte-identical. Storage commits only on success.
//
// HOW IT IS TYPICALLY MISUSED
//   * Deploying and then losing the deployer key before calling `issue()`. The token is then
//     PERMANENTLY UNISSUABLE — a contract with a supply of zero and no way to create one.
//     Call `issue()` in the block after you deploy, from the same key, before anything else.
//   * Believing "no owner" means "fairly distributed". The entire supply lands in the
//     issuer's own balance and the distribution after that is an ordinary sequence of
//     transfers that nothing in this contract constrains. What the code proves is the SIZE of
//     the supply, never who holds it.
//   * Sending to a contract address. A contract cannot be `caller`, so a balance recorded at a
//     contract address can never move again. PERMANENT LOSS, with no recovery path and none
//     possible. The one case this contract can detect — its own address — it refuses.
//   * 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.
//   * 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: issue 1,350,
//     transfer 778, burn 717. Declare a gas limit of at least 14,000.
//
// TO MAKE THIS YOUR TOKEN
//   Edit the four constants below and nothing else. `supplyTotal` is in MINOR units: at 13
//   decimals, one whole token is 10000000000000, so the value shipped here is one million
//   whole tokens. Changing a constant changes the bytecode and therefore the address, which is
//   correct — a different token is a different contract.
// =========================================================================================

contract FixedSupplyToken {
  // ---- edit these four ----------------------------------------------------------------
  state tokenName: string = "Example Fixed Supply Token";
  state tokenSymbol: string = "EFS";
  // 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;
  // The ENTIRE supply, in minor units, issued once by `issue()`. Not shipped as 0: unlike the
  // reference token, this template cannot be uncapped — a fixed supply of nothing is not a
  // token, and there is no mint path that could fill it later. One million whole tokens at 13
  // decimals.
  state supplyTotal: uint = 10000000000000000000;
  // -------------------------------------------------------------------------------------

  // `caller` inside a state initializer is the DEPLOYER: Sindri runs the compiler-generated
  // initializer with the authenticated deployer as caller. The issuer is therefore fixed at
  // deployment with no claim window, and `issue()` is the only thing it can ever do.
  state issuer: address = caller;
  state supply: uint = 0;
  state issuedEver: uint = 0;

  state balances: map<address, uint>;

  // Transfer is the SINGLE record of every balance movement and there is no second one: the
  // issuance is Transfer("", issuer, supplyTotal) 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);
  // The standard's event for a minter change, emitted once, with "" as the next minter:
  // issuance closing is a minter change to nobody, and a wallet watching for it learns that
  // this token can never mint again at the moment it becomes true.
  event MinterChanged(previous: address, next: address);

  // ---- views: the ZK13 fungible interface, complete -------------------------------------

  // Which standard this contract was written against. RECOMMENDED rather than required by the
  // fungible standard, because tokens deployed before that section existed cannot grow a
  // function — see ZK13_TOKEN_STANDARD.md §9. It is a self-declaration and proves nothing; the
  // check is `catalog(address)` against the standard's required surface.
  view fn standard() -> string {
    return "zk13-fungible/1+fixed-supply";
  }

  view fn name() -> string {
    return tokenName;
  }

  view fn symbol() -> string {
    return tokenSymbol;
  }

  view fn decimals() -> uint {
    return tokenDecimals;
  }

  view fn totalSupply() -> uint {
    return supply;
  }

  // A real cap, not 0-for-uncapped: it is the declared supply, and `issue()` cannot exceed it
  // because `issue()` sets exactly this number and can only run once.
  view fn maxSupply() -> uint {
    return supplyTotal;
  }

  // 0 before issuance, `supplyTotal` after, and never anything else. Compare it with
  // `totalSupply()` to learn how much has been burned.
  view fn lifetimeIssued() -> uint {
    return issuedEver;
  }

  view fn balanceOf(account: address) -> uint {
    return balances[account];
  }

  // "" always, and the word for that is: this token has no owner. There is no function in this
  // contract that any owner could call, so returning the deployer's address here would name a
  // party with no powers and imply they had some.
  view fn owner() -> address {
    return "";
  }

  // The issuer until `issue()` runs, "" forever after. This is the view a holder reads to
  // learn whether the supply is still open.
  view fn minter() -> address {
    return issuer;
  }

  // Always false. Required by the standard even for a token that cannot pause, because a
  // missing function is indistinguishable, to a wallet, from a token whose answer is
  // inconvenient — and the answer here is the good one: nothing can stop this token.
  view fn paused() -> bool {
    return false;
  }

  // Convenience for a buyer doing the check described in the header: true exactly when the
  // supply is issued, closed, and equal to the declared total.
  view fn issuanceClosed() -> bool {
    return len(issuer) == 0;
  }

  // ---- the one administrative call this token has ----------------------------------------

  // Mints the entire declared supply to the issuer and closes issuance permanently, in one
  // call. There is no argument: an amount argument would make a partial issuance possible, and
  // a partial issuance is a supply that is not fixed.
  fn issue() -> bool {
    require(len(issuer) > 0, "mint closed");
    require(caller == issuer, "not minter");
    issuedEver = supplyTotal;
    supply = supplyTotal;
    balances[caller] = supplyTotal;
    // Closed in the same call that opens it. The order matters only to a reader — there is no
    // reentrancy on this chain, because there is no call opcode and nothing inside this
    // function hands control anywhere — but it is written the way it would have to be written
    // on a chain where there is.
    emit Transfer("", caller, supplyTotal);
    emit MinterChanged(caller, "");
    issuer = "";
    return true;
  }

  // ---- movement ---------------------------------------------------------------------------

  fn transfer(to: address, amount: uint) -> bool {
    // Unreachable through a call record — `validateArgs` checksum-validates every `address`
    // argument before the first opcode — and kept because it is the standard's rule and
    // because a future caller path that does not run it would need the guard.
    require(len(to) > 0, "empty recipient");
    // The two-statement debit-then-credit below is already safe when `to == caller` on this
    // VM, because MSTORE writes through and the credit reads the already-debited value. The
    // inflation appears the moment somebody caches both balances in locals before writing
    // either. This guard costs a few gas and makes that future edit harmless. Keep it.
    require(to != caller, "self transfer");
    // A balance at this contract's own address could never move again: a contract cannot be
    // `caller`. It is the one permanent-loss case an address check can detect.
    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.
  // This is the only function in the contract that lowers `totalSupply`, and it can never
  // raise `lifetimeIssued`, so burning is permanent and provable.
  fn burn(amount: uint) -> bool {
    require(balances[caller] >= amount, "insufficient balance");
    balances[caller] = balances[caller] - amount;
    supply = supply - amount;
    emit Transfer(caller, "", amount);
    return true;
  }
}