Skip to main content

Escrow Flow (pt 2 - Job Trading)

Here's how Alice buys a compute job from Bob with her ERC-20 token, where the validity of the job result is decided by a mutually trusted third party Charlie:

  1. Alice deposits her ERC-20 token into escrow via the ERC20EscrowObligation contract. Her demand is made via the TrustedOracleArbiter contract, and contains her compute query and the mutually trusted third party's address (Charlie).
  2. Bob parses Alice's query from her demand, completes the compute task, and submits the result in an attestation via the StringResultObligation contract. Then he requests an arbitration on his attestation from Charlie via TrustedOracleArbiter.
  3. Charlie sees the arbitration request event, reads Alice's query and Bob's fulfillment, and determines whether the fulfillment is valid.
  4. Finalization
    1. If Charlie decides the fulfillment is valid, Bob can now use his result attestation from StringResultObligation to claim Alice's escrow.
    2. If not valid, Alice can wait for her escrow to expire and reclaim it.

Depositing escrow using TrustedOracleArbiter

You can use TrustedOracleArbiter when you want an off-chain judge to decide whether a deal has been validly fulfilled. The judge could be a real person or an automated program. In this example, we'll demand that Bob capitalize a string for us, and Charlie will verify off-chain whether Bob did so correctly. In practice, this might represent Bob doing a complex computation, and Charlie verifying if it meets Alice's criteria.

// Encode the demand with TrustedOracleArbiter
bytes memory demand = abi.encode(
TrustedOracleArbiter.DemandData({
oracle: charlie, // Charlie's address as the trusted oracle
data: abi.encode("capitalize hello world") // The job query
})
);

// Approve and deposit ERC-20 into escrow
IERC20(erc20Token).approve(address(erc20EscrowObligation), 100 * 10**18);

bytes32 escrowUid = erc20EscrowObligation.doObligation(
ERC20EscrowObligation.ObligationData({
token: erc20Token,
amount: 100 * 10**18,
arbiter: address(trustedOracleArbiter),
demand: demand
}),
block.timestamp + 86400 // 24 hour expiration
);

Fulfilling a job and requesting arbitration

The data field of TrustedOracleArbiter doesn't enforce any kind of schema, so you have to come to an agreement with a buyer beforehand in order to understand and parse their query. StringObligation is a similarly flexible contract that you can use for fulfillments when the data won't be used directly by other on-chain contracts.

Note that the fulfillment should contain a copy or reference to the demand it's intended to fulfill. This is needed to prevent a fulfillment that's valid for one query being used to claim another escrow demanding the same judge, but with a different query. Usually, this is done by setting the escrow attestation as the refUID of the fulfillment.

// Bob fulfills the job with the result
bytes32 fulfillmentUid = stringObligation.doObligation(
"HELLO WORLD", // The computed result (capitalized)
escrowUid // Reference to the escrow being fulfilled
);

// Bob requests arbitration from Charlie
trustedOracleArbiter.requestArbitration(fulfillmentUid, charlie);

Charlie should listen for ArbitrationRequested events demanding him as the oracle. Then he can retrieve the attestation from EAS, decide if it's valid, and use the arbitrate function on TrustedOracleArbiter to submit his decision on-chain.

Note that the ArbitrationRequested event doesn't have a reference to an escrow/demand. As explained above, the demand should be retrieved from the fulfillment attestation. If it's an escrow attestation referenced by refUID, you can get the attestation from EAS and parse the demand.

// Charlie listens for ArbitrationRequested events (off-chain)
// When an event is found, Charlie retrieves the fulfillment attestation
Attestation memory fulfillment = eas.getAttestation(fulfillmentUid);

// Get the escrow attestation from the fulfillment's refUID
Attestation memory escrow = eas.getAttestation(fulfillment.refUID);

// Parse the demand to get the original query
TrustedOracleArbiter.DemandData memory demandData = abi.decode(
escrow.data,
(TrustedOracleArbiter.DemandData)
);
string memory query = abi.decode(demandData.data, (string));

// Parse the fulfillment result
string memory result = abi.decode(fulfillment.data, (string));

// Charlie validates the result
bool isValid = false;
if (bytes(query).length > 10 && keccak256(bytes(substring(query, 0, 10))) == keccak256(bytes("capitalize"))) {
// Extract the text to capitalize (everything after "capitalize ")
string memory textToCapitalize = substring(query, 11, bytes(query).length);
// Convert to uppercase and compare
isValid = keccak256(bytes(result)) == keccak256(bytes(toUpperCase(textToCapitalize)));
}

