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.
VestingToken for your fork
- Status
- a draft, not deployed nothing here is on any chain; never send value to a draft
- Profile
- ZK13 fungible +vesting
- Holds
- its own token ledger
- The property that matters
- a grant is immutable and spends the cap at once; release mints vested minus released on the block clock
- 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
VestingToken- Compiler
areslang-1.3.0- Bytecode SHA-256
84fa4c79f435ce668c9bbc1c28ef2961105500e0cda468e6921d9af31dac7942- Bytecode
- 3,782 bytes
- Source SHA-256
bf4f8fe4908701b6fae8324e8f4c324e4cf979c7374048377888949eddc5d292- The catalogue text?
- Yes, byte for byte: matches the published vector
- Ready to deploy?
- not yet a deployment refuses while these remain: tokenName = "Example Vesting Token", tokenSymbol = "EVEST"
- 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 vesting-token > vesting-token.aresand edit the marked constants yourself. - Dry-run it on your machine.
strblk wallet deploy vesting-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 vesting-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 |
|---|---|
| grant | 1,368 |
| release | 1,220 |
| mint | 1,144 |
| transfer | 835 |
| burn | 774 |
| acceptOwnership | 612 |
| setMinter | 471 |
| closeMinting | 470 |
| proposeOwner | 412 |
| setPaused | 363 |
- Declare at least
- 13,680 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 | granted | account: address | uint | view |
| function | released | account: address | uint | view |
| function | vestingStart | account: address | uint | view |
| function | vestingDuration | account: address | uint | view |
| function | vested | account: address | uint | view |
| function | owner | address | view | |
| function | minter | 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 | mint | to: address, amount: uint | bool | nonpayable |
| function | grant | beneficiary: address, total: uint, startMs: uint, durationMs: uint | nonpayable | |
| function | release | uint | 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 | Granted | beneficiary: address, total: uint, startMs: uint, durationMs: uint | ||
| 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/vesting-token.ares, as shipped
// =========================================================================================
// VestingToken — a ZK13 fungible token with a built-in per-beneficiary vesting schedule.
//
// WHAT IT IS FOR
// A fungible asset whose ledger lives in this contract's own storage, with one extra power
// the base token does not have: the mint authority can GRANT a beneficiary a total that
// comes into existence gradually, linearly, over a window the grant fixes. It is the
// `+vesting` 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,
// plus `grant` and `release` and the five views that read a schedule back. This is the token
// a project uses to pay a team or an investor on a cliffless linear release without a
// separate escrow contract: the schedule is enforced by the code holders can read, not by a
// spreadsheet somebody promises to honour.
//
// WHAT IT GUARANTEES
// * Supply moves only through `mint`, `release` and `burn`. `transfer` moves it by exactly
// zero. A grant does NOT create tokens: it records a commitment, and the tokens are minted
// by `release` as they vest. So the circulating supply of a granted-but-unreleased schedule
// is zero until the beneficiary pulls it.
// * `maxSupply` bounds LIFETIME ISSUANCE and is immutable. The one counter it is checked
// against is `committedEver` = everything ever MINTED plus everything ever GRANTED, and
// `lifetimeIssued()` returns exactly that. A vesting grant is a promise to issue, so it
// spends cap room the moment it is made, not when it is released — otherwise a schedule
// could be granted against headroom that a direct mint had already taken, and `release`
// would mint past the cap one claim at a time with nothing complaining. `committedEver`
// only ever goes up; `burn` lowers circulating `totalSupply` and never touches it.
// `maxSupply() == 0` means UNCAPPED, which is what this template ships as.
// * The sum of everything a schedule ever pays is exactly its `total`, never more, however
// many times `release` is called and in whatever order. Each `release` pays
// `vestedNow - alreadyReleased`, never `vestedNow` — paying the vested total on every call
// is the classic vesting bug and it pays the whole schedule repeatedly. The final release
// trues up to exactly `total` because at or after the end the vested amount is
// `total * duration / duration`, which is `total` with no remainder.
// * A schedule is IMMUTABLE once granted. There is no `revoke`, no `cancel`, no `accelerate`,
// no `setSchedule` and no second `grant` for the same beneficiary — the mint authority
// keeps NO power over a grant after making it. Read the ABI: there is no such function to
// call. The one thing the authority can still do is appoint a new authority or close
// minting, neither of which reaches an existing schedule.
// * `release` pays the CALLER's own schedule and mints into the caller's own balance. Unlike
// the native `vesting.ares`, nobody can trigger it on a beneficiary's behalf, because a
// mint credits `caller` and a contract cannot be `caller`. A beneficiary with no gas is a
// beneficiary who has not yet released; the schedule waits, it does not expire.
// * The owner can pause and can hand over ownership in two steps. The owner CANNOT move,
// grant away, or release anybody else's balance or schedule: there is no `setBalance`, no
// `adjust`, no `transferFrom`, no `releaseFor`.
// * A revert leaves the ledger byte-identical. Storage commits only on success, so a refused
// release rolls the `released` counter back with the mint that did not happen.
// * A self-transfer is refused, so the classic debit-then-credit inflation bug cannot be
// reached at all.
//
// THE CLOCK, AND WHAT THE SCHEDULE'S HONESTY DOES AND DOES NOT REST ON
// `release` reads the BLOCK's timestamp (ms) and nothing else for its clock — there is no
// time argument on `release`, so there is no unlock moment the beneficiary can supply. The
// grantor DOES supply `startMs` and `durationMs` to `grant`, which is correct: defining the
// schedule is the grantor's job and is done once, visibly, at grant time. A `startMs` in the
// past vests immediately from that point; a `startMs` in the future is a cliff until then.
// Either way it is fixed when the grant is made and no function changes it afterwards.
//
// What the vesting enforces is the RELEASE RATE: the contract will not mint faster than the
// schedule. What it does not and cannot enforce is the BACKING — a grant is the mint
// authority's commitment that the total it promised is a total it means to have issuable, and
// `committedEver` is what holds the authority to that against the cap. There is no collateral
// here and none is claimed.
//
// HOW IT IS TYPICALLY MISUSED
// * Reading `granted(b)` as "already received". It is the WHOLE schedule; `released(b)` is
// what has been minted so far and `vested(b)` is what could be released right now. A
// beneficiary owed 1,000,000 over a year has `granted` 1,000,000 on day one and `balanceOf`
// zero until they release.
// * Expecting the grantor to be able to claw a grant back. It cannot, and that is the point.
// If your agreement needs revocation this is the wrong template, and the beneficiary must
// be able to see that in the ABI before accepting the grant.
// * Granting to a contract address. A contract cannot be `caller`, so it can never call
// `release`, so a grant to a contract address is a schedule nothing can ever draw —
// PERMANENT and unrecoverable. The one case it 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.
// * Deploying and forgetting to `closeMinting`, while telling holders the supply is fixed.
// `minter()` is in the ABI and a holder will read it; so is every outstanding grant's
// committed total, through `granted()` and `lifetimeIssued()`.
// * 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, grant 1,368, release 1,220, proposeOwner 412,
// acceptOwnership 612, setPaused 363, setMinter 471, closeMinting 470. The most expensive
// entry point is `grant` at 1,368. Declare a gas limit of at least 14,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 `revoke`, `cancel`, `accelerate` or `releaseFor`, for the reasons above.
// No upgrade path and no proxy. What you deploy is what holders get, forever.
//
// A NOTE ON THE LINEAR ARITHMETIC
// `vestedNow = total * min(now - start, duration) / duration`. The multiply happens before
// the divide, so the integer division truncates exactly once, at the end. AresLang integers
// are arbitrary precision (thousands of bits), so `total * elapsed` cannot wrap for any total
// and duration a token will ever carry — a total below 2^128 and a duration below 2^64 give a
// product below 2^192, orders of magnitude inside the limit. Intermediate releases may each
// be one unit short of the true continuous value because of the truncation; the FINAL release
// is exact, so the schedule always pays out to the last unit and the dust is never lost.
//
// TO MAKE THIS YOUR TOKEN
// Edit the four 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.
// Supply starts at zero; the deployer is owner and minter, so the first call after deployment
// is `mint(founder, amount)` for the circulating part and `grant(beneficiary, total, startMs,
// durationMs)` for each schedule.
// =========================================================================================
contract VestingToken {
// ---- edit these four ----------------------------------------------------------------
state tokenName: string = "Example Vesting Token";
state tokenSymbol: string = "EVEST";
// 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;
// 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. Immutable once deployed. SHIPPED AT 0, which is uncapped: a template cannot invent a
// cap for a token it knows nothing about. If your holders are to be told the supply is
// bounded, set the real number before you deploy — the address changes with it, which is
// correct — or `closeMinting()` once the whole supply is minted or granted.
state supplyCap: uint = 0;
// -------------------------------------------------------------------------------------
state supply: uint = 0;
// The ONE counter the cap is enforced against: everything ever MINTED plus everything ever
// GRANTED, and it never goes down. `supply` cannot be the cap counter — `burn` lowers it — and
// neither can a mint-only counter, because a grant is a commitment to mint later and a cap
// that ignored commitments could be over-granted and then released past itself one claim at a
// time. `release` does NOT add to this: the grant already counted its whole total here.
state committedEver: 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>;
// The vesting layer, as four FLAT per-beneficiary maps rather than one map of a record: the
// whole schedule is `grantTotal[b]`, `startMsOf[b]`, `durationMsOf[b]` and `releasedOf[b]`.
// `grantTotal[b] == 0` is the single "no schedule" sentinel the whole layer keys off, so a
// grant refuses a zero total and `release`/`vested` treat a zero total as nothing to pay.
state grantTotal: map<address, uint>;
state startMsOf: map<address, uint>;
state durationMsOf: map<address, uint>;
state releasedOf: map<address, uint>;
// Transfer is the SINGLE record of every balance movement and there is no second one: a mint
// and a release are both Transfer("", to, amount), a burn is Transfer(from, "", amount), and a
// transfer is Transfer(from, to, amount). An indexer summing Transfer must arrive at exactly
// the balances in storage, which is why a release emits one too.
event Transfer(from: address, to: address, amount: uint);
// Emitted by `grant` when a schedule is recorded. It is evidence of the commitment, not a
// movement — no tokens exist yet — so it is NOT a Transfer. `release` emits the Transfer.
event Granted(beneficiary: address, total: uint, startMs: uint, durationMs: uint);
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 `+vesting`
// suffix is how a wallet learns, in one cheap read, that this token has a schedule layer with
// a `grant`/`release` 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+vesting";
}
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 or granted, and it never goes down. This is the number the cap is
// checked against — mint and grant both move it, release and burn do not — so a wallet reads
// remaining mintable as `maxSupply() - lifetimeIssued()` and it is right whether or not any
// grant has been released yet.
view fn lifetimeIssued() -> uint {
return committedEver;
}
view fn balanceOf(account: address) -> uint {
return balances[account];
}
// The WHOLE schedule for a beneficiary — the committed total, not what has been released. 0
// means no schedule. Read `released` and `vested` alongside it.
view fn granted(account: address) -> uint {
return grantTotal[account];
}
// How much of a schedule has been minted to the beneficiary so far.
view fn released(account: address) -> uint {
return releasedOf[account];
}
// The schedule's start, in ms, exactly as the grantor fixed it.
view fn vestingStart(account: address) -> uint {
return startMsOf[account];
}
// The schedule's duration, in ms, exactly as the grantor fixed it.
view fn vestingDuration(account: address) -> uint {
return durationMsOf[account];
}
// How much of a schedule has vested as of THIS block's timestamp: the amount a `release`
// called now would bring the beneficiary's released total up to. `vested - released` is what
// the next release would pay. Returns 0 for an address with no schedule and before the start.
view fn vested(account: address) -> uint {
let total = grantTotal[account];
if (total == 0) {
return 0;
}
// `grant` guarantees a non-zero total has a non-zero duration, so the divide below is safe.
let duration = durationMsOf[account];
let start = startMsOf[account];
let elapsed = 0;
// Nothing before the start; `timestamp - start` would be a checked subtract that reverts on
// a negative, so it is only taken when the clock has passed the start.
if (timestamp > start) {
elapsed = timestamp - start;
}
if (elapsed > duration) {
elapsed = duration;
}
// Multiply before divide: the truncation happens once, at the end, and at or past the end
// `elapsed == duration` makes this exactly `total`.
return total * elapsed / duration;
}
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. Closing minting
// also closes granting — a grant is a mint authority power — so an existing schedule can still
// be released but no new one can be made.
view fn minter() -> address {
return mintAuthority;
}
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.
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;
}
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 `committedEver`,
// which a grant also moves, so a direct mint and a vesting commitment share one ceiling.
if (supplyCap > 0) {
require(committedEver + amount <= supplyCap, "supply cap");
}
committedEver = committedEver + amount;
supply = supply + amount;
balances[to] = balances[to] + amount;
emit Transfer("", to, amount);
return true;
}
// ---- the vesting layer ----------------------------------------------------------------
// Records an IMMUTABLE schedule for a beneficiary. The mint authority commits `total` to be
// released linearly from `startMs` over `durationMs`; the tokens are NOT minted here — they
// come into existence as the beneficiary releases them. The commitment spends cap room at
// once, through `committedEver`, because a grant that did not would let the cap be beaten by
// granting and minting against the same headroom. There is no revoke, cancel or accelerate,
// and a beneficiary already holding a schedule cannot be granted a second one — "grant exists"
// protects the first grant's terms from being overwritten.
fn grant(beneficiary: address, total: uint, startMs: uint, durationMs: uint) {
require(frozen == false, "paused");
require(len(mintAuthority) > 0, "mint closed");
require(caller == mintAuthority, "not minter");
require(len(beneficiary) > 0, "empty recipient");
// A schedule to this contract's own address could never be released — a contract is never
// `caller` — so it would be a permanent, unrecoverable commitment. Refused at the door.
require(beneficiary != self, "self is not payable");
require(total > 0, "nothing to grant");
require(durationMs > 0, "duration must be positive");
// Immutable: a second grant would silently rewrite the first one's total, start and clock.
require(grantTotal[beneficiary] == 0, "grant exists");
// The commitment respects the same cap a mint does, counted the same way.
if (supplyCap > 0) {
require(committedEver + total <= supplyCap, "supply cap");
}
committedEver = committedEver + total;
grantTotal[beneficiary] = total;
startMsOf[beneficiary] = startMs;
durationMsOf[beneficiary] = durationMs;
emit Granted(beneficiary, total, startMs, durationMs);
}
// The BENEFICIARY pulls their newly-vested portion. It mints `vested - released` into the
// caller's own balance and nowhere else, so nobody can release on another's behalf and the
// mint authority has no `releaseFor`. Pays the DIFFERENCE and never the vested total — paying
// the total on every call is the classic bug that pays the whole schedule repeatedly. The sum
// of everything a schedule ever pays is exactly its `total`, because the final release lands
// when `elapsed == duration` and vests `total` with no remainder.
fn release() -> uint {
require(frozen == false, "paused");
let total = grantTotal[caller];
require(total > 0, "no grant");
let duration = durationMsOf[caller];
let start = startMsOf[caller];
let elapsed = 0;
// Nothing before the start; the checked subtract is only taken once the clock has passed it.
if (timestamp > start) {
elapsed = timestamp - start;
}
if (elapsed > duration) {
elapsed = duration;
}
// Multiply before divide — one truncation, at the end — and `total > 0` guarantees
// `duration > 0`, so the divide cannot be by zero.
let vestedNow = total * elapsed / duration;
let alreadyReleased = releasedOf[caller];
let claimable = vestedNow - alreadyReleased;
require(claimable > 0, "nothing to release");
// Effects before the mint, and the mint is an ordinary credit of this token. A revert rolls
// the released counter back with the supply that was not created.
releasedOf[caller] = alreadyReleased + claimable;
supply = supply + claimable;
balances[caller] = balances[caller] + claimable;
emit Transfer("", caller, claimable);
return claimable;
}
// ---- 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. This closes both minting and granting permanently. 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 = "";
}
}