Skip to main content

Escrow Flow (pt 3 - Composing Demands)

The logical arbiter contracts AnyArbiter, AllArbiter, and NotArbiter can be used to compose demands. For example, you could demand that

  • a task can only be completed by a particular account, and is validated by a trusted third party
  • a task is completed before a particular deadline, and validated by any of a list of trusted third parties
  • uptime for a server provision is above a certain threshold, and is actually the expected service

Using logical arbiters

You can use AllArbiter to demand multiple conditions at the same time. For example, that your task is completed by a particular individual before a deadline, and validated by a third party.

// Example: Require job completed by specific recipient AND validated by oracle
bytes memory recipientDemand = abi.encode(
RecipientArbiter.DemandData({
recipient: bob // Only Bob can fulfill this
})
);

bytes memory oracleDemand = abi.encode(
TrustedOracleArbiter.DemandData({
oracle: charlie, // Charlie validates the result
data: abi.encode("complete the analysis task")
})
);

// Compose with AllArbiter - both conditions must be met
bytes memory composedDemand = abi.encode(
AllArbiter.DemandData({
arbiters: [address(recipientArbiter), address(trustedOracleArbiter)],
demands: [recipientDemand, oracleDemand]
})
);

// Deposit escrow with the composed demand
IERC20(erc20Token).approve(address(erc20EscrowObligation), 100 * 10**18);
bytes32 escrowUid = erc20EscrowObligation.doObligation(
ERC20EscrowObligation.ObligationData({
token: erc20Token,
amount: 100 * 10**18,
arbiter: address(allArbiter),
demand: composedDemand
}),
block.timestamp + 86400
);

You can use AnyArbiter when multiple alternative conditions can be considered valid. For example, if you accept a decision from any of a list of trusted third party oracles, or if there are different proof mechanisms that equally ensure the validity of a result.

// Example: Accept validation from ANY of multiple trusted oracles
bytes memory oracle1Demand = abi.encode(
TrustedOracleArbiter.DemandData({
oracle: charlie,
data: abi.encode("verify computation result")
})
);

bytes memory oracle2Demand = abi.encode(
TrustedOracleArbiter.DemandData({
oracle: dave,
data: abi.encode("verify computation result")
})
);

bytes memory oracle3Demand = abi.encode(
TrustedOracleArbiter.DemandData({
oracle: eve,
data: abi.encode("verify computation result")
})
);

// Compose with AnyArbiter - any one oracle can validate
bytes memory composedDemand = abi.encode(
AnyArbiter.DemandData({
arbiters: [
address(trustedOracleArbiter),
address(trustedOracleArbiter),
address(trustedOracleArbiter)
],
demands: [oracle1Demand, oracle2Demand, oracle3Demand]
})
);

// Create escrow with flexible oracle validation
bytes32 escrowUid = erc20EscrowObligation.doObligation(
ERC20EscrowObligation.ObligationData({
token: erc20Token,
amount: 50 * 10**18,
arbiter: address(anyArbiter),
demand: composedDemand
}),
block.timestamp + 7200 // 2 hour expiration
);

Logical arbiters can be stacked. For example, you could demand that a job is completed before a deadline by a particular party, and validated by any of a list of trusted oracles.

// Example: Complex composition - deadline AND recipient AND (oracle1 OR oracle2)
bytes memory deadlineDemand = abi.encode(
TimeBeforeArbiter.DemandData({
time: uint64(block.timestamp + 3600) // Must complete within 1 hour
})
);

bytes memory recipientDemand = abi.encode(
RecipientArbiter.DemandData({
recipient: bob // Must be fulfilled by Bob
})
);

// Create the OR condition for oracles
bytes memory oracle1Demand = abi.encode(
TrustedOracleArbiter.DemandData({
oracle: charlie,
data: abi.encode("validate result")
})
);

bytes memory oracle2Demand = abi.encode(
TrustedOracleArbiter.DemandData({
oracle: dave,
data: abi.encode("validate result")
})
);

