SourceLessExplorer · SourceLess AsgardSourceLess Asgard · chain 1313
LiveHeight23,745Head age7 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.

ExchangeVault for your fork

Status
a draft, not deployed nothing here is on any chain; never send value to a draft
Profile
native STR exchange liquidity vault
Holds
native STR
The property that matters
pays only the deposit address active for a public delay, or the fixed return address after the same delay

Edit the draft

Set the constants the template marks for editing, and name a deployer to see where it would land.

string, shipped as "Example Exchange"

string, shipped as "STR/USDT"

string, shipped as "example-str-usdt"

uint, shipped as 10000000000000000000000

Set to caller: the account that deploys it. Edit it in the file if it must be another.

Set to caller: the account that deploys it. Edit it in the file if it must be another.

uint, shipped as 259200000

uint, shipped as 604800000

The form asks this explorer to compile your draft and show it back. It signs nothing, stores nothing and reaches no node.

The draft as shipped

Contract
ExchangeVault
Compiler
areslang-1.3.0
Bytecode SHA-256
e5ae82dd7b7ef35969756e493235d82d1b780fbe40a833e774e8f3a051c8867f
Bytecode
5,660 bytes
Source SHA-256
4a5ea281d7efed5954bc82949de0cea92f268f28af240fb2c21f7a4b2bcfa1c3
The catalogue text?
Yes, byte for byte: matches the published vector
Ready to deploy?
not yet a deployment refuses while these remain: exchangeName = "Example Exchange", marketId = "example-str-usdt"
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.

  1. Save the draft. Download it above, or start from the catalogue text with strblk contract template exchange-vault > exchange-vault.ares and edit the marked constants yourself.
  2. Dry-run it on your machine. strblk wallet deploy exchange-vault.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.
  3. Find your fork's RPC. It is the rpc (Iris, wallets) line of the fork's banner, and the rpcUrl of each node in <data root>/fork.json.
  4. Deploy it. strblk wallet deploy exchange-vault.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 pointGas
release3,695
executeReturn3,565
queueOwner2,028
queueDestination1,978
queueRelease1,871
queueReturn1,754
activateDestination1,010
acceptOwnership958
cancel575
Declare at least
36,950 ten times the most expensive call

Functions

KindNameInputsOutputsMutability
functionexchangestringview
functionpairstringview
functionmarketstringview
functionallocationuintview
functionhelduintview
functionfundedboolview
functionowneraddressview
functiontreasuryaddressview
functiondelayuintview
functiongraceuintview
functionactiveDestinationaddressview
functionisLiveboolview
functionstatusstringview
functionreleaseduintview
functionreturneduintview
functionoperationCountuintview
functionkindOfid: uintuintview
functiontargetOfid: uintaddressview
functionamountOfid: uintuintview
functionetaOfid: uintuintview
functionstatusOfid: uintuintview
functionexecutableid: uintboolview
functionexpiredid: uintboolview
functionpendingDestinationuintview
functionpendingOwneruintview
functionqueueDestinationnext: addressuintnonpayable
functionactivateDestinationid: uintnonpayable
functionqueueReleaseamount: uint, expected: addressuintnonpayable
functionreleaseid: uintuintnonpayable
functionqueueReturnamount: uintuintnonpayable
functionexecuteReturnid: uintuintnonpayable
functioncancelid: uintnonpayable
functionqueueOwnernext: addressuintnonpayable
functionacceptOwnershipid: uintnonpayable
eventDestinationQueuedid: uint, next: address, etaMs: uint
eventDestinationActivatedid: uint, previous: address, next: address, by: address
eventReleaseQueuedid: uint, to: address, amount: uint, etaMs: uint
eventReleasedid: uint, to: address, amount: uint, by: address
eventOwnerQueuedid: uint, current: address, next: address, etaMs: uint
eventOwnerChangedid: uint, previous: address, next: address
eventCancelledid: uint, kind: uint, by: address
eventReturnQueuedid: uint, to: address, amount: uint, etaMs: uint
eventReturnedid: uint, to: address, amount: uint, by: address

Your draft's source

templates/exchange-vault.ares, as shipped

