Open/Funded split; “for interoperability”
The precedent in established ERCs relating to Solidity contract interfaces like ERC-20, ERC-1155, ERC-2535 centers such ERCs around the expected behavior of public functions in the specified interface. Internal mechanisms are only relevant to such specs as far as they affect the observable behavior of public functions. ERC-162 (ENS registrar) specifies internal state machine behavior, but it describes a canonical deployment rather than an interface intended for diverse implementations.
So I think the key question is: what aspects of the Open/Funded states are relevant to external contracts/apps that would want to integrate with diverse ERC-8183 implementations, rather than a specific one? More concretely, what would motivate diverse implementations of createJob, setProvider, setBudget, and fund that follow the same interface but differ in behavior?
Specifying state machine behavior means that implementations that don’t adhere to it technically aren’t ERC-8183-compliant, which is undesirable towards enabling a diverse ecosystem. Therefore I recommend leaving internal behavior as unspecified as possible while leaving any desirable points of integration possible. Any particular implementation can specify state machine behavior more specifically, but such specification doesn’t necessarily belong in the ERC.
Rejected being terminal
The same concern about overspecifying internal behavior applies here. Commitment models can be implemented in the evaluator/arbiter part of the system - see Alkahest’s ExclusiveUnrevocableConfirmationArbiter. The idea for your stated model (final commitment from a provider) would be that Provider A commits to the evaluator/arbiter, which rejects any future submissions from them.
There are many possible commitment models, and I don’t think a general-purpose conditional escrow ERC should enforce any specific one, though models can be enforced by implementations, and this is ideally abstracted in the interface so that implementations of a commitment model can be reused across contexts (a “microcondition” as I described above). Some contexts may want switching to Provider B after rejecting Provider A to require renegotiation etc., and others may not.
Alkahest alignment
My minimal proposal for a general purpose escrow ERC, from the direction of Alkahest, would be just the IArbiter interface defining a condition for escrow release
interface IArbiter {
function checkObligation(
Attestation memory obligation, // fulfillment data; could be custom format, bytes32, or bytes
bytes memory demand, // demand data; arbiter type can be thought of as IArbiter<DemandData>
bytes32 fulfilling // escrow/job uid
) external view returns (bool);
}
Alkahest chooses to depend strongly on EAS Attestations as the obligation data format because its fields align very nicely with what a proof of fulfillment wants, but bytes or bytes32 would be more generic. It may also be worth considering generalizing returns (bool) to bytes or bytes32 (mirroring the reason attestation hash in complete/reject).
The “evaluator” mechanism as specified and the commitment models we’ve been describing can all be straightforwardly implemented as IArbiter instances (see TrustedOracleArbiter for single evaluator approval).
However, the IArbiter pattern conflicts somewhat in purpose with the role of hooks in the proposal draft. A smaller change to the proposal would be to remove many of the specified behaviors, and to instead implement them as hooks. It seems fairly straightforward to wrap any IArbiter implementation as an IACPHook in the evaluation phase. Though, since the IACPHook interface is much less structured than IArbiter, this may lead to less useful composability of hooks - where most hooks are written for and only usable with a specific application.
As I said above, I think the two most useful interfaces in an escrow ERC would be
- the process of conditionally promising a future on-chain action
- the conditions for such promises
and that these two should be specified as little as possible while still enabling composability between the two. IArbiter or IACPHooks at the evaluation step addresses 2., but 1. is harder to specify, especially without overspecifying and precluding potentially useful behaviors.
Alkahest has the abstract contracts BaseEscrowObligation and BaseEscrowObligationTierable, which define the virtual functions
// Called when escrow is created
function _lockEscrow(bytes memory data, address from) internal virtual;
// Called when escrow is collected (after successful fulfillment check)
function _releaseEscrow(
bytes memory escrowData,
address to,
bytes32 fulfillmentUid
) internal virtual returns (bytes memory result);
// Called when escrow expires and is reclaimed
function _returnEscrow(bytes memory data, address to) internal virtual;
// Extract arbiter and demand from encoded data
function extractArbiterAndDemand(
bytes memory data
) public pure virtual returns (address arbiter, bytes memory demand);
but there are difficult decisions in what the shared behavior of escrow contracts should be (we publish 2 variants, but they aren’t exhaustive).
Possibly, escrow contract behavior isn’t useful to standardize into an interface at all. Alkahest’s final contracts have wrappers around the generic bytes-parameter escrow functions which accept more specific data types, and we’ve never encountered a case so far where the generic “raw” version has been needed by consumers over these wrappers. The extractArbiterAndDemand function is useful though, since in this architecture escrows are associated with some demand data, and it’s useful to be able to generically extract this from escrows.
Standardizing just the arbitration interface and some aspects of the escrow data format would be enough for many different escrow processes to share and compose arbitration mechanisms, which I think is the main potential of interoperability for escrow. I’m curious whether you see interoperability scenarios that require more lifecycle standardization than that.