@agenticprimitives/contracts 1.0.0-alpha.7 → 1.0.0-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,137 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5
+ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6
+ import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
7
+
8
+ /**
9
+ * @title PaymentEscrow
10
+ * @notice Hold-and-capture for an exchange order (spec 243 §5.5 escrow rail, FG-PAY-7).
11
+ * A payer funds a hold keyed by `orderHash`; funds then move on exactly ONE terminal path:
12
+ * - release → captured to the payee (on fulfillment evidence)
13
+ * - refund → returned to the payer (payee-consented, before capture)
14
+ * - reclaim → returned to the payer (payer-initiated, after expiry)
15
+ * Symmetric-escrow doctrine (273 EXC-D4): any escrowable leg can be held. Ports the
16
+ * smart-agent `CommitmentRegistry` status-machine pattern; no third-party escrow dependency.
17
+ *
18
+ * @dev HASH-ONLY events (orderHash only — no amounts/addresses leak; privacy posture §6). Apps read
19
+ * hold state via the `getHold` view (`readContract`), never inline `eth_getLogs` (ADR-0012).
20
+ * Fail-closed: a hold moves Held → {Captured|Refunded|Reclaimed} once and never again
21
+ * (checks-effects-interactions + ReentrancyGuard). Uses OZ SafeERC20 for fee/no-return tokens.
22
+ */
23
+ contract PaymentEscrow is ReentrancyGuard {
24
+ using SafeERC20 for IERC20;
25
+
26
+ enum Status {
27
+ None,
28
+ Held,
29
+ Captured,
30
+ Refunded,
31
+ Reclaimed
32
+ }
33
+
34
+ struct Hold {
35
+ address payer; // funded the hold
36
+ address asset; // ERC-20
37
+ uint256 amount;
38
+ address payee; // captures on release
39
+ address refundTo; // receives refund / reclaim
40
+ address releaser; // OPTIONAL extra party authorized to release (0 = payee-only)
41
+ uint64 expiry; // payer may reclaim at/after this time
42
+ Status status;
43
+ }
44
+
45
+ mapping(bytes32 orderHash => Hold) private _holds;
46
+
47
+ event EscrowDeposited(bytes32 indexed orderHash);
48
+ event EscrowReleased(bytes32 indexed orderHash);
49
+ event EscrowRefunded(bytes32 indexed orderHash);
50
+ event EscrowReclaimed(bytes32 indexed orderHash);
51
+
52
+ error HoldExists();
53
+ error NoHold();
54
+ error NotHeld();
55
+ error ZeroAmount();
56
+ error ZeroAddress();
57
+ error BadExpiry();
58
+ error NotAuthorized();
59
+ error NotYetExpired();
60
+
61
+ /**
62
+ * @notice Fund a hold for `orderHash`. Pulls `amount` of `asset` from the caller (the payer).
63
+ * @param refundTo Where refund/reclaim returns funds; 0 defaults to the payer.
64
+ * @param releaser Extra address allowed to `release` besides `payee`; 0 = payee-only.
65
+ */
66
+ function deposit(
67
+ bytes32 orderHash,
68
+ address asset,
69
+ uint256 amount,
70
+ address payee,
71
+ address refundTo,
72
+ address releaser,
73
+ uint64 expiry
74
+ ) external nonReentrant {
75
+ if (_holds[orderHash].status != Status.None) revert HoldExists();
76
+ if (amount == 0) revert ZeroAmount();
77
+ if (asset == address(0) || payee == address(0)) revert ZeroAddress();
78
+ if (expiry <= block.timestamp) revert BadExpiry();
79
+
80
+ _holds[orderHash] = Hold({
81
+ payer: msg.sender,
82
+ asset: asset,
83
+ amount: amount,
84
+ payee: payee,
85
+ refundTo: refundTo == address(0) ? msg.sender : refundTo,
86
+ releaser: releaser,
87
+ expiry: expiry,
88
+ status: Status.Held
89
+ });
90
+ emit EscrowDeposited(orderHash);
91
+ IERC20(asset).safeTransferFrom(msg.sender, address(this), amount);
92
+ }
93
+
94
+ /// @notice Capture a held order to the payee. Authorized: the payee, or the configured releaser.
95
+ function release(bytes32 orderHash) external nonReentrant {
96
+ Hold storage h = _held(orderHash);
97
+ if (msg.sender != h.payee && (h.releaser == address(0) || msg.sender != h.releaser)) revert NotAuthorized();
98
+ h.status = Status.Captured;
99
+ emit EscrowReleased(orderHash);
100
+ IERC20(h.asset).safeTransfer(h.payee, h.amount);
101
+ }
102
+
103
+ /// @notice Payee-consented refund of a held order to the payer (before capture).
104
+ function refund(bytes32 orderHash) external nonReentrant {
105
+ Hold storage h = _held(orderHash);
106
+ if (msg.sender != h.payee) revert NotAuthorized();
107
+ h.status = Status.Refunded;
108
+ emit EscrowRefunded(orderHash);
109
+ IERC20(h.asset).safeTransfer(h.refundTo, h.amount);
110
+ }
111
+
112
+ /// @notice Payer reclaims a held order after expiry (never released).
113
+ function reclaim(bytes32 orderHash) external nonReentrant {
114
+ Hold storage h = _held(orderHash);
115
+ if (msg.sender != h.payer) revert NotAuthorized();
116
+ if (block.timestamp < h.expiry) revert NotYetExpired();
117
+ h.status = Status.Reclaimed;
118
+ emit EscrowReclaimed(orderHash);
119
+ IERC20(h.asset).safeTransfer(h.refundTo, h.amount);
120
+ }
121
+
122
+ /// @notice Full hold record (app reads via this view, not log scans — ADR-0012).
123
+ function getHold(bytes32 orderHash) external view returns (Hold memory) {
124
+ return _holds[orderHash];
125
+ }
126
+
127
+ function statusOf(bytes32 orderHash) external view returns (Status) {
128
+ return _holds[orderHash].status;
129
+ }
130
+
131
+ /// @dev Load a hold that MUST be in `Held` (fail-closed): missing → NoHold; terminal → NotHeld.
132
+ function _held(bytes32 orderHash) private view returns (Hold storage h) {
133
+ h = _holds[orderHash];
134
+ if (h.status == Status.None) revert NoHold();
135
+ if (h.status != Status.Held) revert NotHeld();
136
+ }
137
+ }
@@ -0,0 +1,67 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ /**
5
+ * @title PaymentReceiptRegistry
6
+ * @notice Trustless, queryable settlement log for x402 pay-per-use (spec 272, PAY-CON-4 / X402-D9.3).
7
+ * Only the authorized {PaymentEnforcer} may `record` — a receipt is a byproduct of a gated
8
+ * `DelegationManager.redeemDelegation`, so a service cannot fabricate one and the reader's
9
+ * charge history needs no trust in service logs.
10
+ *
11
+ * @dev HASHES + ADDRESSES ONLY — no URLs, queries, licensed text, or passage refs ever reach the
12
+ * chain (PAY-WIRE-6). Event-only (no per-row storage): apps read settlements via an indexer /
13
+ * app cache, never inline `eth_getLogs` in product hot paths (ADR-0012). The
14
+ * `PaymentEnforcer.PaymentCharged` event is the primary on-chain receipt; `PaymentSettled`
15
+ * here adds payer/payee/mandateId-indexed querying for treasury views.
16
+ */
17
+ contract PaymentReceiptRegistry {
18
+ /// @notice The deployer who wires the sole authorized recorder once.
19
+ address public immutable owner;
20
+ /// @notice The single PaymentEnforcer permitted to `record` (set once via {setEnforcer}).
21
+ address public enforcer;
22
+
23
+ event PaymentSettled(
24
+ bytes32 indexed mandateId,
25
+ address indexed payer,
26
+ address indexed payee,
27
+ address asset,
28
+ uint256 amount,
29
+ bytes32 resourceHash,
30
+ bytes32 delegationHash,
31
+ bytes32 nonce
32
+ );
33
+ event EnforcerSet(address indexed enforcer);
34
+
35
+ error NotOwner();
36
+ error NotEnforcer();
37
+ error EnforcerAlreadySet();
38
+ error ZeroAddress();
39
+
40
+ constructor() {
41
+ owner = msg.sender;
42
+ }
43
+
44
+ /// @notice One-time wiring: authorize the deployed {PaymentEnforcer} as the sole recorder.
45
+ function setEnforcer(address enforcer_) external {
46
+ if (msg.sender != owner) revert NotOwner();
47
+ if (enforcer != address(0)) revert EnforcerAlreadySet();
48
+ if (enforcer_ == address(0)) revert ZeroAddress();
49
+ enforcer = enforcer_;
50
+ emit EnforcerSet(enforcer_);
51
+ }
52
+
53
+ /// @notice Record a settled payment. Reverts unless called by the authorized enforcer.
54
+ function record(
55
+ bytes32 mandateId,
56
+ address payer,
57
+ address payee,
58
+ address asset,
59
+ uint256 amount,
60
+ bytes32 resourceHash,
61
+ bytes32 delegationHash,
62
+ bytes32 nonce
63
+ ) external {
64
+ if (msg.sender != enforcer) revert NotEnforcer();
65
+ emit PaymentSettled(mandateId, payer, payee, asset, amount, resourceHash, delegationHash, nonce);
66
+ }
67
+ }