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

height 23775 of 23775 on the network. 9 peer(s) connected, 77 in its peer directory; 1071 block(s) followed and 894 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

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/fixed-supply-token.ares
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
Compiler in force
areslang-1.3.0
Bytecode
1,301 bytes
Bytecode SHA-256
c08fbc93fe1a81076740fa98e8d84bca78f6ec2ceccad7c8990698293a67040d
Source SHA-256
c7116c4d84b2ca73b00cd4b55b90265050d12e5441c2096fe55c57d4dcf41b62
Published vector
matches the published vector spec/conformance-vectors.json pins c08fbc93fe1a8107… under areslang-1.3.0

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.

ConstantTypeShipped as
tokenNamestring"Example Fixed Supply Token"
tokenSymbolstring"EFS"
tokenDecimalsuint13
supplyTotaluint10000000000000000000

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 pointGas
issue1,350
transfer778
burn717
Declare at least
13,500 ten times the most expensive call; post-mining covers only a tenth of the declared limit

ABI

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

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 fixed-supply-token > fixed-supply-token.ares
strblk contract template fixed-supply-token --info
strblk wallet deploy fixed-supply-token.ares --dry-run --address <your address>
strblk wallet deploy fixed-supply-token.ares --node <your node's RPC>

Source

templates/fixed-supply-token.ares, as embedded in this build

// =========================================================================================
// 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;
  }
}