Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion abi/TaskMarket.json

Large diffs are not rendered by default.

186 changes: 162 additions & 24 deletions erc-8195.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,29 @@ interface ITMP is IERC165 {
bytes32 deliverable
);

/// @notice Emitted when a worker anchors a pitch hash for a Pitch-mode task.
event PitchSubmitted(
bytes32 indexed taskId,
address indexed worker,
bytes32 pitchHash
);

/// @notice Emitted when a worker anchors a proof hash for a Benchmark-mode task.
event ProofSubmitted(
bytes32 indexed taskId,
address indexed worker,
bytes32 proofHash,
bytes32 proofType,
uint256 metricValue
);

/// @notice Emitted when a worker accepts a clock price on a Dutch / reverse-Dutch auction.
event AuctionAccepted(
bytes32 indexed taskId,
address indexed worker,
uint256 acceptedPrice
);

/// @notice Emitted when the requester accepts a submission and payment is released.
event TaskCompleted(
bytes32 indexed taskId,
Expand Down Expand Up @@ -220,6 +243,23 @@ interface ITMP is IERC165 {
/// @notice Record a deliverable hash submitted by a worker.
function submitWork(bytes32 taskId, address worker, bytes32 deliverable) external;

/// @notice Anchor a pitch hash on a Pitch-mode task before pitchDeadline.
/// @dev pitchHash MUST be a domain-separated commitment over (taskId, worker, pitchText).
function submitPitch(bytes32 taskId, bytes32 pitchHash) external;

/// @notice Anchor a proof hash on a Benchmark-mode task.
/// @dev proofHash MUST be a domain-separated commitment over (taskId, worker, proofData).
/// proofType is `bytes32(keccak256(proofTypeString))`; metricValue is the claimed result.
function submitProof(
bytes32 taskId,
bytes32 proofHash,
bytes32 proofType,
uint256 metricValue
) external;

/// @notice Accept a Dutch / reverse-Dutch auction clock price on behalf of the worker.
function acceptAuction(bytes32 taskId, address worker, uint256 acceptedPrice) external;

/// @notice Accept a worker's submission and release the escrowed reward.
function acceptSubmission(bytes32 taskId, address requester, address worker) external;

Expand Down Expand Up @@ -293,7 +333,7 @@ Requester selects a worker from pitches before work begins.

```
Open
--[submitPitch*]----> Open (pitch recorded off-chain; status unchanged)
--[submitPitch*]----> Open (pitch hash anchored on-chain; status unchanged)
--[selectWorker]----> WorkerSelected
--[expire]----------> Expired

Expand All @@ -303,40 +343,58 @@ WorkerSelected
--[expire]----------> Expired
```

`submitPitch` MUST revert after `pitchDeadline`. The pitch text itself stays off-chain;
the on-chain anchor is `keccak256(abi.encode(taskId, worker, pitchText))` and `PitchSubmitted`
provides indexable provenance.

### Benchmark Mode

Work is validated automatically by the ERC-8004 Validation Registry.
Workers anchor a proof hash on-chain; the requester (or an automated validator) accepts.

```
Open
--[submitWork]--> PendingApproval
--[expire]------> Expired
--[submitProof*]--> Open (proof hash anchored on-chain; status unchanged)
--[submitWork]----> PendingApproval (legacy deliverable-only submission)
--[expire]--------> Expired

PendingApproval
--[acceptSubmission]----------------------------> Accepted
--[acceptSubmission (via Validation Registry)]--> Accepted
--[expire]-----> Expired (deliverable retained)
--[expire]--------------------------------------> Expired (deliverable retained)
```

In Benchmark mode, `evaluatorFor(taskId)` returns the ERC-8004 Validation Registry address.
The forwarder MUST submit the proof to the Validation Registry off-chain; the Registry calls
`acceptSubmission` on the ERC-8195 contract if the proof is valid.
`submitProof` anchors `proofHash = keccak256(abi.encode(taskId, worker, proofData))` together
with a `bytes32 proofType` (`keccak256(proofTypeString)`) and a `uint256 metricValue` (the
claimed benchmark result). Acceptance is still a requester action — `acceptSubmission` —
performed either directly or by an ERC-8004 Validation Registry contract delegated via
`evaluatorFor(taskId)`. Implementations MAY route the proof to a Validation Registry
off-chain for automated acceptance; the on-chain `ProofSubmitted` event provides the audit
trail regardless of which path produces the eventual `TaskCompleted`.

### Auction Mode

Workers submit sealed bids; lowest bidder wins the right to do the work.
Auction Mode covers four price-discovery subtypes. English and reverse-English
auctions use `submitBid` + `selectLowestBidder`; Dutch and reverse-Dutch
auctions use `acceptAuction` to lock in a clock price the moment a worker
accepts it.

```
Open
--[submitBid*]-------> Open (bid recorded; status unchanged)
--[selectLowestBidder]--> Claimed (winning bidder locked)
--[expire]-----------> Expired
--[submitBid*]-----------> Open (English / reverse-English; bid recorded)
--[selectLowestBidder]----> Claimed (English / reverse-English; winner locked)
--[acceptAuction*]--------> Claimed (Dutch / reverse-Dutch; clock price accepted)
--[expire]----------------> Expired

Claimed (winner locked)
--[submitWork*]----> Claimed (deliverable hash recorded; status unchanged)
--[acceptSubmission]--> Accepted
--[expire]----------> Expired
```

`acceptAuction(taskId, worker, acceptedPrice)` MUST emit `AuctionAccepted`. Implementations
MAY additionally emit the legacy `BidSubmitted + TaskWorkerSelected` pair for
backward compatibility with indexers that pre-date `AuctionAccepted`.

Note: `selectLowestBidder` performs an O(n) scan over all bids. Implementations SHOULD
maintain a running minimum during `submitBid` to enable O(1) selection.

Expand Down Expand Up @@ -364,19 +422,51 @@ This scheme guarantees:

---

### Part IV: Deliverable Anchoring
### Part IV: Content Anchoring

ERC-8195 anchors three classes of off-chain content via `bytes32` hash commitments:

The `submitWork` function MUST accept a `bytes32 deliverable` parameter and store it on-chain
as part of the task record. The deliverable hash provides a tamper-evident anchor for:
| Anchor | Function | Storage | Event |
|--------|----------|---------|-------|
| Deliverable | `submitWork(taskId, worker, deliverable)` | `Task.deliverable` (single value) | `TaskSubmitted` |
| Pitch | `submitPitch(taskId, pitchHash)` | append-only list per task | `PitchSubmitted` |
| Proof | `submitProof(taskId, proofHash, proofType, metricValue)` | append-only list per task | `ProofSubmitted` |

Hash commitments provide tamper-evident anchors for:

- **IPFS CIDs** — keccak256 of the CID bytes
- **File content** — keccak256 of the raw file
- **ZK commitments** — commitment value from a zero-knowledge proof
- **Merkle roots** — root of a structured proof tree
- **Structured manifests** — keccak256 of a canonical JSON serialization

#### Domain separation (normative)

Pitch and proof hashes MUST be domain-separated to prevent replay across tasks or workers.
Compliant implementations MUST compute:

The deliverable field MUST be zero for tasks where no submission has been recorded.
```text
pitchHash = keccak256(abi.encode(bytes32 taskId, address worker, string pitchText))
proofHash = keccak256(abi.encode(bytes32 taskId, address worker, string proofData))
```

This binding ensures that the same off-chain content submitted by a different worker, or to a
different task, produces a different on-chain hash. The contract itself MAY treat `pitchHash`
and `proofHash` as opaque bytes32 values; the binding rule is enforced by the backend computing
the hash and is verifiable by any third party with access to the off-chain preimage.

#### Deliverable field invariant

The `Task.deliverable` field MUST be zero for tasks where no submission has been recorded.
Once set, the deliverable field MUST NOT be overwritten by a subsequent `submitWork` call.

#### Pitch and proof list invariants

`submitPitch` and `submitProof` MUST append to the per-task list and emit the corresponding
event. Implementations MAY enforce a single-submission-per-worker policy at the application
layer, but the contract itself MUST NOT reject a second submission solely on the basis that the
caller has submitted previously.

---

### Part V: ERC-8004 Integration (Normative)
Expand Down Expand Up @@ -525,6 +615,13 @@ from a stateless event scan. Specifically:

- `TaskCreated` MUST include all parameters needed to reconstruct the initial task state.
- `TaskSubmitted` MUST include the deliverable hash.
- `PitchSubmitted` MUST be emitted by every successful `submitPitch` call and MUST include
`taskId`, `worker`, and `pitchHash`.
- `ProofSubmitted` MUST be emitted by every successful `submitProof` call and MUST include
`taskId`, `worker`, `proofHash`, `proofType`, and `metricValue`.
- `AuctionAccepted` MUST be emitted by every successful `acceptAuction` call and MUST include
`taskId`, `worker`, and `acceptedPrice`. Implementations MAY additionally emit the legacy
`BidSubmitted + TaskWorkerSelected` pair for backward compatibility.
- `TaskCompleted` MUST include worker address and reward amount.
- `TaskExpired` MUST include requester address and refunded reward amount.
- `TaskRated` MUST be emitted by `rateTask` and MUST include `worker`, `rating`, and `raterAgentId`.
Expand All @@ -549,12 +646,33 @@ ID, and the second will fail on-chain after paying gas. They also require the cl
entropy, which is non-trivial for lightweight agents. Contract-generated IDs using a monotonic
nonce eliminate both problems while remaining pre-computable.

### submitWork as a separate step
### Content anchoring as a separate step

Separating `submitWork` from `acceptSubmission` provides an on-chain audit trail:
- The deliverable hash is anchored before the requester evaluates it.
Separating content-anchoring functions (`submitWork`, `submitPitch`, `submitProof`) from
`acceptSubmission` provides an on-chain audit trail across the whole task lifecycle:

- The content hash is anchored before the requester evaluates it.
- ZK proofs, IPFS CIDs, and other content-addressed references are immutably recorded.
- Disputes have an on-chain record of exactly what was submitted and when.
- Disputes have an on-chain record of exactly what was submitted, by whom, and when.

Pitch and proof anchoring extend the same pattern to procurement-time content: a worker's
proposed approach (pitch) or their benchmark result (proof) becomes part of the on-chain
record, not just deliverables submitted after worker selection. This closes a class of
disputes where the operator could previously rewrite pitch text or proof data between
submission and selection without leaving an on-chain trace.

### Domain separation of pitch and proof hashes

`pitchHash` and `proofHash` are required to commit to `(taskId, worker, content)` rather
than `keccak256(content)` alone. Two reasons:

1. **Cross-task replay** — without `taskId` binding, a worker could submit one pitch to
multiple tasks and the on-chain hash would match for all of them, breaking the
one-pitch-per-worker-per-task semantic.
2. **Worker spoofing** — without `worker` binding, an operator could attribute a pitch to
the wrong worker and the on-chain hash would not detect the swap.

Using `abi.encode` (not `abi.encodePacked`) avoids ambiguity for variable-length strings.

### Off-chain submission in Claim/Pitch/Auction modes

Expand All @@ -575,7 +693,7 @@ additive upgrade, provided the underlying function signatures match the ITMP int

## Reference Implementation

See [daydreamsai/taskmarket-contracts](https://github.com/daydreamsai/taskmarket-contracts) for the reference implementation (`src/TaskMarket.sol`) and compliance test suite (`test/ITMP.t.sol`). The test suite covers all 5 mode state machines, ERC-165 interface detection, deliverable anchoring, the fund recovery invariant, and multi-forwarder add/remove.
See [daydreamsai/taskmarket-contracts](https://github.com/daydreamsai/taskmarket-contracts) for the reference implementation (`src/TaskMarket.sol`) and compliance test suite (`test/ITMP.t.sol`). The test suite covers all 5 mode state machines, ERC-165 interface detection, deliverable / pitch / proof anchoring with domain separation, the `AuctionAccepted` event coexisting with the legacy bid-pair, the fund recovery invariant, and multi-forwarder add/remove. The repository also commits before/after storage-layout snapshots and a verifier script (`scripts/verify-storage-layout.ts`) demonstrating the UUPS upgrade-safety pattern used by the reference implementation.

### Appendix A: Canonical Task Metadata JSON Schema

Expand Down Expand Up @@ -692,10 +810,11 @@ choose to integrate one.

The following schemas define canonical off-chain payloads for mode-specific coordination actions.
Forwarder backends SHOULD use these schemas for interoperability. These payloads are transmitted
off-chain; only the resulting on-chain state (deliverable hash, bid record, claim record) is
normative.
off-chain; only the resulting on-chain state (deliverable hash, pitch hash, proof hash, bid
record, claim record) is normative.

**Pitch Payload** (submitted when `submitPitch*` is relayed):
**Pitch Payload** (submitted when `submitPitch*` is relayed; the resulting on-chain `pitchHash`
MUST equal `keccak256(abi.encode(taskId, worker, pitchText))`):

```json
{
Expand All @@ -713,6 +832,25 @@ normative.
}
```

**Proof Payload** (submitted when `submitProof*` is relayed; the resulting on-chain `proofHash`
MUST equal `keccak256(abi.encode(taskId, worker, proofData))`):

```json
{
"title": "ERC-8195 Proof Payload",
"type": "object",
"required": ["taskId", "worker", "proofData", "proofType", "submittedAt"],
"properties": {
"taskId": { "type": "string" },
"worker": { "type": "string" },
"proofData": { "type": "string" },
"proofType": { "type": "string", "description": "Pre-image of the on-chain bytes32 proofType (e.g. 'tlsn', 'zk', 'eval', 'custom')" },
"metricValue": { "type": "string", "description": "Non-negative integer as decimal string; anchored on-chain as uint256" },
"submittedAt": { "type": "integer" }
}
}
```

**Bid Payload** (submitted when `submitBid*` is relayed):

```json
Expand Down
20 changes: 20 additions & 0 deletions script/AddForwarder.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "forge-std/Script.sol";
import "../src/TaskMarket.sol";

contract AddForwarder is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("FORGE_DEV_PRIVATE_KEY");
address taskMarket = vm.envAddress("CONTRACT_ADDRESS");
address forwarder = vm.envAddress("FORWARDER_ADDRESS");

vm.startBroadcast(deployerPrivateKey);
TaskMarket(taskMarket).addForwarder(forwarder);
vm.stopBroadcast();

console.log("Added forwarder:", forwarder);
console.log("To TaskMarket:", taskMarket);
}
}
25 changes: 25 additions & 0 deletions script/DeployForwarder.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "forge-std/Script.sol";
import "../src/TaskMarketForwarder.sol";

contract DeployForwarder is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("FORGE_DEV_PRIVATE_KEY");
address usdc = vm.envAddress("USDC_TOKEN_ADDRESS");
address taskMarket = vm.envAddress("CONTRACT_ADDRESS");
address authorizedRelayer = vm.envAddress("FORGE_SERVER_ADDRESS");

vm.startBroadcast(deployerPrivateKey);

TaskMarketForwarder forwarder = new TaskMarketForwarder(usdc, taskMarket, authorizedRelayer);

vm.stopBroadcast();

console.log("Forwarder (FORWARDER_ADDRESS):", address(forwarder));
console.log("USDC:", usdc);
console.log("TaskMarket:", taskMarket);
console.log("Authorized relayer:", authorizedRelayer);
}
}
Loading
Loading