height 23717 of 23717 on the network. 9 peer(s) connected, 77 in its peer directory; 1013 block(s) followed and 842 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
- 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
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.
- Save the draft. Download it above, or start from the catalogue text with
strblk contract template escrow > escrow.aresand edit the marked constants yourself. - 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. - 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 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 point | Gas |
|---|---|
| release | 3,235 |
| refund | 3,235 |
| timeoutRefund | 3,233 |
| open | 1,276 |
- Declare at least
- 32,350 ten times the most expensive call
Functions
| Kind | Name | Inputs | Outputs | Mutability |
|---|---|---|---|---|
| function | open | buyerAddress: address, sellerAddress: address, refundAfterMs: uint | uint | nonpayable |
| function | release | uint | nonpayable | |
| function | refund | uint | nonpayable | |
| function | timeoutRefund | uint | nonpayable | |
| function | held | uint | view | |
| function | escrowStatus | uint | view | |
| function | buyerIs | address | view | |
| function | sellerIs | address | view | |
| function | arbiterIs | address | view | |
| function | deadline | uint | view | |
| function | paid | uint | view | |
| event | Opened | buyer: address, seller: address, deadlineMs: uint | ||
| event | Released | to: address, amount: uint | ||
| event | Refunded | to: 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;
}
}