SourceLessExplorer · SourceLess AsgardSourceLess Asgard · chain 1313
LiveHeight23,745Head age5 minLast interval4 minFinality23,734Behind−11Pending0 / 8,192Tx in head0
XPlorer following the chain as its own read-only node

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.

Timelock

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/timelock.ares
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
Compiler in force
areslang-1.3.0
Bytecode
1,888 bytes
Bytecode SHA-256
d04e62eecbcd4ca5c508a26b3421fc8be90cc3a35bd11be1cfd3c1e7e8392d8c
Source SHA-256
3671ea1978795bbabdeaecfeb21fff545b587443f9f8d6163a3b24813de2ec40
Published vector
matches the published vector spec/conformance-vectors.json pins d04e62eecbcd4ca5… under areslang-1.3.0

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.

ConstantTypeShipped as
delayMsuint172800000
graceMsuint604800000

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 pointGas
execute3,264
queue1,452
cancel517
Declare at least
32,640 ten times the most expensive call; post-mining covers only a tenth of the declared limit

ABI

KindNameInputsOutputsMutability
functiondelayuintview
functiongraceuintview
functionhelduintview
functionoperationCountuintview
functionrecipientOfid: uintaddressview
functionamountOfid: uintuintview
functionetaOfid: uintuintview
functionstatusid: uintuintview
functionexecutableid: uintboolview
functionexpiredid: uintboolview
functionowneraddressview
functionpendingOwnerIsaddressview
functionqueueto: address, amount: uintuintnonpayable
functioncancelid: uintnonpayable
functionexecuteid: uintuintnonpayable
functionproposeOwnernext: addressnonpayable
functionacceptOwnershipnonpayable
eventQueuedid: uint, to: address, amount: uint, etaMs: uint
eventCancelledid: uint, by: address
eventExecutedid: uint, to: address, amount: uint, by: address
eventOwnerProposedcurrent: address, pending: address
eventOwnerChangedprevious: 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 timelock > timelock.ares
strblk contract template timelock --info
strblk wallet deploy timelock.ares --dry-run --address <your address>
strblk wallet deploy timelock.ares --node <your node's RPC>

Source

templates/timelock.ares, as embedded in this build

// =========================================================================================
// 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 = "";
  }
}