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.
WrappedToken
- 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/wrapped-token.ares- Profile
- ZK13 fungible +wrapped
- Holds
- its own token ledger
- The property that matters
- a bridge-custodied 1:1 wrapper; unwrap burns and announces the withdrawal ref to the relayer
- Compiler in force
areslang-1.3.0- Bytecode
- 2,630 bytes
- Bytecode SHA-256
d0f95c58eed8015f6ba0dd273f741101e586c8737d44e5b65b385aaf618f2e4f- Source SHA-256
58bec6eb4f68224343ee1d521b91e76bda5fb00ca83ff02bf56fbc71af5d8b98- Published vector
- matches the published vector spec/conformance-vectors.json pins d0f95c58eed8015f… 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 Wrapped Token" |
tokenSymbol | string | "wEXA" |
tokenDecimals | uint | 13 |
supplyCap | uint | 0 |
underlyingAsset | string | "unset" |
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,144 |
| unwrap | 879 |
| transfer | 835 |
| burn | 774 |
| acceptOwnership | 612 |
| setMinter | 471 |
| closeMinting | 470 |
| proposeOwner | 412 |
| setPaused | 363 |
- Declare at least
- 11,440 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 | underlying | string | view | |
| function | owner | address | view | |
| function | minter | address | view | |
| function | custodian | address | view | |
| function | paused | bool | view | |
| function | pendingOwnerIs | address | view | |
| function | transfer | to: address, amount: uint | bool | nonpayable |
| function | burn | amount: uint | bool | nonpayable |
| function | unwrap | amount: uint, ref: string | bool | nonpayable |
| function | mint | to: address, amount: uint | bool | 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 | Unwrapped | from: address, amount: uint, ref: string | ||
| 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 wrapped-token > wrapped-token.ares strblk contract template wrapped-token --info strblk wallet deploy wrapped-token.ares --dry-run --address <your address> strblk wallet deploy wrapped-token.ares --node <your node's RPC>
Source
templates/wrapped-token.ares, as embedded in this build
// =========================================================================================
// WrappedToken — a bridge-custodied 1:1 wrapper, as a ZK13 fungible token.
//
// WHAT IT IS FOR
// A fungible asset minted 1:1 by a CUSTODIAN — the bridge authority — when an underlying
// asset is locked on another chain, and burned by a holder to request that the underlying be
// released back to them there. It is the `+wrapped` profile of docs/ZK13_TOKEN_STANDARD.md:
// every view, every mutating function, every event and every `require` message the base
// standard spells is here, spelled exactly, with two additions and no subtractions —
// `underlying()` names the bridged asset and `unwrap(amount, ref)` carries the withdrawal
// destination `ref` out to the relayer. This is what docs/BRIDGE_INTEGRATION.md §5 means by a
// crossed bridge asset becoming an AresLang/ZK13 token contract.
//
// The CUSTODIAN IS THE MINT AUTHORITY. `minter()` is unchanged from the base standard and is
// the authority a holder reads; `custodian()` is an alias for the same address, named for what
// it does here — mint happens only when the underlying is locked, so the party that can mint is
// the party that holds the lock.
//
// WHAT IT GUARANTEES
// * Supply moves only through `mint`, `burn` and `unwrap`. `transfer` moves it by exactly zero.
// `unwrap` burns exactly like `burn` — it is a burn that also announces where the underlying
// should go — so an indexer summing `Transfer` still reconstructs every balance.
// * `maxSupply` bounds LIFETIME ISSUANCE — `lifetimeIssued()`, which only ever goes up — and
// not the circulating `totalSupply`, which `burn` and `unwrap` lower. `maxSupply() == 0`
// means UNCAPPED, which is what this template ships as: a wrapper's cap is the amount of the
// underlying that can ever be locked, which this contract cannot know and must not guess.
// * Minting closes permanently: `closeMinting` is one-way and `setMinter` refuses to reopen it.
// For a wrapper this is the custody being wound down — no more of the underlying will be
// accepted — and it is a promise the code keeps rather than a claim.
// * The owner can pause and can hand over ownership in two steps. The owner CANNOT move
// anybody else's balance: there is no `setBalance`, no `adjust`, no `transferFrom`.
// * A revert leaves the ledger byte-identical. Storage commits only on success.
// * A self-transfer is refused, so the classic debit-then-credit inflation bug cannot be
// reached at all.
//
// HOW THE UNWRAP PATH IS READ, AND WHAT IT IS NOT
// `unwrap(amount, ref)` burns the caller's own balance and emits `Unwrapped(caller, amount,
// ref)` alongside the standard `Transfer(caller, "", amount)`. `ref` is an opaque string the
// relayer interprets as the destination on the underlying chain — an address, a memo, an
// invoice id. THE CONTRACT DOES NOT AND CANNOT VALIDATE IT: there is no cross-chain oracle in
// AresLang, and a contract cannot see the other chain. A malformed `ref` burns the holder's
// wrapped balance and produces a withdrawal the relayer cannot route. The event is EVIDENCE for
// an off-chain relayer and nothing on this chain reads it back (§4.3): the release of the
// underlying is the bridge operator's action, performed off this chain, and this contract's
// only role in it is to have destroyed the wrapped units first.
//
// HOW IT IS TYPICALLY MISUSED
// * Believing the wrapped balance is the underlying. It is a claim on a custodian's promise to
// have locked the underlying, no stronger than that custodian. This contract proves that the
// wrapped supply was minted by the mint authority; it proves nothing about what backs it. Do
// not assert a peg (§7.4) — no mechanism here enforces one.
// * Calling `burn` when you meant `unwrap`. `burn` destroys wrapped units and announces NO
// destination, so the relayer has nothing to act on and the underlying is not released. Use
// `unwrap` to withdraw; `burn` is for destroying units you do not want back.
// * 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.
// * Sending to a contract address. A contract cannot be `caller`, so a balance recorded at a
// contract address can never move again. PERMANENT LOSS; this token has no recovery path and
// cannot be given one. The one case it can detect — its own address — it refuses.
// * 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: mint 1,144,
// transfer 835, burn 774, unwrap 879, proposeOwner 412, acceptOwnership 612, setPaused 363,
// setMinter 471, closeMinting 470. `unwrap`'s cost does not move with the length of `ref` —
// the event is not stored, so a 1-character ref and a 200-character one both cost 879.
// Declare a gas limit of at least 12,000.
//
// WHAT IT DELIBERATELY DOES NOT HAVE
// No `approve` and no `transferFrom`: an allowance exists so another CONTRACT can spend on a
// holder's behalf, and AresLang has no cross-contract calls. See ZK13_TOKEN_STANDARD.md §4.2.
// No upgrade path and no proxy. What you deploy is what holders get, forever.
//
// TO MAKE THIS YOUR TOKEN
// Edit the five constants below and nothing else. Changing them changes the bytecode and
// therefore the deployed address, which is correct: a different token is a different contract.
// `underlyingAsset` ships as "unset" and should name the bridged asset — a contract address,
// a chain-qualified symbol, whatever the relayer and your holders will recognise. Supply
// starts at zero; the deployer is owner and custodian, so the first call after deployment is a
// `mint(holder, amount)` made when the matching lock lands on the underlying chain.
// =========================================================================================
contract WrappedToken {
// ---- edit these five ----------------------------------------------------------------
state tokenName: string = "Example Wrapped Token";
state tokenSymbol: string = "wEXA";
// 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. A wrapper is a candidate to differ, so
// the underlying can be matched unit for unit — but if you change it, say so in your own
// documentation and make sure the relayer locks and releases at the same precision.
state tokenDecimals: uint = 13;
// A ceiling on LIFETIME ISSUANCE, not on circulating supply — see the header. 0 means
// uncapped, and must be declared as 0 rather than as a very large number that looks like a
// cap. SHIPPED AT 0: a wrapper cannot invent a cap, because the real bound is how much of the
// underlying can ever be locked, which lives on the other chain and not in this contract. If
// the custody is itself capped, set this to the real number before you deploy.
state supplyCap: uint = 0;
// The asset this token wraps, as the relayer and your holders will recognise it. Editable, and
// shipped as "unset" rather than as a plausible placeholder that would read as a real binding.
// It is a LABEL: nothing in this contract verifies that the named asset exists or that a lock
// of it stands behind any wrapped unit. `underlying()` reads it back.
state underlyingAsset: string = "unset";
// -------------------------------------------------------------------------------------
state supply: uint = 0;
// Everything `mint` has ever created, and it never goes down. This is what `supplyCap` bounds;
// `supply` cannot be, because `burn` and `unwrap` lower it, and mint -> unwrap -> 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. That is why this token needs no
// `claimOwnership` call and has no window in which a stranger could claim it.
state contractOwner: address = caller;
state pendingOwner: address = "";
state mintAuthority: address = caller;
state frozen: bool = false;
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 and an unwrap are both Transfer(from, "", amount).
// An indexer summing Transfer must arrive at exactly the balances in storage.
event Transfer(from: address, to: address, amount: uint);
// Emitted by `unwrap` ALONGSIDE the Transfer burn, never instead of it. It carries the
// withdrawal destination `ref` on the underlying chain, which the relayer reads to release the
// locked asset. It is evidence, not state: nothing on this chain reads it back.
event Unwrapped(from: address, amount: uint, ref: string);
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 `+wrapped`
// suffix is how a wallet learns, in one cheap read, that this token is a bridge wrapper with an
// `unwrap` withdrawal path. A self-declaration that proves nothing; the check is
// `catalog(address)` against the standard's required surface.
view fn standard() -> string {
return "zk13-fungible/1+wrapped";
}
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;
}
// Everything ever minted, burned or not. `totalSupply` answers "how much exists"; this answers
// "how much was ever issued", and it is the one the cap is enforced against.
view fn lifetimeIssued() -> uint {
return issuedEver;
}
view fn balanceOf(account: address) -> uint {
return balances[account];
}
// The asset this token wraps, exactly as the deployer labelled it. A LABEL and not a proof:
// nothing here verifies the named asset or that a lock backs any wrapped unit.
view fn underlying() -> string {
return underlyingAsset;
}
view fn owner() -> address {
return contractOwner;
}
// "" means minting is closed permanently. Required even though this token does mint: a holder
// must be able to ask "who can mint this?" and get an answer from the ABI.
view fn minter() -> address {
return mintAuthority;
}
// The custodian is the mint authority: an alias for `minter()`, named for what the address does
// in a bridge wrapper. It returns the same value as `minter()`, including "" once minting is
// closed. `minter()` stays the standard's name; this is the wrapper's name for the same party.
view fn custodian() -> address {
return mintAuthority;
}
// Required even though a token might never pause. A missing function is indistinguishable, to a
// wallet, from a token whose answer is inconvenient.
view fn paused() -> bool {
return frozen;
}
view fn pendingOwnerIs() -> address {
return pendingOwner;
}
// ---- movement -------------------------------------------------------------------------
fn transfer(to: address, amount: uint) -> bool {
require(frozen == false, "paused");
// Unreachable through a call record — the ABI check refuses a malformed `address` argument
// before the first opcode runs — and kept because it is the standard's rule.
require(len(to) > 0, "empty recipient");
// The guard costs 8 gas and makes a future caching edit harmless; see zk13-token.ares §6.6.
require(to != caller, "self transfer");
// A balance recorded at a contract address can never move, per standard section 7.3. This is
// the one case of it the token can cheaply refuse outright.
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 destroys wrapped units and announces NO destination: use `unwrap` to withdraw the
// underlying, and `burn` only to destroy units you do not want back.
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;
}
// Burns the caller's own balance exactly as `burn` does, and ALSO emits `Unwrapped` so the
// bridge relayer learns where to release the underlying. The burn and the announcement are one
// atomic call: there is no state in which the wrapped units are destroyed and the withdrawal is
// unannounced, or announced and the units still live. `ref` is opaque to this contract — the
// destination on the underlying chain — and is not validated, because a contract cannot see the
// other chain. A malformed `ref` still burns the balance; the relayer, not this code, decides
// what it can route.
fn unwrap(amount: uint, ref: string) -> bool {
require(frozen == false, "paused");
require(balances[caller] >= amount, "insufficient balance");
balances[caller] = balances[caller] - amount;
supply = supply - amount;
// The standard's single movement record, identical to a plain burn, so an indexer summing
// Transfer still reconstructs the ledger without knowing unwrap from burn.
emit Transfer(caller, "", amount);
// The withdrawal instruction, for the relayer only.
emit Unwrapped(caller, amount, ref);
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.
require(to != self, "self is not payable");
// Refuses at the cap rather than truncating. The cap is tested against LIFETIME ISSUANCE and
// never against `supply`, which `burn` and `unwrap` 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 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 = "";
}
fn setPaused(next: bool) {
require(caller == contractOwner, "not owner");
frozen = next;
emit PausedChanged(next, caller);
}
fn setMinter(next: address) {
require(caller == contractOwner, "not owner");
// Minting, once closed, stays closed. A fixed supply that can be un-fixed is not one.
require(len(mintAuthority) > 0, "mint closed");
emit MinterChanged(mintAuthority, next);
mintAuthority = next;
}
// One-way. For a wrapper this winds the custody down: no more of the underlying will be
// accepted. There is no argument because an `address` argument cannot be "": the ABI check
// refuses it before the first opcode.
fn closeMinting() {
require(caller == contractOwner, "not owner");
require(len(mintAuthority) > 0, "mint closed");
emit MinterChanged(mintAuthority, "");
mintAuthority = "";
}
}