bytes memory oracleOrDemand = abi.encode(
AnyArbiter.DemandData({
arbiters: [address(trustedOracleArbiter), address(trustedOracleArbiter)],
demands: [oracle1Demand, oracle2Demand]
})
);

// Combine all conditions with AllArbiter
bytes memory finalDemand = abi.encode(
AllArbiter.DemandData({
arbiters: [
address(timeBeforeArbiter),
address(recipientArbiter),
address(anyArbiter)
],
demands: [
deadlineDemand,
recipientDemand,
oracleOrDemand
]
})
);

// Create the escrow with nested logical arbiters
bytes32 escrowUid = erc20EscrowObligation.doObligation(
ERC20EscrowObligation.ObligationData({
token: erc20Token,
amount: 200 * 10**18,
arbiter: address(allArbiter),
demand: finalDemand
}),
block.timestamp + 7200 // 2 hour overall expiration
);

Parsing composed demands

When parsing composed demands, you can use the arbiter address of each subdemand as a type id. You could communicate the whole composed demand off-chain, and only validate that the on-chain demand matches what was communicated.

// Example: Validate that on-chain demand matches expected structure
function validateDemand(
bytes memory onChainDemand,
bytes memory expectedDemand
) public pure returns (bool) {
// Simply check if the demands match
return keccak256(onChainDemand) == keccak256(expectedDemand);
}

// Or decode and validate specific parts
function parseAllArbiterDemand(bytes memory demand)
public
pure
returns (address[] memory arbiters, bytes[] memory demands)
{
AllArbiter.DemandData memory demandData = abi.decode(
demand,
(AllArbiter.DemandData)
);
return (demandData.arbiters, demandData.demands);
}

// Example usage
(address[] memory arbiters, bytes[] memory subDemands) = parseAllArbiterDemand(composedDemand);
require(arbiters[0] == address(timeBeforeArbiter), "First arbiter must be TimeBeforeArbiter");
require(arbiters[1] == address(recipientArbiter), "Second arbiter must be RecipientArbiter");

If this isn't possible, you could keep a local record mapping sub-arbiters that you support (including each logical arbiter) to their DemandData formats, and parse demands recursively.

// Example: Recursive demand parser contract
contract DemandParser {
struct ArbiterInfo {
string name;
string demandFormat;
bool isLogical;
}

mapping(address => ArbiterInfo) public arbiterRegistry;

constructor() {
// Register known arbiters
arbiterRegistry[address(timeBeforeArbiter)] = ArbiterInfo(
"TimeBeforeArbiter",
"(uint64)",
false
);
arbiterRegistry[address(recipientArbiter)] = ArbiterInfo(
"RecipientArbiter",
"(address)",
false
);
arbiterRegistry[address(trustedOracleArbiter)] = ArbiterInfo(
"TrustedOracleArbiter",
"(address,bytes)",
false
);
arbiterRegistry[address(allArbiter)] = ArbiterInfo(
"AllArbiter",
"(address[],bytes[])",
true
);
arbiterRegistry[address(anyArbiter)] = ArbiterInfo(
"AnyArbiter",
"(address[],bytes[])",
true
);
}

function parseDemand(address arbiter, bytes memory demand)
public
view
returns (string memory arbiterName, bytes memory parsedData)
{
ArbiterInfo memory info = arbiterRegistry[arbiter];
require(bytes(info.name).length > 0, "Unknown arbiter");

if (info.isLogical) {
// Recursively parse logical arbiters
(address[] memory subArbiters, bytes[] memory subDemands) =
abi.decode(demand, (address[], bytes[]));

// Continue parsing sub-demands...
for (uint i = 0; i < subArbiters.length; i++) {
(string memory subName,) = parseDemand(subArbiters[i], subDemands[i]);
// Process sub-demand...
}
}

return (info.name, demand);
}
}