// SourceLess Chain — Intellectual property of Alexandru Marius Stratulat and the SourceLess team.
// GitHub: alexccoin · Email: alex@strlabs.io · All rights reserved. No use without written consent. See LICENSE.
// =========================================================================================
// ExchangeVault — one exchange's STR liquidity, held in the open, paid out on a public delay.
//
// WHAT IT IS FOR
//   Holding the native STR set aside for ONE exchange market, so that anyone can read the pool
//   as a live balance at a contract address rather than as a promise in a press release. The
//   balance IS the pool: `held()` reads the native ledger, and every movement out of it is an
//   event in this contract's feed.
//
//   It can pay in exactly two directions and no third:
//     * to the exchange's DEPOSIT ADDRESS, once one is active, after a public delay;
//     * back to the RETURN ADDRESS fixed at deployment — the account that funded it.
//
// THE FOUR QUEUES, AND WHO MAY DO WHAT
//   Every power the owner has over where the money can go is an OPERATION: queued in public,
//   executable only after `delay()` milliseconds of block time, and dead `grace()` after that.
//     kind 1  a new deposit address   `queueDestination` -> wait -> `activateDestination`
//     kind 2  a release of STR        `queueRelease`     -> wait -> `release`
//             (`queueRelease(amount, expected)` names the payee it expects, and refuses another)
//     kind 3  a new owner             `queueOwner`       -> wait -> `acceptOwnership`
//     kind 4  a return of STR         `queueReturn`      -> wait -> `executeReturn`
//   Queuing and cancelling are the owner's. Executing a matured kind 1, 2 or 4 is open to
//   ANYBODY: the owner's decision was taken, and announced, when it was queued, so the execute
//   step carries no authority of its own and needs no second signature from a custody quorum.
//   Accepting a kind 3 is for the named new owner only, which proves the new key exists.
//   Nothing moves the pool, its destination or its owner without a queued operation.
//
// WHAT IT GUARANTEES
//   * Nothing leaves except to the active deposit address or the return address. There is no
//     other `transfer` in this file; read it. The return address is a constant of the bytecode
//     and no function writes it.
//   * No deposit address takes effect sooner than `delay()` after it was queued, and none takes
//     effect at all without a sealed `activateDestination` the whole network can see.
//   * A release pays the deposit address that was active WHEN IT WAS QUEUED, or nothing. If the
//     destination changes in between, the queued release refuses with `destination changed`;
//     an amount announced against one address can never land at another.
//   * No release executes sooner than `delay()` after it was queued, and each executes at most
//     once: the status moves 1 to 2 before the transfer.
//   * A return pays only the return address, and no sooner than `delay()` after it was queued.
//   * A cancelled operation can never execute, and a cancellation cannot be undone.
//   * The owner's veto ends at the deadline. `cancel` requires `timestamp < etaOf(id)`, for every
//     kind; after that only execution or expiry ends an operation.
//   * A revert leaves the queue, the balance and the totals byte-identical. Storage and value
//     commit together, only on success.
//   * The clock is the SEALED BLOCK's timestamp. No function takes a time argument, so no
//     caller can bring a deadline forward.
//
// WHY A RETURN WAITS TOO
//   A pool that could be emptied at once is a pool a market cannot rely on: the exchange and its
//   traders would be reading a balance the owner can take away between two blocks. So a return
//   is announced exactly like a release and waits the same delay. Its recipient is still fixed
//   in the bytecode, so a stolen owner key can at most queue the pool back to the account that
//   funded it, in public, 72 hours ahead.
//
// WHY THE VETO ENDS AT THE DEADLINE
//   As in `timelock.ares`: once the wait is over the owner can no longer stop an operation, and
//   anybody may execute it. The delay is the time in which a mistake is caught and cancelled; a
//   veto that outlived it would make every eta a suggestion rather than a commitment.
//
// WHAT IT CANNOT DO
//   * See who paid it. A call record carries no value and there is no deposit hook, so the pool
//     is whatever balance the address holds. `funded()` compares that balance with the declared
//     allocation and nothing more; it cannot tell you where the STR came from.
//   * Check that a deposit address belongs to the exchange. It checks the address is well formed
//     and not this contract. The 72-hour delay is the window in which a person verifies it.
//   * Call another contract. AresLang has no cross-contract call.
//
// HOW IT IS TYPICALLY MISUSED
//   * Funding it before checking the deployed bytecode digest and `owner()`, `treasury()` and
//     `delay()` against the deployment manifest. Once STR is in, those are the rules it lives by.
//   * Queuing a release against an unfunded vault and reading the queue as a promise. Solvency is
//     checked at EXECUTION, because funds may legitimately arrive or be returned in between.
//   * Expecting to cancel a matured operation. The veto ends at the eta: cancel inside the delay
//     or not at all.
//   * Letting a queued operation expire. After `etaOf(id) + grace()` it is dead and must be
//     queued again, delay and all.
//   * Rotating the owner's key, or the return address's key, while the vault holds funds. A
//     retired address can be neither the caller nor the payee of anything; hand the vault over
//     with `queueOwner` first, or return the funds first.
//   * Under-declaring the gas limit on the call record. Post-mining covers only
//     min(gasUsed, gasLimit / 10). MEASURED, on the v2 gas table, with 53-character addresses:
//     queueDestination 1,978, activateDestination 1,010, queueRelease 1,871, release 3,695 —
//     of which 2,100 is the `TRANSFER` and 400 the balance read — queueReturn 1,754,
//     executeReturn 3,565, cancel 575, queueOwner 2,028, acceptOwnership 958. Declare a gas
//     limit of at least 37,000.
//
// TO USE IT
//   Generate the source with `node scripts/exchange-vault.mjs plan`, which writes the eight
//   constants below from a parameter set and refuses one it cannot write safely; or edit them by
//   hand. Deploy, verify, fund the address by an ordinary transfer, then queue a destination.
// =========================================================================================

