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.
Timelock for your fork
- Status
- a draft, not deployed nothing here is on any chain; never send value to a draft
- Profile
- native STR timelock
- Holds
- native STR
- The property that matters
- nothing pays sooner than the declared delay, and after it the owner cannot stop it
- 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
Timelock- Compiler
areslang-1.3.0- Bytecode SHA-256
d04e62eecbcd4ca5c508a26b3421fc8be90cc3a35bd11be1cfd3c1e7e8392d8c- Bytecode
- 1,888 bytes
- Source SHA-256
3671ea1978795bbabdeaecfeb21fff545b587443f9f8d6163a3b24813de2ec40- 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 timelock > timelock.aresand edit the marked constants yourself. - Dry-run it on your machine.
strblk wallet deploy timelock.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 timelock.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 |
|---|---|
| execute | 3,264 |
| queue | 1,452 |
| cancel | 517 |
- Declare at least
- 32,640 ten times the most expensive call
Functions
| Kind | Name | Inputs | Outputs | Mutability |
|---|---|---|---|---|
| function | delay | uint | view | |
| function | grace | uint | view | |
| function | held | uint | view | |
| function | operationCount | uint | view | |
| function | recipientOf | id: uint | address | view |
| function | amountOf | id: uint | uint | view |
| function | etaOf | id: uint | uint | view |
| function | status | id: uint | uint | view |
| function | executable | id: uint | bool | view |
| function | expired | id: uint | bool | view |
| function | owner | address | view | |
| function | pendingOwnerIs | address | view | |
| function | queue | to: address, amount: uint | uint | nonpayable |
| function | cancel | id: uint | nonpayable | |
| function | execute | id: uint | uint | nonpayable |
| function | proposeOwner | next: address | nonpayable | |
| function | acceptOwnership | nonpayable | ||
| event | Queued | id: uint, to: address, amount: uint, etaMs: uint | ||
| event | Cancelled | id: uint, by: address | ||
| event | Executed | id: uint, to: address, amount: uint, by: address | ||
| event | OwnerProposed | current: address, pending: address | ||
| event | OwnerChanged | previous: address, next: address |
Your draft's source
templates/timelock.ares, as shipped
// =========================================================================================
// Timelock — every payment announced before it can happen, with a delay nobody can shorten.
//
// WHAT IT IS FOR
// Holding native STR under the rule that the owner may only pay it out AFTER a declared
// waiting period. That is what a timelock is for on every chain: not to stop a payment, but
// to make it impossible for one to happen faster than the people affected can see it coming
// and react — move their funds, sell, complain, call a lawyer, fork the front end.
//
// The delay is fixed at deployment. There is no `setDelay`, and there could not be a useful
// one: an owner who can shorten the delay has no delay, and an owner who can shorten it after
// a proposal is queued has a timelock that exists only while nobody minds.
//
// THE THREE PROPERTIES THAT MAKE IT A TIMELOCK
// 1. QUEUE IS PUBLIC AND EARLY. `queue` writes the recipient, the amount and the earliest
// execution time into storage and emits them. Every one is readable by anyone from the
// moment it is queued, which is the entire security model: a timelock protects nobody if
// the queue is private.
// 2. THE OWNER LOSES CONTROL AT THE DEADLINE. `cancel` requires `timestamp < eta`. Once the
// wait is over the owner cannot stop the payment, and `execute` may be called BY ANYBODY
// — so a beneficiary is not at the mercy of an owner who has gone quiet. Before the
// deadline the owner may cancel freely; that is what the waiting period is for.
// 3. AN OLD OPERATION DIES. `execute` refuses after `eta + grace`. Without that, a payment
// queued and forgotten two years ago could be executed by anyone at any moment — a
// timelock is a delay, not an indefinite standing authority.
//
// WHAT IT GUARANTEES
// * No operation can execute earlier than `delay()` milliseconds after the block it was
// queued in. The time comes from the SEALED BLOCK's timestamp, never from a caller
// argument and never from a node's clock, so no caller can move it and every node agrees.
// * An operation executes at most once. The status moves 1 (queued) to 2 (executed) before
// the transfer, and both a second `execute` and a late `cancel` refuse on it.
// * A cancelled operation can never execute, and a cancellation cannot be undone: there is
// no path back from status 3.
// * The owner cannot change a queued operation's recipient or amount. There is no setter;
// an amended payment is a new operation with a new delay, which is the point.
// * The owner cannot pay themselves faster than anybody else. There is no privileged path
// and no emergency withdrawal — read the ABI: `queue`, `cancel`, `execute` and the
// ownership handover are all of it.
// * A revert leaves the queue and the balance byte-identical. Storage and value commit
// together, only on success.
//
// WHAT IT CANNOT DO, AND WHY THAT MATTERS HERE MORE THAN ELSEWHERE
// A timelock on other chains usually guards a CALL: it queues "call `setFee` on that
// contract" and executes it later. THIS CANNOT DO THAT. There are no cross-contract calls in
// AresLang, so the only operation expressible is a payment of native STR out of this
// contract's own balance. A timelocked governance layer over another contract is not
// writable today, and no arrangement of this code makes it so.
//
// It also cannot see who funded it: a call record carries no value and there is no deposit
// hook, so the STR in this contract is pooled and belongs to whatever the owner queues. Fund
// it by an ordinary transfer to its address, AFTER deployment — the receive gate answers from
// the deployed set, so an address that does not exist yet cannot be funded.
//
// HOW IT IS TYPICALLY MISUSED
// * Queuing an operation the contract cannot afford, and reading the queue as a promise.
// Solvency is checked at EXECUTION, not at queue time, because the money may legitimately
// arrive in between. An operation queued against an empty contract reverts with
// `insufficient balance` when its time comes, and stays queued until it expires.
// * Setting a delay of a few seconds to "get the pattern in place". A delay nobody can act
// within is decoration. Pick the time the affected parties would actually need.
// * Setting a grace window shorter than the parties can be relied on to be awake. If nobody
// executes inside it the operation expires and must be queued again, delay and all.
// * Forgetting that `execute` is open to anyone. That is deliberate — it is what stops an
// owner censoring a payment by inaction — but it means a payment WILL land the moment
// anybody bothers, not at a moment the owner picks.
// * 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: queue 1,452,
// cancel 517, execute 3,264 — of which 2,100 is the `TRANSFER` itself and 400 the balance
// read — proposeOwner 412, acceptOwnership 612. Declare a gas limit of at least 33,000.
//
// TO USE IT
// Edit the two constants below — they are the whole policy — deploy, fund the address by an
// ordinary transfer, then `queue(to, amount)`, wait, and `execute(id)`.
// =========================================================================================
contract Timelock {
// ---- edit these two ------------------------------------------------------------------
// The waiting period, in milliseconds. 172800000 is 48 hours. Immutable once deployed: see
// the header for why a setter would defeat the contract.
state delayMs: uint = 172800000;
// How long after the waiting period an operation stays executable. 604800000 is 7 days.
// After that it expires and has to be queued again from the start.
state graceMs: uint = 604800000;
// -------------------------------------------------------------------------------------
// `caller` inside a state initializer is the DEPLOYER: Sindri runs the compiler-generated
// initializer with the authenticated deployer as caller, so there is no claim race.
state contractOwner: address = caller;
state pendingOwner: address = "";
// Ids are handed out from here, start at 1, and are never reused.
state opCount: uint = 0;
// One operation, spread across four maps because AresLang has no struct. All written by
// `queue` and, apart from the status, never written again.
state opTo: map<uint, address>;
state opAmount: map<uint, uint>;
state opEta: map<uint, uint>;
// 0 no such operation, 1 queued, 2 executed, 3 cancelled. Plain numbers rather than an enum
// because AresLang has none; the codes are part of the ABI contract with a client and are
// documented on `status()` below.
state opStatus: map<uint, uint>;
event Queued(id: uint, to: address, amount: uint, etaMs: uint);
event Cancelled(id: uint, by: address);
event Executed(id: uint, to: address, amount: uint, by: address);
event OwnerProposed(current: address, pending: address);
event OwnerChanged(previous: address, next: address);
// ---- views ----------------------------------------------------------------------------
view fn delay() -> uint {
return delayMs;
}
view fn grace() -> uint {
return graceMs;
}
// What the contract holds right now, read from the native ledger rather than from a mirror
// in state: a cached balance is a second copy of the truth and diverges the moment anybody
// pays this contract without calling it.
view fn held() -> uint {
return balance(self);
}
view fn operationCount() -> uint {
return opCount;
}
view fn recipientOf(id: uint) -> address {
return opTo[id];
}
view fn amountOf(id: uint) -> uint {
return opAmount[id];
}
// The earliest block time at which this operation may execute. Absolute milliseconds, so a
// reader needs nothing but this number and their own clock.
view fn etaOf(id: uint) -> uint {
return opEta[id];
}
// 0 no such operation, 1 queued, 2 executed, 3 cancelled. A status of 1 does not mean
// executable — see `executable(id)`, which also accounts for the wait and the grace window.
view fn status(id: uint) -> uint {
return opStatus[id];
}
// Everything `execute` checks except the balance, which a client should read separately with
// `held()` so that "not yet" and "not funded" are distinguishable.
view fn executable(id: uint) -> bool {
return opStatus[id] == 1 && timestamp >= opEta[id] && timestamp <= opEta[id] + graceMs;
}
// True once the window has closed on a queued operation that nobody executed. It stays
// status 1 forever — there is no sweeper to mark it, because a contract has no way to act on
// its own without a caller.
view fn expired(id: uint) -> bool {
return opStatus[id] == 1 && timestamp > opEta[id] + graceMs;
}
view fn owner() -> address {
return contractOwner;
}
view fn pendingOwnerIs() -> address {
return pendingOwner;
}
// ---- the queue ---------------------------------------------------------------------------
fn queue(to: address, amount: uint) -> uint {
require(caller == contractOwner, "not owner");
// Unreachable through a call record — `validateArgs` checksum-validates every `address`
// argument before the first opcode — and kept for a future caller path that does not.
require(len(to) > 0, "empty recipient");
require(amount > 0, "amount must be positive");
// Paying itself would move nothing and could never be spent: a contract cannot be
// `caller`, so value at this address is stuck here whatever the queue says.
require(to != self, "self is not payable");
let id = opCount + 1;
opCount = id;
opTo[id] = to;
opAmount[id] = amount;
// The deadline is computed from the BLOCK's timestamp, which is the one clock every node
// agrees on. Nothing in this contract can move it afterwards.
opEta[id] = timestamp + delayMs;
opStatus[id] = 1;
emit Queued(id, to, amount, opEta[id]);
return id;
}
// The owner's veto, and it has a deadline. After `eta` this refuses — which is the whole
// difference between a timelock and a promise.
fn cancel(id: uint) {
require(caller == contractOwner, "not owner");
require(opStatus[id] == 1, "not queued");
require(timestamp < opEta[id], "wait is over");
opStatus[id] = 3;
emit Cancelled(id, caller);
}
// Callable BY ANYONE once the wait is over, so an owner cannot censor a payment by going
// quiet. Checks, then effects, then the interaction: the status is written to 2 before the
// transfer, so even on a chain that had reentrancy — this one does not, there is no call
// opcode — a second execution could not be reached.
fn execute(id: uint) -> uint {
require(opStatus[id] == 1, "not queued");
require(timestamp >= opEta[id], "still waiting");
// Expiry, so a forgotten operation is not a standing authority to move money years later.
require(timestamp <= opEta[id] + graceMs, "operation expired");
let amount = opAmount[id];
// Checked here rather than at queue time because the funds may legitimately arrive in
// between. The explicit check is about the MESSAGE: without it the revert comes from the
// ledger refusing the transfer, and says nothing about which of the two problems it was.
require(balance(self) >= amount, "insufficient balance");
opStatus[id] = 2;
emit Executed(id, opTo[id], amount, caller);
transfer(opTo[id], amount);
return amount;
}
// ---- ownership --------------------------------------------------------------------------
// 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. Note what the
// handover does NOT do: operations already queued keep their recipients, amounts and
// deadlines, and the new owner inherits the same deadline-bounded veto as the old one.
fn proposeOwner(next: address) {
require(caller == contractOwner, "not owner");
pendingOwner = next;
emit OwnerProposed(contractOwner, next);
}
fn acceptOwnership() {
require(caller == pendingOwner, "not the pending owner");
emit OwnerChanged(contractOwner, caller);
contractOwner = caller;
pendingOwner = "";
}
}