height 23774 of 23774 on the network. 9 peer(s) connected, 77 in its peer directory; 1070 block(s) followed and 893 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
- 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/exchange-vault.ares- 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
- Compiler in force
areslang-1.3.0- Bytecode
- 5,660 bytes
- Bytecode SHA-256
e5ae82dd7b7ef35969756e493235d82d1b780fbe40a833e774e8f3a051c8867f- Source SHA-256
4a5ea281d7efed5954bc82949de0cea92f268f28af240fb2c21f7a4b2bcfa1c3- Published vector
- matches the published vector spec/conformance-vectors.json pins e5ae82dd7b7ef359… under areslang-1.3.0
- Catalogue
- every template
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.
| Constant | Type | Shipped as |
|---|---|---|
exchangeName | string | "Example Exchange" |
marketPair | string | "STR/USDT" |
marketId | string | "example-str-usdt" |
allocationMinor | uint | 10000000000000000000000 |
vaultOwner | address | caller |
returnAddress | address | caller |
delayMs | uint | 259200000 |
graceMs | uint | 604800000 |
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 point | Gas |
|---|---|
| release | 3,695 |
| executeReturn | 3,565 |
| queueOwner | 2,028 |
| queueDestination | 1,978 |
| queueRelease | 1,871 |
| queueReturn | 1,754 |
| activateDestination | 1,010 |
| acceptOwnership | 958 |
| cancel | 575 |
- Declare at least
- 36,950 ten times the most expensive call; post-mining covers only a tenth of the declared limit
ABI
| Kind | Name | Inputs | Outputs | Mutability |
|---|---|---|---|---|
| function | exchange | string | view | |
| function | pair | string | view | |
| function | market | string | view | |
| function | allocation | uint | view | |
| function | held | uint | view | |
| function | funded | bool | view | |
| function | owner | address | view | |
| function | treasury | address | view | |
| function | delay | uint | view | |
| function | grace | uint | view | |
| function | activeDestination | address | view | |
| function | isLive | bool | view | |
| function | status | string | view | |
| function | released | uint | view | |
| function | returned | uint | view | |
| function | operationCount | uint | view | |
| function | kindOf | id: uint | uint | view |
| function | targetOf | id: uint | address | view |
| function | amountOf | id: uint | uint | view |
| function | etaOf | id: uint | uint | view |
| function | statusOf | id: uint | uint | view |
| function | executable | id: uint | bool | view |
| function | expired | id: uint | bool | view |
| function | pendingDestination | uint | view | |
| function | pendingOwner | uint | view | |
| function | queueDestination | next: address | uint | nonpayable |
| function | activateDestination | id: uint | nonpayable | |
| function | queueRelease | amount: uint, expected: address | uint | nonpayable |
| function | release | id: uint | uint | nonpayable |
| function | queueReturn | amount: uint | uint | nonpayable |
| function | executeReturn | id: uint | uint | nonpayable |
| function | cancel | id: uint | nonpayable | |
| function | queueOwner | next: address | uint | nonpayable |
| function | acceptOwnership | id: uint | nonpayable | |
| 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 |
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 exchange-vault > exchange-vault.ares strblk contract template exchange-vault --info strblk wallet deploy exchange-vault.ares --dry-run --address <your address> strblk wallet deploy exchange-vault.ares --node <your node's RPC>
Source
templates/exchange-vault.ares, as embedded in this build
// 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;
}
}