height 23717 of 23717 on the network. 9 peer(s) connected, 77 in its peer directory; 1013 block(s) followed and 842 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.
Vesting for your fork
- Status
- a draft, not deployed nothing here is on any chain; never send value to a draft
- Profile
- native STR vesting
- Holds
- native STR
- The property that matters
- nothing is payable before the cliff; the schedule pays vested minus released
- 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
Vesting- Compiler
areslang-1.3.0- Bytecode SHA-256
2e6f7e39cccb11dad877e1d3595cb0f246b71a9f0fe6e1a1c17fa309117e83e5- Bytecode
- 1,198 bytes
- Source SHA-256
9cf22c4d30dca943b353a3a379e454615924663b6b90f73954595da49e5b9052- The catalogue text?
- Yes, byte for byte: matches the published vector
- Ready to deploy?
- no catalogue examples left
- 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 > vesting.aresand edit the marked constants yourself. - Dry-run it on your machine.
strblk wallet deploy vesting.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.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 |
|---|---|
| release | 3,105 |
| release(final) | 2,890 |
| start | 1,971 |
- Declare at least
- 31,050 ten times the most expensive call
Functions
| Kind | Name | Inputs | Outputs | Mutability |
|---|---|---|---|---|
| function | start | who: address, cliffAfterMs: uint, durationMs: uint | uint | nonpayable |
| function | release | uint | nonpayable | |
| function | beneficiaryIs | address | view | |
| function | grantTotal | uint | view | |
| function | releasedToDate | uint | view | |
| function | cliff | uint | view | |
| function | endsAt | uint | view | |
| function | held | uint | view | |
| function | hasStarted | bool | view | |
| event | Started | beneficiary: address, total: uint, cliffAtMs: uint, endsAtMs: uint | ||
| event | Released | to: address, amount: uint, releasedToDate: uint |
Your draft's source
templates/vesting.ares, as shipped
// =========================================================================================
// Vesting — releases native STR to one beneficiary over time, with a cliff.
//
// WHAT IT IS FOR
// A grantor funds this contract, then starts a schedule. From the cliff onwards the
// beneficiary can draw the linearly vested share; after the end date they can draw the
// rest. Nobody can draw it earlier and nobody can redirect it.
//
// WHAT IT GUARANTEES
// * Nothing is payable before the cliff. The clock is the BLOCK's timestamp; there is no
// time argument in the ABI, so there is no unlock date a caller can supply.
// * Every release pays `vested - alreadyReleased`, never `vested`. The classic vesting bug
// is paying the vested total on every call, which pays the whole schedule repeatedly.
// * The sum of everything ever paid is exactly `total`, and never more, however many times
// `release` is called and in whatever order.
// * `release` pays the STORED beneficiary. Any caller may trigger it — a beneficiary
// without gas is not a beneficiary who cannot be paid — but the money goes to one
// address and there is no function that changes it.
// * The schedule cannot be restarted, revoked, cancelled or accelerated. The grantor keeps
// NO power after `start`. Read the ABI: there is no such function to call.
//
// THE IDENTITY RULE — CORRECTED 2026-09-08
// This block described a rule that no longer exists. `ValueJournal.receiveRefusal` used to
// require a payment recipient to hold an ALLOCATED STR.domain or be a deployed contract; it
// now refuses only Hnitbjorg reserved accounts. The gate became MORE permissive, so nothing
// built against the old text is unsafe — but it instructed issuers to pre-flight a check that
// no longer applies, and told them a payout would revert when it will not.
//
// Today: any well-formed, non-reserved address, or a deployed contract, may be paid. What a
// recipient with no proved domain still cannot do is SPEND what it receives — a fact about
// that holder, not about this contract.
//
// HOW IT IS FUNDED, AND THE LIMIT THAT FOLLOWS
// A call record carries no value, so the grantor funds this contract with a PLAIN STR
// TRANSFER first, and `start` then snapshots `balance(self)` as the granted total. The
// contract cannot see who paid it or when, which is why the total has to be taken as a
// snapshot rather than accumulated per deposit.
// ANYTHING TRANSFERRED IN AFTER `start` IS STRANDED. It is not part of `total`, it is not
// vested, and this contract has no sweep — deliberately, because a grantor sweep is a
// revocation power wearing a support tool's clothes. Fund fully, check `grantTotal()`,
// then start.
//
// HOW IT IS TYPICALLY MISUSED
// * Starting before funding. `start` refuses a zero balance, but it cannot refuse a
// PARTIAL one: it snapshots whatever is there. Check `grantTotal()` after starting and
// before the beneficiary relies on it.
// * Assuming the grantor can cancel. 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.
// * Reading `height` and `timestamp` as interchangeable. This template uses time because a
// vesting date is a date. For anything adversarial prefer height: a producer has some
// latitude over the timestamp it writes and none over the height.
// * Under-declaring the gas limit. MEASURED: start 1,971, release 3,105 during the linear
// period and 2,890 after the end date. Declare at least 32,000.
// =========================================================================================
contract Vesting {
// The deployer. `caller` in a state initializer is the authenticated deployer, so there is
// no claim race between deployment and setup.
state grantor: address = caller;
state beneficiary: address = "";
state startedAtMs: uint = 0;
state cliffAtMs: uint = 0;
state endsAtMs: uint = 0;
state total: uint = 0;
state released: uint = 0;
state started: bool = false;
event Started(beneficiary: address, total: uint, cliffAtMs: uint, endsAtMs: uint);
event Released(to: address, amount: uint, releasedToDate: uint);
// Fund the contract FIRST, then call this. The balance at this instant is the grant.
fn start(who: address, cliffAfterMs: uint, durationMs: uint) -> uint {
require(caller == grantor, "not the grantor");
require(started == false, "already started");
require(durationMs > 0, "duration must be positive");
require(cliffAfterMs <= durationMs, "cliff must be inside the schedule");
// THE GRANT IS LOST IF THIS LINE IS REMOVED. The value ledger refuses a contract that
// pays itself, so a schedule whose beneficiary is this contract reverts on every
// `release` forever — and there is no sweep, no restart and no revocation here by
// design. MEASURED 2026-10-07: `start` accepted it, reported the grant total, emitted
// `Started`, and every later release failed with `a contract cannot transfer to itself`.
// Refused at the door, where the grantor can still fix it.
require(who != self, "self is not payable");
let held = balance(self);
require(held > 0, "fund the contract before starting");
beneficiary = who;
startedAtMs = timestamp;
cliffAtMs = timestamp + cliffAfterMs;
endsAtMs = timestamp + durationMs;
total = held;
started = true;
emit Started(who, held, cliffAtMs, endsAtMs);
return held;
}
fn release() -> uint {
require(started == true, "not started");
require(timestamp >= cliffAtMs, "cliff not reached");
// Vested-to-date. Multiplication happens before division so the integer division
// truncates once, at the end; AresLang integers are arbitrary precision to 4096 bits, so
// `total * elapsed` cannot wrap.
let vested = total;
if (timestamp < endsAtMs) {
let elapsed = timestamp - startedAtMs;
let span = endsAtMs - startedAtMs;
vested = total * elapsed / span;
}
// The line the whole template turns on: pay the DIFFERENCE, not the vested total.
let due = vested - released;
require(due > 0, "nothing to release");
// Effects before the payment.
released = released + due;
transfer(beneficiary, due);
emit Released(beneficiary, due, released);
return due;
}
view fn beneficiaryIs() -> address {
return beneficiary;
}
view fn grantTotal() -> uint {
return total;
}
view fn releasedToDate() -> uint {
return released;
}
view fn cliff() -> uint {
return cliffAtMs;
}
view fn endsAt() -> uint {
return endsAtMs;
}
view fn held() -> uint {
return balance(self);
}
view fn hasStarted() -> bool {
return started;
}
}