@compiler >= 8
include "Option.aes"
// xchange escrow for Gaju.
//
// Funds for an offer sit in this contract. They leave by exactly two doors:
// `release` to the counterparty recorded on the escrow, less the configured
// fee which stays here for the authority to withdraw, or `refund` to the
// depositor. The coordinator may open the first door and set a counterparty
// once, and only to an address that registered itself. Nobody, not even the
// authority, can send an escrow anywhere else. Times are Chain.timestamp,
// milliseconds since the epoch.
contract XchangeEscrow =
// status: 0 funded, 1 released, 2 refunded
record escrow = { depositor : address,
amount : int,
counterparty : option(address),
expiry : int,
matched : bool,
status : int }
record state = { authority : address,
coordinator : address,
fee_bps : int,
fees : int,
registry : map(address, bool),
escrows : map(string, escrow) }
entrypoint init(coordinator : address, fee_bps : int) : state =
require(fee_bps >= 0 && fee_bps =< 1000, "FEE_TOO_HIGH")
{ authority = Call.caller,
coordinator = coordinator,
fee_bps = fee_bps,
fees = 0,
registry = {},
escrows = {} }
stateful entrypoint set_coordinator(coordinator : address) =
require(Call.caller == state.authority, "NOT_AUTHORITY")
put(state{ coordinator = coordinator })
stateful entrypoint set_fee(fee_bps : int) =
require(Call.caller == state.authority, "NOT_AUTHORITY")
require(fee_bps >= 0 && fee_bps =< 1000, "FEE_TOO_HIGH")
put(state{ fee_bps = fee_bps })
// A person registers their own address by calling from it.
stateful entrypoint register() =
put(state{ registry[Call.caller] = true })
payable stateful entrypoint deposit(order_id : string, counterparty : option(address), expiry : int) =
require(Call.value > 0, "ZERO_AMOUNT")
require(expiry > Chain.timestamp, "EXPIRY_IN_PAST")
require(!Map.member(order_id, state.escrows), "ORDER_EXISTS")
let e = { depositor = Call.caller,
amount = Call.value,
counterparty = counterparty,
expiry = expiry,
matched = Option.is_some(counterparty),
status = 0 }
put(state{ escrows[order_id] = e })
stateful entrypoint set_counterparty(order_id : string, counterparty : address) =
require(Call.caller == state.coordinator, "NOT_COORDINATOR")
require(Map.member(counterparty, state.registry), "NOT_REGISTERED")
let e = state.escrows[order_id]
require(e.status == 0, "NOT_FUNDED")
require(!e.matched, "ALREADY_MATCHED")
require(Chain.timestamp < e.expiry, "EXPIRED")
put(state{ escrows[order_id] = e{ counterparty = Some(counterparty), matched = true } })
stateful entrypoint release(order_id : string) =
require(Call.caller == state.coordinator, "NOT_COORDINATOR")
let e = state.escrows[order_id]
require(e.status == 0, "NOT_FUNDED")
require(Chain.timestamp < e.expiry, "EXPIRED")
switch(e.counterparty)
None => abort("NO_COUNTERPARTY")
Some(to) =>
let fee = e.amount * state.fee_bps / 10000
put(state{ escrows[order_id] = e{ status = 1 }, fees = state.fees + fee })
Chain.spend(to, e.amount - fee)
// The depositor at any time while unmatched; anyone once expired.
stateful entrypoint refund(order_id : string) =
let e = state.escrows[order_id]
require(e.status == 0, "NOT_FUNDED")
let by_depositor = Call.caller == e.depositor && !e.matched
require(by_depositor || Chain.timestamp >= e.expiry, "NOT_REFUNDABLE")
put(state{ escrows[order_id] = e{ status = 2 } })
Chain.spend(e.depositor, e.amount)
stateful entrypoint withdraw_fees(to : address) =
require(Call.caller == state.authority, "NOT_AUTHORITY")
let amount = state.fees
put(state{ fees = 0 })
Chain.spend(to, amount)
entrypoint get_escrow(order_id : string) : option(escrow) =
Map.lookup(order_id, state.escrows)
entrypoint is_registered(who : address) : bool =
Map.member(who, state.registry)
entrypoint config() : (address * address * int * int) =
(state.authority, state.coordinator, state.fee_bps, state.fees)