Skip to content

Commit 607b54a

Browse files
Adeyemirclaude
andcommitted
feat: route payouts to agent owner, not agent wallet
Agent wallet (operational) only signs completeJob + submitProofOfWork. All USDC payouts (autoSettle, settleJob, resolveDisputeByOwner, cross-chain CCTP) now go to identityRegistry.ownerOf(agentId) — the agent owner's wallet. - Add _resolvePayoutAddress() helper on XcrowEscrow - Require agentId > 0 on createJobByWallet (no anonymous hires) - Require erc8004AgentId > 0 on hireAgentByWalletWithPermit - Update cross-chain mint recipient to agent owner - Update test assertions: payout checks use agentOwner address Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 20d92a7 commit 607b54a

4 files changed

Lines changed: 49 additions & 24 deletions

File tree

src/core/XcrowEscrow.sol

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,27 @@ contract XcrowEscrow is IXcrowEscrow, ReentrancyGuard, Pausable, Ownable {
5959
nextJobId = 1;
6060
}
6161

62+
// --- Internal Helpers ---
63+
64+
/// @notice Resolve the payout address for a job — always the agent owner's wallet
65+
/// @dev Agent wallet signs completions; owner wallet receives payment
66+
function _resolvePayoutAddress(uint256 agentId) internal view returns (address) {
67+
address ownerAddr = identityRegistry.ownerOf(agentId);
68+
require(ownerAddr != address(0), "Agent owner not found");
69+
return ownerAddr;
70+
}
71+
6272
// --- Core Functions ---
6373

