ITEN
Demo · Base Sepolia · test money

The Escrow contract

Escrow.sol, in packages/contracts, is where the sum of every Scontract is held. One deal per mandate. It does three things: it accepts the funding, pays the payee on a signed attestation, and refunds the payer after the deadline.

It has no owner, can't be paused and can't be upgraded. Nobody, not even us, can move the money outside its rules.

The address in this environment is on the Test environment page.

Functions

Function Who What it does
open(mandate, payeeSignature, attestor) the payer Opens the deal. The id is the EIP-712 hash of the mandate. Checks that the signature is the payee's, on those terms.
fund(id) the payer Pulls in the sum, which must first be authorized with approve. Only before the deadline.
release(id, observedAt, signature) anyone Pays the payee, with the signature of the attestor recorded in the deal. Valid only for an observation made by the deadline.
settleExpired(id) anyone, after the deadline Refunds the payer, or pays the payee if the mandate provides for tacit execution.
cancel(id) the payer Cancels a deal that is open and not yet funded.
deals(id) view The parties, the token, the sum, the condition, the deadline, the attestor, the status.
mandateId(mandate) view The deal id, that is the EIP-712 hash of the mandate.
attestationDigest(id, conditionHash, observedAt) view What the attestor signs.

A deal's status is None, Open, Funded, Released, Refunded or Cancelled.

Signatures

The EIP-712 domain is { name: "Scontratto", version: "1", chainId, verifyingContract: <escrow> }. It is computed every time, so a copy of the contract at another address or on another chain has a different domain, and signatures can't be reused.

The payee signs the mandate:

Mandate(address payer, address payee, address token, uint256 amount,
        bytes32 conditionHash, uint64 deadline, bool executeOnExpiry, bytes32 nonce)

The attestor signs the attestation:

Attestation(bytes32 dealId, bytes32 conditionHash, bool met, uint64 observedAt)

The core computes the same hash in TypeScript. A Foundry test compares the two byte for byte: if they diverged, the signature given in the app would not open the deal.

Events

Opened(id, payer, payee, amount, conditionHash, deadline), Funded(id), Released(id, conditionHash), Refunded(id), Cancelled(id).

Errors

Error When
NotPayer Someone other than the payer tries to open, fund or cancel.
BadPayeeSignature The signature isn't the payee's, or isn't on these terms.
Exists A deal with the same mandate already exists.
Unknown The deal doesn't exist.
WrongStatus The deal isn't in the right status for this action.
BadAttestor The attestation isn't signed by the deal's attestor.
Expired Funding or observation after the deadline.
NotExpired settleExpired called before the deadline.
TransferFailed The token rejected the transfer.

Before mainnet

The contract is a skeleton with no external dependencies. Before going to mainnet, signature handling moves to the OpenZeppelin libraries, and the contract goes through an audit. The next step for funding is EIP-3009: the payer signs just once, with no approve transaction and no gas.