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.

Escrow for your fork

Status
a draft, not deployed nothing here is on any chain; never send value to a draft
Profile
native STR escrow
Holds
native STR
The property that matters
the seller cannot release to themselves; the buyer cannot refund early

Edit the draft

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

This template marks no constant to edit: its parties and amounts are given to its functions after it is deployed. You can still name a deployer to see where it would land.

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
Escrow
Compiler
areslang-1.3.0
Bytecode SHA-256
5921cb9039b2d6c6bdf33caf0eed4cc10c898b7cbb553f5265bb114c39063498
Bytecode
1,567 bytes
Source SHA-256
03b4abe7e54e462324b6a849eb0494bb25785b852452253fae84f1cfa1567c0b
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.

  1. Save the draft. Download it above, or start from the catalogue text with strblk contract template escrow > escrow.ares and edit the marked constants yourself.
  2. Dry-run it on your machine. strblk wallet deploy escrow.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 escrow.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,235
refund3,235
timeoutRefund3,233
open1,276
Declare at least
32,350 ten times the most expensive call

Functions

KindNameInputsOutputsMutability
functionopenbuyerAddress: address, sellerAddress: address, refundAfterMs: uintuintnonpayable
functionreleaseuintnonpayable
functionrefunduintnonpayable
functiontimeoutRefunduintnonpayable
functionhelduintview
functionescrowStatusuintview
functionbuyerIsaddressview
functionsellerIsaddressview
functionarbiterIsaddressview
functiondeadlineuintview
functionpaiduintview
eventOpenedbuyer: address, seller: address, deadlineMs: uint
eventReleasedto: address, amount: uint
eventRefundedto: address, amount: uint

Your draft's source

templates/escrow.ares, as shipped

// =========================================================================================
// Escrow — holds native STR for two parties, with an arbiter and a deadline.
//
// WHAT IT IS FOR
//   A buyer funds this contract; the seller performs; the buyer (or the arbiter) releases.
//   If nobody releases, the buyer takes the money back after a deadline the contract fixes
//   at setup and nobody can move afterwards.
//
// WHAT IT GUARANTEES
//   * The seller cannot pay themselves. `release` refuses any caller who is not the buyer or
//     the arbiter, and it pays the address stored in `seller` — never the caller.
//   * The arbiter cannot be a party. `open` refuses a buyer or a seller equal to the arbiter,
//     because an arbiter that names ITSELF the seller can `release` the whole balance to
//     itself in one call — including value transferred in before `open`, which status 0 has no
//     way to return. That is not arbitration; it is theft with a settlement's paperwork. The
//     check is on both sides: an arbiter that is the buyer takes the same money via `refund`.
//   * The buyer cannot take the money back early. `timeoutRefund` reads the BLOCK's
//     timestamp; there is no time argument anywhere in the ABI, so there is no unlock date a
//     caller can supply.
//   * It closes exactly once. `status` moves open -> released or open -> refunded, and every
//     paying function refuses a status that is not open, so no payout can happen twice.
//   * A refused payout moves nothing at all. Storage and value roll back together, so an
//     escrow whose release is rejected by the ledger is still open, still funded, and can
//     still be refunded.
//
// THE IDENTITY RULE, WHICH YOU MUST READ BEFORE YOU DEPLOY THIS
//   CORRECTED 2026-09-08: this block described a rule that no longer exists, and quoted a
//   revert string that is now unreachable. `ValueJournal.receiveRefusal` used to require the
//   recipient to hold an ALLOCATED STR.domain or be a deployed contract; it now refuses only
//   Hnitbjorg reserved accounts. The gate got MORE permissive, so nothing here was unsafe — but
//   four templates instructed issuers to pre-flight a check that is gone, and this one told them
//   a `release` holding the buyer's money would revert when it will not.
//
//   The rule today: any well-formed, non-reserved address, or a deployed contract. A payout to
//   an address with no domain succeeds. What such an address still cannot do is SPEND what it
//   receives until it proves a domain of its own, which is a fact about the recipient rather
//   than about this escrow.
//
// HOW IT IS FUNDED, AND THE LIMIT THAT FOLLOWS
//   A call record carries no value, so the buyer funds this contract with a PLAIN STR
//   TRANSFER to its address, AFTER it is deployed. The address is derivable from name,
//   bytecode, deployer and salt before the contract exists, but it cannot be funded before it
//   exists: the ledger's receive gate answers from the deployed set, not from the shape of the
//   string, because crediting a derived-but-undeployed address would create a balance with no
//   key and no code behind it.
//   The consequence, and it is the sharpest limit in this template: THE CONTRACT CANNOT SEE
//   WHO PAID IT. There is no deposit hook and no `msg.value`. So this escrow does not track
//   a deposit per party — it escrows whatever balance it holds when it pays out. Anyone can
//   add to it and nobody can be credited for having done so.
//   FUND IT ONLY AFTER `open`, AND ONLY AFTER READING `buyerIs()` AND `sellerIs()`. In status
//   0 there is no buyer and no seller recorded, so there is no exit at all: no release, no
//   refund, no timeout, and none can be added, because the contract has nobody it could
//   honestly pay. Value sent before `open` is hostage to the terms the arbiter chooses next.
//
// HOW IT IS TYPICALLY MISUSED
//   * Funding it AFTER release. `release` pays `balance(self)` at the moment it runs; money
//     that arrives later is stranded in a closed escrow with no path out. Fund, then verify
//     `held()`, then perform.
//   * Treating the arbiter as neutral because the README says so. The arbiter here can send
//     the money to either party at any time before the deadline. That is custody. It is
//     visible in the ABI as `arbiterIs()` and it is the whole reason `arbiter` is a separate
//     view. What the arbiter can no longer do is keep it: it cannot be the buyer or the
//     seller, so every path it can take pays somebody else.
//   * Wanting an escrow with no arbiter. THIS TEMPLATE CANNOT BE ONE. The deployer is the
//     arbiter and `open` now refuses to name it as a party, so "open with the buyer's own
//     address as the arbiter" — which earlier versions of this header suggested — reverts.
//     Deploy from a key both parties accept as the decider, or use a template whose payout
//     needs no decision at all: `vesting.ares` pays on the clock and `multisig.ares` pays on
//     M-of-N.
//   * Deploying without a deadline in mind. An escrow whose release depends on a party who
//     disappears is funds locked forever in an immutable contract. `refundAfterMs` is
//     required and is checked to be non-zero.
//   * Under-declaring the gas limit. MEASURED: open 1,276, release 3,235, refund 3,235,
//     timeoutRefund 3,233. Declare at least 33,000. The paying calls are dominated by one
//     TRANSFER at 2,100 and one BALANCE at 400.
// =========================================================================================