contract ExchangeVault {
  // ---- edit these eight ----------------------------------------------------------------
  // Which exchange and which market this pool is for. Read through `exchange()`, `pair()` and
  // `market()`; checked by nothing, and immutable once deployed.
  state exchangeName: string = "Example Exchange";
  state marketPair: string = "STR/USDT";
  state marketId: string = "example-str-usdt";
  // The pool this vault is declared to hold, in minor STR (13 decimals). 1,000,000,000 STR.
  state allocationMinor: uint = 10000000000000000000000;
  // Who may queue and cancel. `caller` here is the DEPLOYER, so the deployer is the owner unless
  // this line names an address.
  state vaultOwner: address = caller;
  // The one address a return pays. No function changes it.
  state returnAddress: address = caller;
  // The waiting period, in milliseconds. 259200000 is 72 hours. Immutable once deployed.
  state delayMs: uint = 259200000;
  // How long after the waiting period an operation stays executable. 604800000 is 7 days.
  state graceMs: uint = 604800000;
  // -------------------------------------------------------------------------------------

  // The active deposit address, "" until the first `activateDestination`.
  state destination: address = "";

  // Ids start at 1 and are never reused. One operation is spread across five maps because
  // AresLang has no struct; all are written by the queue call and only the status after it.
  state opCount: uint = 0;
  // 1 deposit address, 2 release, 3 owner, 4 return.
  state opKind: map<uint, uint>;
  state opTarget: map<uint, address>;
  state opAmount: map<uint, uint>;
  state opEta: map<uint, uint>;
  // 0 no such operation, 1 queued, 2 executed, 3 cancelled.
  state opStatus: map<uint, uint>;

  // At most one deposit-address change and one owner change in flight, so a reader of the queue
  // never has to work out which of two competing changes will win.
  state pendingDestinationId: uint = 0;
  state pendingOwnerId: uint = 0;

  // Lifetime totals, so the explorer can show where the pool went without replaying the feed.
  state releasedMinor: uint = 0;
  state returnedMinor: uint = 0;

  event DestinationQueued(id: uint, next: address, etaMs: uint);
  event DestinationActivated(id: uint, previous: address, next: address, by: address);
  event ReleaseQueued(id: uint, to: address, amount: uint, etaMs: uint);
  event Released(id: uint, to: address, amount: uint, by: address);
  event OwnerQueued(id: uint, current: address, next: address, etaMs: uint);
  event OwnerChanged(id: uint, previous: address, next: address);
  event Cancelled(id: uint, kind: uint, by: address);
  event ReturnQueued(id: uint, to: address, amount: uint, etaMs: uint);
  event Returned(id: uint, to: address, amount: uint, by: address);

  // ---- views ----------------------------------------------------------------------------

  view fn exchange() -> string {
    return exchangeName;
  }

  view fn pair() -> string {
    return marketPair;
  }

  view fn market() -> string {
    return marketId;
  }

  view fn allocation() -> uint {
    return allocationMinor;
  }

  // Read from the native ledger, never mirrored: a cached balance diverges the moment anybody
  // pays this address without calling it.
  view fn held() -> uint {
    return balance(self);
  }

  view fn funded() -> bool {
    return balance(self) >= allocationMinor;
  }

  view fn owner() -> address {
    return vaultOwner;
  }

  view fn treasury() -> address {
    return returnAddress;
  }

  view fn delay() -> uint {
    return delayMs;
  }

  view fn grace() -> uint {
    return graceMs;
  }

  view fn activeDestination() -> address {
    return destination;
  }

  view fn isLive() -> bool {
    return len(destination) > 0;
  }

  // What the explorer prints beside the pool.
  view fn status() -> string {
    if (len(destination) > 0) {
      return "live";
    }
    return "integration in progress";
  }

  view fn released() -> uint {
    return releasedMinor;
  }

  view fn returned() -> uint {
    return returnedMinor;
  }

  view fn operationCount() -> uint {
    return opCount;
  }

  view fn kindOf(id: uint) -> uint {
    return opKind[id];
  }

  view fn targetOf(id: uint) -> address {
    return opTarget[id];
  }

  view fn amountOf(id: uint) -> uint {
    return opAmount[id];
  }

  // The earliest block time, in absolute milliseconds, at which this operation may execute.
  view fn etaOf(id: uint) -> uint {
    return opEta[id];
  }

  // 0 no such operation, 1 queued, 2 executed, 3 cancelled. A 1 is not "executable": see below.
  view fn statusOf(id: uint) -> uint {
    return opStatus[id];
  }

  // Everything the execute step checks except the balance and, for a release, the destination;
  // read `held()` and `activeDestination()` beside it so "not yet", "not funded" and
  // "destination changed" stay distinguishable.
  view fn executable(id: uint) -> bool {
    return opStatus[id] == 1 && timestamp >= opEta[id] && timestamp <= opEta[id] + graceMs;
  }

  view fn expired(id: uint) -> bool {
    return opStatus[id] == 1 && timestamp > opEta[id] + graceMs;
  }

  // The id of the deposit-address change in flight, or 0. An expired one is not in flight.
  view fn pendingDestination() -> uint {
    let id = pendingDestinationId;
    if (id > 0 && opStatus[id] == 1 && timestamp <= opEta[id] + graceMs) {
      return id;
    }
    return 0;
  }

  view fn pendingOwner() -> uint {
    let id = pendingOwnerId;
    if (id > 0 && opStatus[id] == 1 && timestamp <= opEta[id] + graceMs) {
      return id;
    }
    return 0;
  }

  // ---- the deposit address ---------------------------------------------------------------

  fn queueDestination(next: address) -> uint {
    require(caller == vaultOwner, "not owner");
    require(next != self, "self is not payable");
    require(next != destination, "already the active destination");
    let pending = pendingDestinationId;
    require(!(pending > 0 && opStatus[pending] == 1 && timestamp <= opEta[pending] + graceMs),
      "a destination change is already queued");
    let id = opCount + 1;
    opCount = id;
    opKind[id] = 1;
    opTarget[id] = next;
    opAmount[id] = 0;
    // From the BLOCK's timestamp, the one clock every node agrees on.
    opEta[id] = timestamp + delayMs;
    opStatus[id] = 1;
    pendingDestinationId = id;
    emit DestinationQueued(id, next, opEta[id]);
    return id;
  }

  // Open to anybody once the wait is over: the owner decided when it queued.
  fn activateDestination(id: uint) {
    require(opKind[id] == 1, "not a destination change");
    require(opStatus[id] == 1, "not queued");
    require(timestamp >= opEta[id], "still waiting");
    require(timestamp <= opEta[id] + graceMs, "operation expired");
    opStatus[id] = 2;
    let previous = destination;
    destination = opTarget[id];
    emit DestinationActivated(id, previous, opTarget[id], caller);
  }

  // ---- releases ---------------------------------------------------------------------------

  // Bound to the deposit address active NOW. Changing it later voids this release.
  //
  // `expected` is the payee, restated by the caller. It changes nothing the contract would do —
  // the release goes to the active destination or nowhere — but it puts the payee INSIDE the
  // signed call, so a custody room signing a release reads aloud the address it pays, and a
  // call prepared against one destination cannot be replayed into a vault that has another.
  fn queueRelease(amount: uint, expected: address) -> uint {
    require(caller == vaultOwner, "not owner");
    require(amount > 0, "amount must be positive");
    require(len(destination) > 0, "no destination is active");
    require(expected == destination, "not the active destination");
    let id = opCount + 1;
    opCount = id;
    opKind[id] = 2;
    opTarget[id] = destination;
    opAmount[id] = amount;
    // From the BLOCK's timestamp, the one clock every node agrees on.
    opEta[id] = timestamp + delayMs;
    opStatus[id] = 1;
    emit ReleaseQueued(id, destination, amount, opEta[id]);
    return id;
  }

  // Checks, then effects, then the one interaction.
  fn release(id: uint) -> uint {
    require(opKind[id] == 2, "not a release");
    require(opStatus[id] == 1, "not queued");
    require(timestamp >= opEta[id], "still waiting");
    require(timestamp <= opEta[id] + graceMs, "operation expired");
    require(opTarget[id] == destination, "destination changed since this release was queued");
    let amount = opAmount[id];
    require(balance(self) >= amount, "insufficient balance");
    opStatus[id] = 2;
    releasedMinor = releasedMinor + amount;
    emit Released(id, opTarget[id], amount, caller);
    transfer(opTarget[id], amount);
    return amount;
  }

  // ---- returns ---------------------------------------------------------------------------

  // Announced like a release and paid only to the return address, which no argument can name.
  fn queueReturn(amount: uint) -> uint {
    require(caller == vaultOwner, "not owner");
    require(amount > 0, "amount must be positive");
    let id = opCount + 1;
    opCount = id;
    opKind[id] = 4;
    opTarget[id] = returnAddress;
    opAmount[id] = amount;
    // From the BLOCK's timestamp, the one clock every node agrees on.
    opEta[id] = timestamp + delayMs;
    opStatus[id] = 1;
    emit ReturnQueued(id, returnAddress, amount, opEta[id]);
    return id;
  }

  // Open to anybody once the wait is over, like a release.
  fn executeReturn(id: uint) -> uint {
    require(opKind[id] == 4, "not a return");
    require(opStatus[id] == 1, "not queued");
    require(timestamp >= opEta[id], "still waiting");
    require(timestamp <= opEta[id] + graceMs, "operation expired");
    let amount = opAmount[id];
    require(balance(self) >= amount, "insufficient balance");
    opStatus[id] = 2;
    returnedMinor = returnedMinor + amount;
    emit Returned(id, returnAddress, amount, caller);
    transfer(returnAddress, amount);
    return amount;
  }

  // ---- cancelling and ownership -------------------------------------------------------------

  // Any queued operation, inside its delay. After the eta the veto is gone.
  fn cancel(id: uint) {
    require(caller == vaultOwner, "not owner");
    require(opStatus[id] == 1, "not queued");
    require(timestamp < opEta[id], "wait is over");
    opStatus[id] = 3;
    emit Cancelled(id, opKind[id], caller);
  }

  fn queueOwner(next: address) -> uint {
    require(caller == vaultOwner, "not owner");
    require(next != self, "a contract cannot own itself");
    require(next != vaultOwner, "already the owner");
    let pending = pendingOwnerId;
    require(!(pending > 0 && opStatus[pending] == 1 && timestamp <= opEta[pending] + graceMs),
      "an owner change is already queued");
    let id = opCount + 1;
    opCount = id;
    opKind[id] = 3;
    opTarget[id] = next;
    opAmount[id] = 0;
    // From the BLOCK's timestamp, the one clock every node agrees on.
    opEta[id] = timestamp + delayMs;
    opStatus[id] = 1;
    pendingOwnerId = id;
    emit OwnerQueued(id, vaultOwner, next, opEta[id]);
    return id;
  }

  // Only the named key, which proves it exists, and only after the same delay as everything else.
  fn acceptOwnership(id: uint) {
    require(opKind[id] == 3, "not an owner change");
    require(caller == opTarget[id], "not the queued owner");
    require(opStatus[id] == 1, "not queued");
    require(timestamp >= opEta[id], "still waiting");
    require(timestamp <= opEta[id] + graceMs, "operation expired");
    opStatus[id] = 2;
    emit OwnerChanged(id, vaultOwner, caller);
    vaultOwner = caller;
  }
}