Contract Explorer

AgentEscrow ERC-8183 Smart Contract

Sepolia Testnet0x00000000...Demo (Not Deployed)
Solidity:^0.8.24
License:MIT
Standard:ERC-8183
Dependencies:OpenZeppelin 5.x
Audit:Pending
AgentEscrow.sol
1"color:#64748b">// SPDX-License-Identifier: MIT
2"color:#818cf8">pragma solidity ^0.8.24;
3 
4"color:#818cf8">import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5"color:#818cf8">import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6"color:#818cf8">import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
7 
8"color:#64748b">/// @title IACPHook — Hook "color:#818cf8">interface "color:#818cf8">for ERC-8183 extensions
9"color:#818cf8">interface IACPHook {
10 "color:#818cf8">function beforeAction("color:#818cf8">"color:#2dd4bf">uint256 jobId, "color:#2dd4bf">bytes4 action, "color:#818cf8">"color:#2dd4bf">address caller) "color:#818cf8">external;
11 "color:#818cf8">function afterAction("color:#818cf8">"color:#2dd4bf">uint256 jobId, "color:#2dd4bf">bytes4 action, "color:#818cf8">"color:#2dd4bf">address caller) "color:#818cf8">external;
12}
13 
14"color:#64748b">/// @title AgentEscrow — ERC-8183 Agentic Commerce Protocol
15"color:#64748b">/// @notice Trustless escrow "color:#818cf8">for AI agent commerce with evaluator attestation
16"color:#64748b">/// @dev Implements the ERC-8183 standard "color:#818cf8">for agentic job lifecycle management
17"color:#818cf8">contract AgentEscrow is ReentrancyGuard {
18 using SafeERC20 "color:#818cf8">for IERC20;
19 
20 "color:#64748b">// ─── Enums ──────────────────────────────────────────────────────────────
21 "color:#818cf8">enum JobState { Open, Funded, Submitted, Completed, Rejected, Expired }
22 
23 "color:#64748b">// ─── Structs ─────────────────────────────────────────────────────────────
24 "color:#818cf8">struct Job {
25 "color:#818cf8">"color:#2dd4bf">address client; "color:#64748b">// Creates and funds the job
26 "color:#818cf8">"color:#2dd4bf">address provider; "color:#64748b">// Executes work and submits deliverable
27 "color:#818cf8">"color:#2dd4bf">address evaluator; "color:#64748b">// Attests completion or rejects (FULLY TRUSTED)
28 "color:#818cf8">"color:#2dd4bf">address token; "color:#64748b">// ERC-20 token used "color:#818cf8">for payment
29 "color:#818cf8">"color:#2dd4bf">uint256 budget; "color:#64748b">// Agreed payment amount (in token units)
30 "color:#818cf8">"color:#2dd4bf">uint256 expiredAt; "color:#64748b">// Unix timestamp after which refund is claimable
31 "color:#818cf8">"color:#2dd4bf">bytes32 deliverable; "color:#64748b">// "color:#818cf8">"color:#2dd4bf">bytes32 hash of submitted work
32 JobState state;
33 "color:#818cf8">"color:#2dd4bf">address hook; "color:#64748b">// Optional IACPHook "color:#818cf8">for composable extensions
34 "color:#818cf8">"color:#2dd4bf">string description; "color:#64748b">// Off-chain job description or IPFS CID
35 }
36 
37 "color:#64748b">// ─── State ───────────────────────────────────────────────────────────────
38 "color:#818cf8">"color:#2dd4bf">uint256 "color:#818cf8">public jobCount;
39 "color:#818cf8">mapping("color:#818cf8">"color:#2dd4bf">uint256 => Job) "color:#818cf8">public jobs;
40 
41 "color:#64748b">// ─── Events ──────────────────────────────────────────────────────────────
42 "color:#818cf8">event JobCreated(
43 "color:#818cf8">"color:#2dd4bf">uint256 indexed jobId,
44 "color:#818cf8">"color:#2dd4bf">address indexed client,
45 "color:#818cf8">"color:#2dd4bf">address indexed provider,
46 "color:#818cf8">"color:#2dd4bf">address evaluator,
47 "color:#818cf8">"color:#2dd4bf">address token,
48 "color:#818cf8">"color:#2dd4bf">uint256 expiredAt
49 );
50 "color:#818cf8">event ProviderSet("color:#818cf8">"color:#2dd4bf">uint256 indexed jobId, "color:#818cf8">"color:#2dd4bf">address provider);
51 "color:#818cf8">event BudgetSet("color:#818cf8">"color:#2dd4bf">uint256 indexed jobId, "color:#818cf8">"color:#2dd4bf">uint256 amount);
52 "color:#818cf8">event JobFunded("color:#818cf8">"color:#2dd4bf">uint256 indexed jobId, "color:#818cf8">"color:#2dd4bf">uint256 amount);
53 "color:#818cf8">event WorkSubmitted("color:#818cf8">"color:#2dd4bf">uint256 indexed jobId, "color:#818cf8">"color:#2dd4bf">bytes32 deliverableHash);
54 "color:#818cf8">event JobCompleted("color:#818cf8">"color:#2dd4bf">uint256 indexed jobId, "color:#818cf8">"color:#2dd4bf">string reason);
55 "color:#818cf8">event JobRejected("color:#818cf8">"color:#2dd4bf">uint256 indexed jobId, "color:#818cf8">"color:#2dd4bf">string reason);
56 "color:#818cf8">event RefundClaimed("color:#818cf8">"color:#2dd4bf">uint256 indexed jobId, "color:#818cf8">"color:#2dd4bf">address client, "color:#818cf8">"color:#2dd4bf">uint256 amount);
57 
58 "color:#64748b">// ─── Errors ──────────────────────────────────────────────────────────────
59 "color:#818cf8">error Unauthorized();
60 "color:#818cf8">error InvalidState(JobState current, JobState required);
61 "color:#818cf8">error BudgetMismatch("color:#818cf8">"color:#2dd4bf">uint256 expected, "color:#818cf8">"color:#2dd4bf">uint256 actual);
62 "color:#818cf8">error JobExpired();
63 "color:#818cf8">error JobNotExpired();
64 "color:#818cf8">error ZeroAddress();
65 "color:#818cf8">error InvalidExpiry();
66 
67 "color:#64748b">// ─── Modifiers ───────────────────────────────────────────────────────────
68 "color:#818cf8">modifier onlyClient("color:#818cf8">"color:#2dd4bf">uint256 jobId) {
69 "color:#818cf8">if (msg.sender != jobs[jobId].client) "color:#818cf8">revert Unauthorized();
70 _;
71 }
72 
73 "color:#818cf8">modifier onlyProvider("color:#818cf8">"color:#2dd4bf">uint256 jobId) {
74 "color:#818cf8">if (msg.sender != jobs[jobId].provider) "color:#818cf8">revert Unauthorized();
75 _;
76 }
77 
78 "color:#818cf8">modifier onlyEvaluator("color:#818cf8">"color:#2dd4bf">uint256 jobId) {
79 "color:#818cf8">if (msg.sender != jobs[jobId].evaluator) "color:#818cf8">revert Unauthorized();
80 _;
81 }
82 
83 "color:#818cf8">modifier inState("color:#818cf8">"color:#2dd4bf">uint256 jobId, JobState required) {
84 "color:#818cf8">if (jobs[jobId].state != required)
85 "color:#818cf8">revert InvalidState(jobs[jobId].state, required);
86 _;
87 }
88 
89 "color:#818cf8">modifier notExpired("color:#818cf8">"color:#2dd4bf">uint256 jobId) {
90 "color:#818cf8">if (block.timestamp >= jobs[jobId].expiredAt) "color:#818cf8">revert JobExpired();
91 _;
92 }
93 
94 "color:#64748b">// ─── Hook helper ─────────────────────────────────────────────────────────
95 "color:#818cf8">function _callHook("color:#818cf8">"color:#2dd4bf">uint256 jobId, "color:#2dd4bf">bytes4 action, "color:#818cf8">"color:#2dd4bf">bool before) "color:#818cf8">internal {
96 "color:#818cf8">"color:#2dd4bf">address hook = jobs[jobId].hook;
97 "color:#818cf8">if (hook == "color:#818cf8">"color:#2dd4bf">address(0)) "color:#818cf8">return;
98 "color:#818cf8">if (before) IACPHook(hook).beforeAction(jobId, action, msg.sender);
99 "color:#818cf8">else IACPHook(hook).afterAction(jobId, action, msg.sender);
100 }
101 
102 "color:#64748b">// ─── Core Functions ──────────────────────────────────────────────────────
103 
104 "color:#64748b">/// @notice Create a "color:#818cf8">new escrow job
105 "color:#64748b">/// @param provider Address of the service provider (can be "color:#818cf8">"color:#2dd4bf">address(0) "color:#818cf8">for open bidding)
106 "color:#64748b">/// @param evaluator Address of the trusted evaluator (can be client itself)
107 "color:#64748b">/// @param token ERC-20 token "color:#818cf8">"color:#2dd4bf">address "color:#818cf8">for payment
108 "color:#64748b">/// @param expiredAt Unix timestamp "color:#818cf8">for job expiry
109 "color:#64748b">/// @param description Off-chain description or IPFS CID
110 "color:#64748b">/// @param hook Optional hook "color:#818cf8">contract "color:#818cf8">"color:#2dd4bf">address ("color:#818cf8">"color:#2dd4bf">address(0) to disable)
111 "color:#64748b">/// @"color:#818cf8">return jobId The ID of the newly created job
112 "color:#818cf8">function createJob(
113 "color:#818cf8">"color:#2dd4bf">address provider,
114 "color:#818cf8">"color:#2dd4bf">address evaluator,
115 "color:#818cf8">"color:#2dd4bf">address token,
116 "color:#818cf8">"color:#2dd4bf">uint256 expiredAt,
117 "color:#818cf8">"color:#2dd4bf">string "color:#818cf8">calldata description,
118 "color:#818cf8">"color:#2dd4bf">address hook
119 ) "color:#818cf8">external "color:#818cf8">returns ("color:#818cf8">"color:#2dd4bf">uint256 jobId) {
120 "color:#818cf8">if (evaluator == "color:#818cf8">"color:#2dd4bf">address(0)) "color:#818cf8">revert ZeroAddress();
121 "color:#818cf8">if (token == "color:#818cf8">"color:#2dd4bf">address(0)) "color:#818cf8">revert ZeroAddress();
122 "color:#818cf8">if (expiredAt <= block.timestamp) "color:#818cf8">revert InvalidExpiry();
123 
124 jobId = ++jobCount;
125 jobs[jobId] = Job({
126 client: msg.sender,
127 provider: provider,
128 evaluator: evaluator,
129 token: token,
130 budget: 0,
131 expiredAt: expiredAt,
132 deliverable: "color:#818cf8">"color:#2dd4bf">bytes32(0),
133 state: JobState.Open,
134 hook: hook,
135 description: description
136 });
137 
138 "color:#818cf8">emit JobCreated(jobId, msg.sender, provider, evaluator, token, expiredAt);
139 }
140 
141 "color:#64748b">/// @notice Set or update the provider ("color:#818cf8">for bidding flows)
142 "color:#818cf8">function setProvider("color:#818cf8">"color:#2dd4bf">uint256 jobId, "color:#818cf8">"color:#2dd4bf">address provider)
143 "color:#818cf8">external
144 onlyClient(jobId)
145 inState(jobId, JobState.Open)
146 {
147 _callHook(jobId, "color:#818cf8">this.setProvider.selector, true);
148 jobs[jobId].provider = provider;
149 "color:#818cf8">emit ProviderSet(jobId, provider);
150 _callHook(jobId, "color:#818cf8">this.setProvider.selector, false);
151 }
152 
153 "color:#64748b">/// @notice Set the agreed budget "color:#818cf8">for the job
154 "color:#818cf8">function setBudget("color:#818cf8">"color:#2dd4bf">uint256 jobId, "color:#818cf8">"color:#2dd4bf">uint256 amount)
155 "color:#818cf8">external
156 onlyClient(jobId)
157 inState(jobId, JobState.Open)
158 {
159 _callHook(jobId, "color:#818cf8">this.setBudget.selector, true);
160 jobs[jobId].budget = amount;
161 "color:#818cf8">emit BudgetSet(jobId, amount);
162 _callHook(jobId, "color:#818cf8">this.setBudget.selector, false);
163 }
164 
165 "color:#64748b">/// @notice Fund the job — locks ERC-20 tokens into escrow
166 "color:#64748b">/// @param jobId The job to fund
167 "color:#64748b">/// @param expectedBudget Must match job.budget to prevent front-running
168 "color:#818cf8">function fund("color:#818cf8">"color:#2dd4bf">uint256 jobId, "color:#818cf8">"color:#2dd4bf">uint256 expectedBudget)
169 "color:#818cf8">external
170 nonReentrant
171 onlyClient(jobId)
172 inState(jobId, JobState.Open)
173 notExpired(jobId)
174 {
175 Job "color:#818cf8">storage job = jobs[jobId];
176 "color:#818cf8">if (job.budget != expectedBudget)
177 "color:#818cf8">revert BudgetMismatch(job.budget, expectedBudget);
178 
179 _callHook(jobId, "color:#818cf8">this.fund.selector, true);
180 IERC20(job.token).safeTransferFrom(msg.sender, "color:#818cf8">"color:#2dd4bf">address("color:#818cf8">this), job.budget);
181 job.state = JobState.Funded;
182 "color:#818cf8">emit JobFunded(jobId, job.budget);
183 _callHook(jobId, "color:#818cf8">this.fund.selector, false);
184 }
185 
186 "color:#64748b">/// @notice Provider submits work deliverable hash
187 "color:#64748b">/// @param jobId The funded job
188 "color:#64748b">/// @param deliverableHash "color:#818cf8">"color:#2dd4bf">bytes32 hash of the deliverable (IPFS CID, file hash, etc.)
189 "color:#818cf8">function submit("color:#818cf8">"color:#2dd4bf">uint256 jobId, "color:#818cf8">"color:#2dd4bf">bytes32 deliverableHash)
190 "color:#818cf8">external
191 onlyProvider(jobId)
192 inState(jobId, JobState.Funded)
193 notExpired(jobId)
194 {
195 _callHook(jobId, "color:#818cf8">this.submit.selector, true);
196 jobs[jobId].deliverable = deliverableHash;
197 jobs[jobId].state = JobState.Submitted;
198 "color:#818cf8">emit WorkSubmitted(jobId, deliverableHash);
199 _callHook(jobId, "color:#818cf8">this.submit.selector, false);
200 }
201 
202 "color:#64748b">/// @notice Evaluator approves work — releases escrow to provider
203 "color:#64748b">/// @param jobId The submitted job
204 "color:#64748b">/// @param reason Human-readable approval reason
205 "color:#818cf8">function complete("color:#818cf8">"color:#2dd4bf">uint256 jobId, "color:#818cf8">"color:#2dd4bf">string "color:#818cf8">calldata reason)
206 "color:#818cf8">external
207 nonReentrant
208 onlyEvaluator(jobId)
209 inState(jobId, JobState.Submitted)
210 {
211 _callHook(jobId, "color:#818cf8">this.complete.selector, true);
212 Job "color:#818cf8">storage job = jobs[jobId];
213 job.state = JobState.Completed;
214 IERC20(job.token).safeTransfer(job.provider, job.budget);
215 "color:#818cf8">emit JobCompleted(jobId, reason);
216 _callHook(jobId, "color:#818cf8">this.complete.selector, false);
217 }
218 
219 "color:#64748b">/// @notice Evaluator or client rejects work — refunds client
220 "color:#64748b">/// @param jobId The submitted job
221 "color:#64748b">/// @param reason Human-readable rejection reason
222 "color:#818cf8">function reject("color:#818cf8">"color:#2dd4bf">uint256 jobId, "color:#818cf8">"color:#2dd4bf">string "color:#818cf8">calldata reason)
223 "color:#818cf8">external
224 nonReentrant
225 inState(jobId, JobState.Submitted)
226 {
227 Job "color:#818cf8">storage job = jobs[jobId];
228 "color:#818cf8">if (msg.sender != job.evaluator && msg.sender != job.client)
229 "color:#818cf8">revert Unauthorized();
230 
231 _callHook(jobId, "color:#818cf8">this.reject.selector, true);
232 job.state = JobState.Rejected;
233 IERC20(job.token).safeTransfer(job.client, job.budget);
234 "color:#818cf8">emit JobRejected(jobId, reason);
235 _callHook(jobId, "color:#818cf8">this.reject.selector, false);
236 }
237 
238 "color:#64748b">/// @notice Claim refund after expiry — NOT hookable (guaranteed recovery)
239 "color:#64748b">/// @param jobId The expired job (must be in Open or Funded state)
240 "color:#818cf8">function claimRefund("color:#818cf8">"color:#2dd4bf">uint256 jobId) "color:#818cf8">external nonReentrant {
241 Job "color:#818cf8">storage job = jobs[jobId];
242 "color:#818cf8">if (block.timestamp < job.expiredAt) "color:#818cf8">revert JobNotExpired();
243 "color:#818cf8">if (job.state != JobState.Open && job.state != JobState.Funded)
244 "color:#818cf8">revert InvalidState(job.state, JobState.Funded);
245 
246 "color:#818cf8">"color:#2dd4bf">uint256 amount = job.budget;
247 job.state = JobState.Expired;
248 "color:#818cf8">if (amount > 0) {
249 IERC20(job.token).safeTransfer(job.client, amount);
250 }
251 "color:#818cf8">emit RefundClaimed(jobId, job.client, amount);
252 }
253 
254 "color:#64748b">/// @notice Get full job details
255 "color:#818cf8">function getJob("color:#818cf8">"color:#2dd4bf">uint256 jobId) "color:#818cf8">external "color:#818cf8">view "color:#818cf8">returns (Job "color:#818cf8">memory) {
256 "color:#818cf8">return jobs[jobId];
257 }
258}