contract Escrow {
  // The deployer is the arbiter. `caller` in a state initializer is the authenticated
  // deployer, so there is no window between deployment and setup in which a stranger could
  // claim this contract.
  state arbiter: address = caller;

  state buyer: address = "";
  state seller: address = "";
  state deadlineMs: uint = 0;

  // 0 = not set up, 1 = open, 2 = released to the seller, 3 = refunded to the buyer.
  state status: uint = 0;
  state paidMinor: uint = 0;

  event Opened(buyer: address, seller: address, deadlineMs: uint);
  event Released(to: address, amount: uint);
  event Refunded(to: address, amount: uint);

  // Set up once, by the arbiter, before anything is at stake. There is no function that
  // changes buyer, seller or deadline afterwards: an escrow whose terms can move is not one.
  fn open(buyerAddress: address, sellerAddress: address, refundAfterMs: uint) -> uint {
    require(caller == arbiter, "not the arbiter");
    require(status == 0, "already open");
    require(buyerAddress != sellerAddress, "buyer and seller must differ");
    // The arbiter decides who is paid, so an arbiter that is also a party decides in its own
    // favour. Naming ITSELF the seller and then calling `release` is not an edge case: it is a
    // one-call theft of everything this contract holds, including anything transferred in
    // before `open`, and it leaves a chain that still validates. Both directions are refused,
    // because an arbiter that is the buyer takes the same money through `refund`.
    require(buyerAddress != arbiter, "the arbiter cannot be the buyer");
    require(sellerAddress != arbiter, "the arbiter cannot be the seller");
    // And neither party may be this contract. The value ledger refuses a contract paying
    // itself, so an escrow whose seller is this address can never release and one whose
    // buyer is this address can never refund: the deposit sits here until the other side
    // of the switch is taken, and if both were set this way nothing could move at all.
    require(buyerAddress != self, "self is not payable");
    require(sellerAddress != self, "self is not payable");
    require(refundAfterMs > 0, "an escrow needs a deadline");
    buyer = buyerAddress;
    seller = sellerAddress;
    // The block's clock, not an argument. A caller-supplied deadline is a caller-supplied
    // escape hatch.
    deadlineMs = timestamp + refundAfterMs;
    status = 1;
    emit Opened(buyerAddress, sellerAddress, deadlineMs);
    return deadlineMs;
  }

  // The buyer accepts delivery, or the arbiter decides for them. Pays the SELLER — the
  // stored address, never the caller.
  fn release() -> uint {
    require(status == 1, "escrow is not open");
    require(caller == buyer || caller == arbiter, "only the buyer or the arbiter may release");
    let amount = balance(self);
    require(amount > 0, "nothing held");
    // Effects before the payment. There are no cross-contract calls in AresLang, so a
    // transfer cannot re-enter — the ordering is free and correct anyway.
    status = 2;
    paidMinor = amount;
    transfer(seller, amount);
    emit Released(seller, amount);
    return amount;
  }

  // The seller cancels, or the arbiter decides for them. Pays the BUYER.
  fn refund() -> uint {
    require(status == 1, "escrow is not open");
    require(caller == seller || caller == arbiter, "only the seller or the arbiter may refund");
    let amount = balance(self);
    require(amount > 0, "nothing held");
    status = 3;
    paidMinor = amount;
    transfer(buyer, amount);
    emit Refunded(buyer, amount);
    return amount;
  }

  // The way out when everybody else has vanished. Anyone may call it, because it can only
  // ever pay the buyer, and only after the deadline the block's clock has passed.
  fn timeoutRefund() -> uint {
    require(status == 1, "escrow is not open");
    require(timestamp >= deadlineMs, "deadline has not passed");
    let amount = balance(self);
    require(amount > 0, "nothing held");
    status = 3;
    paidMinor = amount;
    transfer(buyer, amount);
    emit Refunded(buyer, amount);
    return amount;
  }

  // Reads are never paused and never gated. A stuck escrow must stay auditable.
  view fn held() -> uint {
    return balance(self);
  }

  view fn escrowStatus() -> uint {
    return status;
  }

  view fn buyerIs() -> address {
    return buyer;
  }

  view fn sellerIs() -> address {
    return seller;
  }

  view fn arbiterIs() -> address {
    return arbiter;
  }

  view fn deadline() -> uint {
    return deadlineMs;
  }

  view fn paid() -> uint {
    return paidMinor;
  }
}