trustedOracleArbiter.arbitrate(fulfillmentUid, isValid);

Waiting for arbitration and claiming escrow

Bob can listen for the ArbitrationMade event from TrustedOracleArbiter, then claim the escrow if the decision was positive, or retry if not.

// Bob listens for ArbitrationMade events (off-chain)
// Filter for events related to Bob's fulfillment
// When a positive decision is found:

// Bob claims the escrow using the fulfillment attestation
erc20EscrowObligation.collectEscrow(escrowUid, fulfillmentUid);

If no valid fulfillment is made, Alice can reclaim the escrow after it expires.

// Alice reclaims her expired escrow
erc20EscrowObligation.reclaimExpired(escrowUid);

SDK utilities

The SDK provides functions to make it easier to set up an automatic validator server that interacts with TrustedOracleArbiter.

// Set up an automatic oracle that validates string capitalization
const { unwatch } = await charlieClient.oracle.listenAndArbitrate({
fulfillment: {
attester: stringObligation,
obligationAbi: parseAbiParameters("(string item)"),
},
arbitrate: async (obligation) => {
// Custom validation logic for any capitalization task
const result = obligation[0].item;
// Check if result is all uppercase (simple heuristic)
const isCapitalized = result === result.toUpperCase() && result.length > 0;
return isCapitalized ? true : false;
},
onAfterArbitrate: async (decision) => {
console.log(`Arbitrated ${decision.attestation.uid}: ${decision.decision}`);
},
pollingInterval: 1000, // Check every second
});

// For more complex validation with access to the original demand
const { unwatch: unwatchEscrow } =
await charlieClient.oracle.listenAndArbitrateForEscrow({
escrow: {
attester: erc20EscrowObligation,
demandAbi: parseAbiParameters("(string query)"),
},
fulfillment: {
attester: stringObligation,
obligationAbi: parseAbiParameters("(string item)"),
},
arbitrate: async (obligation, demand) => {
// Validate that the fulfillment matches the demand
const query = demand[0].query;
const result = obligation[0].item;

// Support different query types
if (query.startsWith("capitalize ")) {
const textToCapitalize = query.substring(11);
return result === textToCapitalize.toUpperCase();
} else if (query.startsWith("reverse ")) {
const textToReverse = query.substring(8);
return result === textToReverse.split("").reverse().join("");
} else if (query.startsWith("length ")) {
const textToMeasure = query.substring(7);
return result === textToMeasure.length.toString();
}

return false;
},
onAfterArbitrate: async (decision) => {
console.log(`Validated job: ${decision.decision}`);
},
pollingInterval: 1000,
});

// Arbitrate past fulfillments (useful for catching up)
const decisions = await charlieClient.oracle.arbitratePast({
fulfillment: {
attester: stringObligation,
obligationAbi: parseAbiParameters("(string item)"),
},
arbitrate: async (obligation) => {
return obligation[0].item === obligation[0].item.toUpperCase();
},
skipAlreadyArbitrated: true, // Don't re-arbitrate
});

It also provides functions for waiting for an escrow to be fulfilled (claimed), and for listening for the ArbitrationMade event.

// Alice waits for her escrow to be fulfilled
const fulfillmentResult = await aliceClient.waitForFulfillment(
aliceClient.contractAddresses.erc20EscrowObligation,
escrow.uid,
1000, // Optional polling interval
);

if (fulfillmentResult.fulfillment) {
console.log(`Escrow fulfilled by ${fulfillmentResult.fulfiller}`);
console.log(`Fulfillment UID: ${fulfillmentResult.fulfillment}`);
}

// Bob waits for arbitration and then claims
const waitForArbitrationAndClaim = async () => {
// Wait for arbitration decision
return new Promise((resolve) => {
const unwatch = publicClient.watchEvent({
address: trustedOracleArbiter,
event: parseAbiEvent(
"event ArbitrationMade(bytes32 indexed obligation, address indexed oracle, bool decision)",
),
args: { obligation: fulfillment.uid },
onLogs: async (logs) => {
if (logs[0].args.decision) {
// Positive decision - claim the escrow
await bobClient.erc20.collectEscrow(escrow.uid, fulfillment.uid);
resolve({ success: true });
} else {
resolve({ success: false });
}
unwatch();
},
pollingInterval: 1000,
});
});
};

const result = await waitForArbitrationAndClaim();