// ========================================================================================= // 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; } }