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 for your fork
- Status
- a draft, not deployed nothing here is on any chain; never send value to a draft
- 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
- 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
WrappedToken- Compiler
areslang-1.3.0- Bytecode SHA-256
d0f95c58eed8015f6ba0dd273f741101e586c8737d44e5b65b385aaf618f2e4f- Bytecode
- 2,630 bytes
- Source SHA-256
58bec6eb4f68224343ee1d521b91e76bda5fb00ca83ff02bf56fbc71af5d8b98- The catalogue text?
- Yes, byte for byte: matches the published vector
- Ready to deploy?
- not yet a deployment refuses while these remain: tokenName = "Example Wrapped Token", tokenSymbol = "wEXA", underlyingAsset = "unset"
- 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 wrapped-token > wrapped-token.aresand edit the marked constants yourself. - Dry-run it on your machine.
strblk wallet deploy wrapped-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 wrapped-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 |
|---|---|
| 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
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 | 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 |
Your draft's source
templates/wrapped-token.ares, as shipped
// =========================================================================================
// 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 = "";
}
}