Escrow: holding value between strangers
A buyer, a seller, and a contract that will only ever do one of two things with the money.
Escrow is the smallest useful thing two parties who do not trust each other can share. The buyer deposits. Then exactly one of two outcomes happens: the money releases to the seller, or it refunds to the buyer. Never both, never neither-forever, never something else.
The whole design is a state machine, and writing the states down first is most of the work:
NEW ──deposit──▶ FUNDED ──release──▶ RELEASED
│
└────refund────▶ REFUNDED
RELEASED and REFUNDED are terminal. Once you are in one, nothing can move you out. That single property is what makes escrow safe to use.
The bug to avoid is the second payout. If release does not check the current state, a seller can call it twice and be paid twice — or call it after a refund has already returned the money to the buyer. The contract pays out value it no longer has, and whoever asks last gets nothing.
Guard every transition on the state you are *leaving*, not just on who is calling. "Only the buyer may refund" is not enough; it must also be true that the escrow is currently FUNDED.
The real contracts/examples/escrow adds a dispute path with an arbiter who can resolve in either direction, plus cancel_before_deposit. Same state machine, more edges. Get the two-outcome core right first.
Your turn
Build the escrow core. deposit moves NEW → FUNDED (buyer only). release moves FUNDED → RELEASED (buyer only, pays the seller). refund moves FUNDED → REFUNDED (seller only, returns to the buyer). Both terminal states must reject everything afterwards.
Hints
One way to do it
from stdlib import abi, events, storage
def _uget(key: bytes) -> int:
raw = storage.get(key, b"")
return 0 if raw == b"" else int.from_bytes(raw, "big")
def _uset(key: bytes, value: int) -> None:
v = int(value)
abi.require(v >= 0, b"negative")
if v == 0:
storage.delete(key)
return
storage.set(key, v.to_bytes(max(1, (v.bit_length() + 7) // 8), "big"))
def _flag(key: bytes) -> bool:
return storage.get(key, b"") == b"\x01"
def _set_flag(key: bytes) -> None:
storage.set(key, b"\x01")
K_BUYER = b"esc:buyer"
K_SELLER = b"esc:seller"
K_STATE = b"esc:state"
K_AMOUNT = b"esc:amount"
NEW = 0
FUNDED = 1
RELEASED = 2
REFUNDED = 3
def init(buyer: bytes, seller: bytes) -> None:
storage.set(K_BUYER, bytes(buyer))
storage.set(K_SELLER, bytes(seller))
_uset(K_STATE, NEW)
def state() -> int:
return _uget(K_STATE)
def amount() -> int:
return _uget(K_AMOUNT)
def deposit(value: int) -> int:
abi.require(abi.caller() == storage.get(K_BUYER, b""), b"not_buyer")
abi.require(_uget(K_STATE) == NEW, b"not_new")
amt = int(value)
abi.require(amt > 0, b"must_be_positive")
_uset(K_AMOUNT, amt)
_uset(K_STATE, FUNDED)
events.emit(b"Deposited", {"buyer": abi.caller(), "value": amt})
return _uget(K_STATE)
def release() -> int:
abi.require(abi.caller() == storage.get(K_BUYER, b""), b"not_buyer")
# Guard the state you are LEAVING, or a terminal escrow can pay out twice.
abi.require(_uget(K_STATE) == FUNDED, b"not_funded")
_uset(K_STATE, RELEASED)
events.emit(b"Released", {"to": storage.get(K_SELLER, b""), "value": _uget(K_AMOUNT)})
return _uget(K_STATE)
def refund() -> int:
abi.require(abi.caller() == storage.get(K_SELLER, b""), b"not_seller")
abi.require(_uget(K_STATE) == FUNDED, b"not_funded")
_uset(K_STATE, REFUNDED)
events.emit(b"Refunded", {"to": storage.get(K_BUYER, b""), "value": _uget(K_AMOUNT)})
return _uget(K_STATE)
Claim your 10 ANM
Finish this lesson and claim 10 ANM, once per address. Paid from the Animica treasury in batches — allow a few minutes.
Stuck? Ask
Answered by Animica's own free inference network. It is donated GPU capacity, so give it 20-30 seconds.