64-
/// @notice Create a job by specifying the agent's wallet address directly (no ERC-8004 lookup needed)
65-
function createJobByWallet(address agentWallet, uint256 amount, bytes32 taskHash, uint256 deadline)
74+
/// @notice Create a job by specifying the agent's wallet address directly
75+
/// @dev agentId is required so payout can be routed to the agent owner
76+
function createJobByWallet(
77+
address agentWallet,
78+
uint256 amount,
79+
bytes32 taskHash,
80+
uint256 deadline,
81+
uint256 agentId
82+
)
6683
external
6784
nonReentrant
6885
whenNotPaused
@@ -72,13 +89,14 @@ contract XcrowEscrow is IXcrowEscrow, ReentrancyGuard, Pausable, Ownable {
7289
require(deadline > block.timestamp, "Deadline must be future");
7390
require(taskHash != bytes32(0), "Task hash required");
7491
require(agentWallet != address(0), "Invalid agent wallet");
92+
require(agentId > 0, "Agent ID required");
7593

7694
uint256 platformFee = (amount * protocolFeeBps) / 10000;
7795
jobId = nextJobId++;
7896

7997
jobs[jobId] = XcrowTypes.Job({
8098
jobId: jobId,
81-
agentId: 0,
99+
agentId: agentId,
82100
agentChainId: uint32(block.chainid),
83101
client: msg.sender,
84102
agentWallet: agentWallet,
@@ -96,10 +114,11 @@ contract XcrowEscrow is IXcrowEscrow, ReentrancyGuard, Pausable, Ownable {
96114
});
97115

98116
clientJobs[msg.sender].push(jobId);
117+
agentJobs[agentId].push(jobId);
99118
agentWalletJobs[agentWallet].push(jobId);
100119

101120
usdc.safeTransferFrom(msg.sender, address(this), amount);
102-
emit JobCreated(jobId, 0, msg.sender, amount);
121+
emit JobCreated(jobId, agentId, msg.sender, amount);
103122
}
104123

105124
/// @inheritdoc IXcrowEscrow
@@ -250,12 +269,13 @@ contract XcrowEscrow is IXcrowEscrow, ReentrancyGuard, Pausable, Ownable {
250269
require(block.timestamp >= job.proofSubmittedAt + settlementWindow, "Settlement window not elapsed");
251270

252271
uint256 agentPayout = job.amount - job.platformFee;
272+
address payoutAddress = _resolvePayoutAddress(job.agentId);
253273

254274
job.status = XcrowTypes.JobStatus.Settled;
255275
job.settledAt = block.timestamp;
256276
accumulatedFees += job.platformFee;
257277

258-
usdc.safeTransfer(job.agentWallet, agentPayout);
278+
usdc.safeTransfer(payoutAddress, agentPayout);
259279

260280
emit JobSettled(jobId, agentPayout, job.platformFee);
261281
}
@@ -267,15 +287,16 @@ contract XcrowEscrow is IXcrowEscrow, ReentrancyGuard, Pausable, Ownable {
267287
require(msg.sender == job.client, "Only client can settle");
268288

269289
uint256 agentPayout = job.amount - job.platformFee;
290+
address payoutAddress = _resolvePayoutAddress(job.agentId);
270291

271292
job.status = XcrowTypes.JobStatus.Settled;
272293
job.settledAt = block.timestamp;
273294

274295
// Accumulate protocol fees
275296
accumulatedFees += job.platformFee;
276297

277-
// Pay the agent
278-
usdc.safeTransfer(job.agentWallet, agentPayout);
298+
// Pay the agent owner
299+
usdc.safeTransfer(payoutAddress, agentPayout);
279300

280301
emit JobSettled(jobId, agentPayout, job.platformFee);
281302
}
@@ -337,10 +358,11 @@ contract XcrowEscrow is IXcrowEscrow, ReentrancyGuard, Pausable, Ownable {
337358

338359
if (favorAgent) {
339360
uint256 agentPayout = job.amount - job.platformFee;
361+
address payoutAddress = _resolvePayoutAddress(job.agentId);
340362
job.status = XcrowTypes.JobStatus.Settled;
341363
job.settledAt = block.timestamp;
342364
accumulatedFees += job.platformFee;
343-
usdc.safeTransfer(job.agentWallet, agentPayout);
365+
usdc.safeTransfer(payoutAddress, agentPayout);
344366
emit JobSettled(jobId, agentPayout, job.platformFee);
345367
} else {
346368
job.status = XcrowTypes.JobStatus.Refunded;

src/core/XcrowRouter.sol

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ contract XcrowRouter is ReentrancyGuard, Pausable, Ownable {
6767

6868
// --- Core Functions ---
6969

70-
/// @notice Hire an agent by wallet address with EIP-2612 permit — no ERC-8004 ID needed
71-
/// @param erc8004AgentId The agent's ERC-8004 token ID for reputation tracking (0 if unknown)
70+
/// @notice Hire an agent by wallet address with EIP-2612 permit
71+
/// @param erc8004AgentId The agent's ERC-8004 token ID (required for payout routing)
7272
function hireAgentByWalletWithPermit(
7373
address agentWallet,
7474
uint256 amount,
@@ -80,13 +80,14 @@ contract XcrowRouter is ReentrancyGuard, Pausable, Ownable {
8080
bytes32 r,
8181
bytes32 s
8282
) external nonReentrant whenNotPaused returns (uint256 jobId) {
83+
require(erc8004AgentId > 0, "Agent ID required");
8384
IERC20Permit(address(usdc)).permit(msg.sender, address(this), amount, permitDeadline, v, r, s);
8485
usdc.safeTransferFrom(msg.sender, address(this), amount);
8586
usdc.forceApprove(address(escrow), amount);
8687

87-
jobId = escrow.createJobByWallet(agentWallet, amount, taskHash, deadline);
88+
jobId = escrow.createJobByWallet(agentWallet, amount, taskHash, deadline, erc8004AgentId);
8889
originalClient[jobId] = msg.sender;
89-
if (erc8004AgentId != 0) jobERC8004AgentId[jobId] = erc8004AgentId;
90+
jobERC8004AgentId[jobId] = erc8004AgentId;
9091

9192
emit AgentHired(jobId, erc8004AgentId, msg.sender, amount, false);
9293
}
@@ -256,8 +257,10 @@ contract XcrowRouter is ReentrancyGuard, Pausable, Ownable {
256257
// Transfer USDC to settler for cross-chain bridging
257258
usdc.safeTransfer(address(settler), agentPayout);
258259

259-
// Convert agent wallet to bytes32 for CCTP
260-
bytes32 mintRecipient = bytes32(uint256(uint160(job.agentWallet)));
260+
// Convert agent owner to bytes32 for CCTP — payment goes to owner, not agent wallet
261+
address agentOwner = identityRegistry.ownerOf(job.agentId);
262+
require(agentOwner != address(0), "Agent owner not found");
263+
bytes32 mintRecipient = bytes32(uint256(uint160(agentOwner)));
261264

262265
// Initiate cross-chain settlement
263266
uint64 nonce = settler.settleCrossChain(jobId, agentPayout, destinationDomain, mintRecipient, hookData);

test/XcrowEscrow.t.sol

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ contract XcrowEscrowTest is Test {
165165
uint256 expectedFee = (JOB_AMOUNT * PROTOCOL_FEE_BPS) / 10000; // 2.5 USDC
166166
uint256 expectedPayout = JOB_AMOUNT - expectedFee; // 97.5 USDC
167167

168-
uint256 agentBalBefore = usdc.balanceOf(agentWallet);
168+
uint256 agentBalBefore = usdc.balanceOf(agentOwner);
169169

170170
vm.prank(client);
171171
escrow.settleJob(jobId);
@@ -175,7 +175,7 @@ contract XcrowEscrowTest is Test {
175175
assertGt(job.settledAt, 0);
176176

177177
// Agent got paid
178-
assertEq(usdc.balanceOf(agentWallet), agentBalBefore + expectedPayout);
178+
assertEq(usdc.balanceOf(agentOwner), agentBalBefore + expectedPayout);
179179

180180
// Fees accumulated
181181
assertEq(escrow.accumulatedFees(), expectedFee);
@@ -434,7 +434,7 @@ contract XcrowEscrowTest is Test {
434434

435435
function test_resolveDisputeByOwner_favorAgent() public {
436436
uint256 jobId = _createDefaultJob();
437-
uint256 agentBalBefore = usdc.balanceOf(agentWallet);
437+
uint256 agentBalBefore = usdc.balanceOf(agentOwner);
438438

439439
vm.prank(client);
440440
escrow.disputeJob(jobId, "disagreement");
@@ -447,7 +447,7 @@ contract XcrowEscrowTest is Test {
447447

448448
XcrowTypes.Job memory job = escrow.getJob(jobId);
449449
assertEq(uint8(job.status), uint8(XcrowTypes.JobStatus.Settled));
450-
assertEq(usdc.balanceOf(agentWallet), agentBalBefore + expectedPayout);
450+
assertEq(usdc.balanceOf(agentOwner), agentBalBefore + expectedPayout);
451451
assertEq(escrow.accumulatedFees(), expectedFee);
452452
}
453453

@@ -508,7 +508,7 @@ contract XcrowEscrowTest is Test {
508508
uint256 expectedFee = (JOB_AMOUNT * PROTOCOL_FEE_BPS) / 10000;
509509
uint256 expectedPayout = JOB_AMOUNT - expectedFee;
510510

511-
assertEq(usdc.balanceOf(agentWallet), expectedPayout);
511+
assertEq(usdc.balanceOf(agentOwner), expectedPayout);
512512
assertEq(escrow.accumulatedFees(), expectedFee);
513513

514514
// 6. Owner withdraws fees
@@ -633,7 +633,7 @@ contract XcrowEscrowTest is Test {
633633

634634
uint256 expectedFee = (JOB_AMOUNT * PROTOCOL_FEE_BPS) / 10000;
635635
uint256 expectedPayout = JOB_AMOUNT - expectedFee;
636-
uint256 agentBalBefore = usdc.balanceOf(agentWallet);
636+
uint256 agentBalBefore = usdc.balanceOf(agentOwner);
637637

638638
vm.prank(agentWallet);
639639
escrow.submitProofOfWork(jobId, proofHash);
@@ -648,7 +648,7 @@ contract XcrowEscrowTest is Test {
648648
XcrowTypes.Job memory job = escrow.getJob(jobId);
649649
assertEq(uint8(job.status), uint8(XcrowTypes.JobStatus.Settled));
650650
assertGt(job.settledAt, 0);
651-
assertEq(usdc.balanceOf(agentWallet), agentBalBefore + expectedPayout);
651+
assertEq(usdc.balanceOf(agentOwner), agentBalBefore + expectedPayout);
652652
assertEq(escrow.accumulatedFees(), expectedFee);
653653
}
654654

test/XcrowRouter.t.sol

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ contract XcrowRouterTest is Test {
213213
assertEq(uint8(job.status), uint8(XcrowTypes.JobStatus.Settled));
214214

215215
// Agent got paid (100 - 2.5% = 97.5)
216-
assertEq(usdc.balanceOf(agentWallet), 97_500_000);
216+
assertEq(usdc.balanceOf(agentOwner), 97_500_000);
217217
}
218218

219219
function test_router_submitFeedback_delegation() public {
@@ -280,7 +280,7 @@ contract XcrowRouterTest is Test {
280280
assertEq(job.client, client); // Direct client, not router
281281

282282
// Agent got paid (100 - 2.5% fee = 97.5)
283-
assertEq(usdc.balanceOf(agentWallet), 97_500_000);
283+
assertEq(usdc.balanceOf(agentOwner), 97_500_000);
284284
}
285285

286286
function test_directEscrow_withFeedback() public {
@@ -450,7 +450,7 @@ contract XcrowRouterTest is Test {
450450
XcrowTypes.Job memory job = escrow.getJob(jobId);
451451
assertEq(uint8(job.status), uint8(XcrowTypes.JobStatus.Settled));
452452
// Agent got paid (100 - 2.5% = 97.5)
453-
assertEq(usdc.balanceOf(agentWallet), 97_500_000);
453+
assertEq(usdc.balanceOf(agentOwner), 97_500_000);
454454
}
455455

456456
function test_autoSettleViaRouter_revert_windowNotElapsed() public {

0 commit comments

Comments
 (0)