height 23718 of 23718 on the network. 9 peer(s) connected, 77 in its peer directory; 1014 block(s) followed and 843 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
- 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
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.
- Save the draft. Download it above, or start from the catalogue text with
strblk contract template exchange-vault > exchange-vault.aresand edit the marked constants yourself. - 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. - 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 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 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
Functions
| 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 |
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;
}
}