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.
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
- 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
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.
- Save the draft. Download it above, or start from the catalogue text with
strblk contract template fixed-supply-token > fixed-supply-token.aresand edit the marked constants yourself. - 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. - 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 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 point | Gas |
|---|---|
| issue | 1,350 |
| transfer | 778 |
| burn | 717 |
- Declare at least
- 13,500 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 | owner | address | view | |
| function | minter | address | view | |
| function | paused | bool | view | |
| function | issuanceClosed | bool | view | |
| function | issue | bool | nonpayable | |
| function | transfer | to: address, amount: uint | bool | nonpayable |
| function | burn | amount: uint | bool | nonpayable |
| event | Transfer | from: address, to: address, amount: uint | ||
| event | MinterChanged | previous: 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;
}
}