@agenticprimitives/contracts 1.0.0-alpha.4 → 1.0.0-alpha.6

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.
Files changed (57) hide show
  1. package/AUDIT.md +304 -47
  2. package/README.md +19 -8
  3. package/deployments-base-sepolia.json +1 -0
  4. package/dist/abi/AgentAccount.json +1647 -0
  5. package/dist/abi/AgentAccountFactory.json +472 -0
  6. package/dist/abi/AgentNameRegistry.json +46 -0
  7. package/dist/abi/AgreementRegistry.json +432 -0
  8. package/dist/abi/AttestationRegistry.json +436 -0
  9. package/dist/abi/CallDataHashEnforcer.json +119 -0
  10. package/dist/abi/CustodyPolicy.json +1111 -0
  11. package/dist/abi/DelegationManager.json +645 -0
  12. package/dist/abi/GeoFeatureRegistry.json +625 -0
  13. package/dist/abi/IAgentAccount.json +262 -0
  14. package/dist/abi/IAgentAccountFactoryAccessor.json +15 -0
  15. package/dist/abi/IAgentAccountFactoryView.json +41 -0
  16. package/dist/abi/IAgentAccountPauseView.json +15 -0
  17. package/dist/abi/IAgenticPrimitivesAgentAccount.json +15 -0
  18. package/dist/abi/IApprovedHashRegistryView.json +26 -0
  19. package/dist/abi/ICustodyPolicyFactoryView.json +15 -0
  20. package/dist/abi/ICustodyPolicyPauseView.json +15 -0
  21. package/dist/abi/IDelegationManager.json +171 -0
  22. package/dist/abi/IERC7579HookLike.json +44 -0
  23. package/dist/abi/IERC7579ModuleLike.json +28 -0
  24. package/dist/abi/PermissionlessSubregistry.json +5 -0
  25. package/dist/abi/QuorumEnforcer.json +295 -0
  26. package/dist/abi/SignatureSlotRecovery.json +88 -0
  27. package/dist/abi/SkillDefinitionRegistry.json +491 -0
  28. package/dist/abi/SmartAgentPaymaster.json +664 -0
  29. package/dist/abi/UniversalSignatureValidator.json +65 -0
  30. package/dist/abi/index.d.ts +23 -0
  31. package/dist/abi/index.js +23 -0
  32. package/dist/deployments/base-sepolia.d.ts +37 -0
  33. package/dist/deployments/base-sepolia.js +35 -0
  34. package/dist/deployments/index.d.ts +8 -0
  35. package/dist/deployments/index.js +13 -0
  36. package/package.json +20 -3
  37. package/src/AgentAccount.sol +130 -9
  38. package/src/AgentAccountFactory.sol +34 -5
  39. package/src/SmartAgentPaymaster.sol +2 -0
  40. package/src/UniversalSignatureValidator.sol +3 -0
  41. package/src/agency/DelegationManager.sol +155 -0
  42. package/src/agency/IDelegationManager.sol +35 -0
  43. package/src/agreement/AgreementRegistry.sol +306 -0
  44. package/src/attestation/AttestationRegistry.sol +320 -0
  45. package/src/custody/CustodyPolicy.sol +79 -2
  46. package/src/enforcers/AllowedMethodsEnforcer.sol +6 -0
  47. package/src/enforcers/AllowedTargetsEnforcer.sol +6 -0
  48. package/src/enforcers/CallDataHashEnforcer.sol +61 -0
  49. package/src/enforcers/QuorumEnforcer.sol +8 -0
  50. package/src/enforcers/TimestampEnforcer.sol +7 -0
  51. package/src/enforcers/ValueEnforcer.sol +6 -0
  52. package/src/geo/GeoFeatureRegistry.sol +197 -0
  53. package/src/naming/AgentNameRegistry.sol +25 -13
  54. package/src/naming/AgentNameUniversalResolver.sol +1 -1
  55. package/src/naming/PermissionlessSubregistry.sol +19 -2
  56. package/src/relationships/AgentRelationship.sol +8 -0
  57. package/src/skills/SkillDefinitionRegistry.sol +187 -0
@@ -0,0 +1,320 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ import "@openzeppelin/contracts/interfaces/IERC1271.sol";
5
+
6
+ /**
7
+ * @title AttestationRegistry
8
+ * @notice EAS-aligned, bilateral-consent attestation registry per ADR-0023.
9
+ *
10
+ * ─── Substrate role ───
11
+ *
12
+ * Layers 12-15 of the coordination spine (Evidence / Outcome / Validation /
13
+ * TrustUpdate) plus the substrate's Association + JointAgreement attestations
14
+ * all share THIS one contract. Per ADR-0024 Decision 2 — the architectural
15
+ * inverse of the smart-contract-per-credential anti-pattern.
16
+ *
17
+ * ─── Key invariants (AR-*) ───
18
+ *
19
+ * AR-01 deterministic UID = keccak256(subject, issuer, credentialType, credentialHash, refUID, salt)
20
+ * AR-02 issuer signature verified via ERC-1271 against the named issuer SA
21
+ * AR-03 schemaId MUST point to a registered ShapeRegistry shape (governance-gated)
22
+ * AR-04 bilateral-consent verified via DelegationManager.verifyAuthorization
23
+ * AR-05 holder-only revocation for unilateral; either-party for joint
24
+ * AR-06 NO issuerRevoke(...) entrypoint — by static analysis
25
+ * AR-07 epoch-bucket timestamps; raw block.timestamp never stored
26
+ * AR-08 four indexed event topics (EAS-like)
27
+ * AR-09 immutable + non-upgradeable; no admin
28
+ *
29
+ * Spec 242 §9 + ADR-0023.
30
+ */
31
+ contract AttestationRegistry {
32
+ // ─── Constants ──────────────────────────────────────────────────────
33
+
34
+ uint64 public constant EPOCH_SECONDS = 3600;
35
+
36
+ // ─── Errors ─────────────────────────────────────────────────────────
37
+
38
+ error InvalidIssuerSignature();
39
+ error AttestationAlreadyExists();
40
+ error AttestationNotFound();
41
+ error NotSubject();
42
+ error NotParty();
43
+ error AlreadyRevoked();
44
+ error InvalidParties();
45
+ error InvalidPartyConsent();
46
+ error EmptyCredentialHash();
47
+
48
+ /// @dev RW1-1 (ADR-0027) — the canonical consent payload each party signs to consent to a
49
+ /// joint agreement. The contract RECOMPUTES this digest from calldata; a stored/supplied
50
+ /// reference is not consent. Binds the consent to the exact parties + agreement + credential.
51
+ bytes32 internal constant JOINT_CONSENT_TYPEHASH = keccak256(
52
+ "JointAgreementConsent(address party1,address party2,bytes32 agreementCommitment,bytes32 credentialHash)"
53
+ );
54
+
55
+ // ─── Storage ────────────────────────────────────────────────────────
56
+
57
+ /// @notice The canonical attestation row. EAS-aligned shape.
58
+ struct Attestation {
59
+ bytes32 uid;
60
+ bytes32 schemaId; // → ShapeRegistry row
61
+ bytes32 credentialType; // VC type class
62
+ bytes32 credentialHash; // RFC 8785 JCS hash of off-chain VC body
63
+ bytes32 refUID; // EAS-style back-pointer (e.g. AgreementCommitment for JointAgreement)
64
+ bytes32 bilateralConsentRef; // 0 for unilateral; hash for joint
65
+ bytes32 offchainCredentialStatusList; // W3C VC StatusList2021 pointer
66
+ uint64 epochBucket; // attest time / EPOCH_SECONDS
67
+ uint64 revocationEpochBucket; // 0 if not revoked
68
+ address subject; // holder (unilateral) or party[0] (joint)
69
+ address party2; // address(0) for unilateral; party[1] for joint
70
+ address issuer; // off-chain VC signer SA
71
+ }
72
+
73
+ mapping(bytes32 => Attestation) private _attestations;
74
+
75
+ // ─── Requests ───────────────────────────────────────────────────────
76
+
77
+ struct AssociationAttestationRequest {
78
+ bytes32 schemaId;
79
+ bytes32 credentialType;
80
+ bytes32 credentialHash;
81
+ bytes32 offchainCredentialStatusList;
82
+ address subject;
83
+ address issuer;
84
+ /// @dev EIP-712 signature by `issuer` SA over the credentialHash;
85
+ /// verified via ERC-1271.
86
+ bytes issuerSignature;
87
+ uint256 salt;
88
+ }
89
+
90
+ struct JointAgreementAttestationRequest {
91
+ bytes32 schemaId;
92
+ bytes32 credentialType;
93
+ bytes32 credentialHash;
94
+ bytes32 refUID; // AgreementCommitment hash (spec 241)
95
+ bytes32 bilateralConsentRef; // DEPRECATED / IGNORED (RW1-1, ADR-0027): consent is now
96
+ // VERIFIED on-chain from party1Signature + party2Signature over the RECOMPUTED
97
+ // consent digest; the stored `bilateralConsentRef` is that recomputed digest, not this.
98
+ bytes32 offchainCredentialStatusList;
99
+ address party1;
100
+ address party2;
101
+ address issuer;
102
+ bytes issuerSignature;
103
+ bytes party1Signature; // RW1-1: party1's consent over JOINT_CONSENT_TYPEHASH digest
104
+ bytes party2Signature; // RW1-1: party2's consent over JOINT_CONSENT_TYPEHASH digest
105
+ uint256 salt;
106
+ }
107
+
108
+ // ─── Events (EAS-like; 4 indexed topics each) ───────────────────────
109
+
110
+ /// @dev Solidity caps indexed-args at 3 per non-anonymous event (EVM
111
+ /// topic0 = event signature). Matches EAS event shape:
112
+ /// `uid` is non-indexed payload; subject + issuer + credentialType
113
+ /// are indexed for filtering.
114
+ event Attested(
115
+ address indexed subject,
116
+ address indexed issuer,
117
+ bytes32 indexed credentialType,
118
+ bytes32 uid
119
+ );
120
+
121
+ event JointAttested(
122
+ address indexed party1,
123
+ address indexed party2,
124
+ bytes32 indexed credentialType,
125
+ bytes32 uid
126
+ );
127
+
128
+ event Revoked(
129
+ address indexed subjectOrParty,
130
+ bytes32 indexed credentialType,
131
+ bytes32 uid,
132
+ bytes32 reasonHash
133
+ );
134
+
135
+ // ─── Public surface ─────────────────────────────────────────────────
136
+
137
+ /// @notice Unilateral attestation by a holder against an issued credential.
138
+ function assertAssociation(AssociationAttestationRequest calldata req) external returns (bytes32) {
139
+ if (req.credentialHash == bytes32(0)) revert EmptyCredentialHash();
140
+
141
+ // AR-02: verify issuer signature over credentialHash
142
+ if (!_isValidSignatureBool(req.issuer, req.credentialHash, req.issuerSignature)) {
143
+ revert InvalidIssuerSignature();
144
+ }
145
+
146
+ bytes32 uid = _computeUid(
147
+ req.subject,
148
+ address(0),
149
+ req.issuer,
150
+ req.credentialType,
151
+ req.credentialHash,
152
+ bytes32(0),
153
+ req.salt
154
+ );
155
+
156
+ if (_attestations[uid].uid != bytes32(0)) revert AttestationAlreadyExists();
157
+
158
+ uint64 bucket = uint64(block.timestamp) / EPOCH_SECONDS;
159
+
160
+ _attestations[uid] = Attestation({
161
+ uid: uid,
162
+ schemaId: req.schemaId,
163
+ credentialType: req.credentialType,
164
+ credentialHash: req.credentialHash,
165
+ refUID: bytes32(0),
166
+ bilateralConsentRef: bytes32(0),
167
+ offchainCredentialStatusList: req.offchainCredentialStatusList,
168
+ epochBucket: bucket,
169
+ revocationEpochBucket: 0,
170
+ subject: req.subject,
171
+ party2: address(0),
172
+ issuer: req.issuer
173
+ });
174
+
175
+ emit Attested(req.subject, req.issuer, req.credentialType, uid);
176
+ return uid;
177
+ }
178
+
179
+ /// @notice Joint attestation by two parties against an agreement.
180
+ /// @dev RW1-1 (ADR-0027) — bilateral consent is VERIFIED on-chain, not trusted. The contract
181
+ /// recomputes the canonical consent digest (`JOINT_CONSENT_TYPEHASH` over party1, party2,
182
+ /// the agreement commitment, and the credential hash) and requires BOTH parties' signatures
183
+ /// over it (ERC-1271 for smart accounts, ECDSA for EOAs). A nonzero `bilateralConsentRef` is
184
+ /// NOT consent (the prior "caller's responsibility" model, AR-04). The recomputed digest is
185
+ /// what gets stored. Alternative consent proof via `DelegationManager.verifyAuthorizationForCall`
186
+ /// (an exact-call sub-delegation per party) is spec 249's option (b); this entrypoint takes
187
+ /// the direct two-party-signature form.
188
+ function assertJointAgreement(
189
+ JointAgreementAttestationRequest calldata req
190
+ ) external returns (bytes32) {
191
+ if (req.credentialHash == bytes32(0)) revert EmptyCredentialHash();
192
+ if (req.party1 == address(0) || req.party2 == address(0)) revert InvalidParties();
193
+ if (req.party1 == req.party2) revert InvalidParties();
194
+
195
+ if (!_isValidSignatureBool(req.issuer, req.credentialHash, req.issuerSignature)) {
196
+ revert InvalidIssuerSignature();
197
+ }
198
+
199
+ // VERIFY bilateral consent: recompute the digest, require both parties' signatures over it.
200
+ bytes32 consentDigest = keccak256(
201
+ abi.encode(JOINT_CONSENT_TYPEHASH, req.party1, req.party2, req.refUID, req.credentialHash)
202
+ );
203
+ if (!_isValidSignatureBool(req.party1, consentDigest, req.party1Signature)) revert InvalidPartyConsent();
204
+ if (!_isValidSignatureBool(req.party2, consentDigest, req.party2Signature)) revert InvalidPartyConsent();
205
+
206
+ bytes32 uid = _computeUid(
207
+ req.party1,
208
+ req.party2,
209
+ req.issuer,
210
+ req.credentialType,
211
+ req.credentialHash,
212
+ req.refUID,
213
+ req.salt
214
+ );
215
+ if (_attestations[uid].uid != bytes32(0)) revert AttestationAlreadyExists();
216
+
217
+ uint64 bucket = uint64(block.timestamp) / EPOCH_SECONDS;
218
+
219
+ _attestations[uid] = Attestation({
220
+ uid: uid,
221
+ schemaId: req.schemaId,
222
+ credentialType: req.credentialType,
223
+ credentialHash: req.credentialHash,
224
+ refUID: req.refUID,
225
+ bilateralConsentRef: consentDigest, // the VERIFIED, recomputed consent digest (RW1-1)
226
+ offchainCredentialStatusList: req.offchainCredentialStatusList,
227
+ epochBucket: bucket,
228
+ revocationEpochBucket: 0,
229
+ subject: req.party1,
230
+ party2: req.party2,
231
+ issuer: req.issuer
232
+ });
233
+
234
+ emit JointAttested(req.party1, req.party2, req.credentialType, uid);
235
+ return uid;
236
+ }
237
+
238
+ /// @notice Holder-only revocation for unilateral attestations (AR-05).
239
+ function revokeOwnAssociation(bytes32 uid, bytes32 reasonHash) external {
240
+ Attestation storage a = _attestations[uid];
241
+ if (a.uid == bytes32(0)) revert AttestationNotFound();
242
+ if (a.party2 != address(0)) revert NotSubject(); // joint — wrong entrypoint
243
+ if (msg.sender != a.subject) revert NotSubject();
244
+ if (a.revocationEpochBucket != 0) revert AlreadyRevoked();
245
+
246
+ a.revocationEpochBucket = uint64(block.timestamp) / EPOCH_SECONDS;
247
+ emit Revoked(msg.sender, a.credentialType, uid, reasonHash);
248
+ }
249
+
250
+ /// @notice Either-party revocation for joint attestations (D-26 + AR-05).
251
+ function revokeOwnJointAgreement(bytes32 uid, bytes32 reasonHash) external {
252
+ Attestation storage a = _attestations[uid];
253
+ if (a.uid == bytes32(0)) revert AttestationNotFound();
254
+ if (a.party2 == address(0)) revert NotParty(); // unilateral — wrong entrypoint
255
+ if (msg.sender != a.subject && msg.sender != a.party2) revert NotParty();
256
+ if (a.revocationEpochBucket != 0) revert AlreadyRevoked();
257
+
258
+ a.revocationEpochBucket = uint64(block.timestamp) / EPOCH_SECONDS;
259
+ emit Revoked(msg.sender, a.credentialType, uid, reasonHash);
260
+ }
261
+
262
+ /// @notice Get the full attestation row.
263
+ function getAttestation(bytes32 uid) external view returns (Attestation memory) {
264
+ Attestation memory a = _attestations[uid];
265
+ if (a.uid == bytes32(0)) revert AttestationNotFound();
266
+ return a;
267
+ }
268
+
269
+ /// @notice Convenience — `!revoked` view.
270
+ function isValid(bytes32 uid) external view returns (bool) {
271
+ return _attestations[uid].uid != bytes32(0) && _attestations[uid].revocationEpochBucket == 0;
272
+ }
273
+
274
+ // ─── Internal ───────────────────────────────────────────────────────
275
+
276
+ function _computeUid(
277
+ address subject,
278
+ address party2,
279
+ address issuer,
280
+ bytes32 credentialType,
281
+ bytes32 credentialHash,
282
+ bytes32 refUID,
283
+ uint256 salt
284
+ ) internal pure returns (bytes32) {
285
+ return
286
+ keccak256(
287
+ abi.encode(subject, party2, issuer, credentialType, credentialHash, refUID, salt)
288
+ );
289
+ }
290
+
291
+ /// @dev ERC-1271-first signature verification (smart accounts), with EOA fallback.
292
+ function _isValidSignatureBool(
293
+ address signer,
294
+ bytes32 digest,
295
+ bytes memory signature
296
+ ) internal view returns (bool) {
297
+ if (signer.code.length > 0) {
298
+ try IERC1271(signer).isValidSignature(digest, signature) returns (bytes4 result) {
299
+ return result == IERC1271.isValidSignature.selector;
300
+ } catch {
301
+ return false;
302
+ }
303
+ }
304
+ // EOA fallback via eth-signed message hash + ecrecover
305
+ bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest));
306
+ if (signature.length != 65) return false;
307
+ bytes32 r;
308
+ bytes32 s;
309
+ uint8 v;
310
+ // solhint-disable-next-line no-inline-assembly
311
+ assembly {
312
+ r := mload(add(signature, 0x20))
313
+ s := mload(add(signature, 0x40))
314
+ v := byte(0, mload(add(signature, 0x60)))
315
+ }
316
+ if (v < 27) v += 27;
317
+ address recovered = ecrecover(ethHash, v, r, s);
318
+ return recovered != address(0) && recovered == signer;
319
+ }
320
+ }
@@ -4,6 +4,22 @@ pragma solidity ^0.8.28;
4
4
  import {IAgentAccount, AgentAccountRecoveryArgs, AgentAccountRecoveryPasskeyAdd} from "../IAgentAccount.sol";
5
5
  import {SignatureSlotRecovery} from "../libraries/SignatureSlotRecovery.sol";
6
6
 
7
+ /// @dev R6.6 — minimal interface for the `account → factory → governance.isPaused()`
8
+ /// staticcall chain. Mirrors the same patterns introduced by R6.5
9
+ /// in `AgentAccount` (see `IAgentAccountFactoryView` /
10
+ /// `IAgentAccountPauseView` in `AgentAccount.sol`).
11
+ interface IAgentAccountFactoryAccessor {
12
+ function factory() external view returns (address);
13
+ }
14
+
15
+ interface ICustodyPolicyFactoryView {
16
+ function governance() external view returns (address);
17
+ }
18
+
19
+ interface ICustodyPolicyPauseView {
20
+ function isPaused() external view returns (bool);
21
+ }
22
+
7
23
  /**
8
24
  * @title CustodyPolicy
9
25
  * @notice ERC-7579 module that owns the schedule / apply / cancel
@@ -125,6 +141,64 @@ contract CustodyPolicy {
125
141
  _CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator();
126
142
  }
127
143
 
144
+ // ─── R6.6 / CON-CustodyPolicy-005 — system-pause modifier ──────
145
+ //
146
+ // `whenAccountNotPaused(account)` reverts when
147
+ // `AgenticGovernance.isPaused() == true`, where governance is
148
+ // resolved per-call via the account's factory:
149
+ //
150
+ // account.factory() → factory.governance() → governance.isPaused()
151
+ //
152
+ // 3 staticcalls. Custody schedule/apply are rare; the gas cost
153
+ // is acceptable. Non-conforming hops (EOA factory, EOA governance,
154
+ // missing fn) treat the system as "not paused" so legacy / test
155
+ // deploys keep working — mirrors the R6.5 pattern in AgentAccount
156
+ // and `GovernanceManaged._pausedSafe()`.
157
+ //
158
+ // Applied to: `scheduleCustodyChange`, `applyCustodyChange`.
159
+ //
160
+ // Deliberately NOT applied to (recovery primitives that must
161
+ // work during an incident):
162
+ // - `cancelScheduledChange` — defensive de-escalation
163
+ // - `onUninstall` — removing attack surface
164
+ // - `onInstall` — gated upstream by paused
165
+ // `installModule` on AgentAccount (R6.5)
166
+ // and paused `createAgentAccount`
167
+ // on AgentAccountFactory
168
+ modifier whenAccountNotPaused(address account) {
169
+ if (_systemPausedFor(account)) revert SystemPaused();
170
+ _;
171
+ }
172
+
173
+ /// @notice R6.6 — thrown when a `whenAccountNotPaused`-guarded
174
+ /// entrypoint is called while the governance pause flag
175
+ /// is set.
176
+ error SystemPaused();
177
+
178
+ function _systemPausedFor(address account) internal view returns (bool) {
179
+ if (account == address(0) || account.code.length == 0) return false;
180
+ // Hop 1: account → factory
181
+ (bool okF, bytes memory dataF) = account.staticcall(
182
+ abi.encodeWithSelector(IAgentAccountFactoryAccessor.factory.selector)
183
+ );
184
+ if (!okF || dataF.length < 32) return false;
185
+ address fac = abi.decode(dataF, (address));
186
+ if (fac == address(0) || fac.code.length == 0) return false;
187
+ // Hop 2: factory → governance
188
+ (bool okG, bytes memory dataG) = fac.staticcall(
189
+ abi.encodeWithSelector(ICustodyPolicyFactoryView.governance.selector)
190
+ );
191
+ if (!okG || dataG.length < 32) return false;
192
+ address gov = abi.decode(dataG, (address));
193
+ if (gov == address(0) || gov.code.length == 0) return false;
194
+ // Hop 3: governance → isPaused
195
+ (bool okP, bytes memory dataP) = gov.staticcall(
196
+ abi.encodeWithSelector(ICustodyPolicyPauseView.isPaused.selector)
197
+ );
198
+ if (!okP || dataP.length < 32) return false;
199
+ return abi.decode(dataP, (bool));
200
+ }
201
+
128
202
  function _buildDomainSeparator() internal view returns (bytes32) {
129
203
  return keccak256(
130
204
  abi.encode(
@@ -335,7 +409,7 @@ contract CustodyPolicy {
335
409
  CustodyAction action,
336
410
  bytes calldata args,
337
411
  bytes calldata quorumSigs
338
- ) external returns (uint256 changeId) {
412
+ ) external whenAccountNotPaused(account) returns (uint256 changeId) {
339
413
  Config storage c = _configs[account];
340
414
  if (!c.installed) revert NotInstalledOn(account);
341
415
 
@@ -394,7 +468,10 @@ contract CustodyPolicy {
394
468
  emit CustodyChangeScheduled(account, changeId, action, eta, msg.sender);
395
469
  }
396
470
 
397
- function applyCustodyChange(address account, uint256 changeId, bytes calldata quorumSigs) external {
471
+ function applyCustodyChange(address account, uint256 changeId, bytes calldata quorumSigs)
472
+ external
473
+ whenAccountNotPaused(account)
474
+ {
398
475
  Config storage c = _configs[account];
399
476
  if (!c.installed) revert NotInstalledOn(account);
400
477
 
@@ -8,6 +8,12 @@ import "../agency/ICaveatEnforcer.sol";
8
8
  * @notice Restricts delegated calls to specific function selectors.
9
9
  * @dev terms = abi.encode(bytes4[] allowedSelectors)
10
10
  * Follows ERC-7710 / MetaMask DeleGator beforeHook/afterHook pattern.
11
+ *
12
+ * R6.7 — Stateless validator; no `whenNotPaused` modifier needed
13
+ * (hooks are `pure`, contract has zero storage). The DM-side pause
14
+ * check in `DelegationManager.redeemDelegation` gates all
15
+ * reachable enforcer dispatch. See `docs/audits/r6-contracts-recon-2026-05-31.md`
16
+ * § 4.4 + `test/EnforcerPauseInvariantR67.t.sol`.
11
17
  */
12
18
  contract AllowedMethodsEnforcer is ICaveatEnforcer {
13
19
  error MethodNotAllowed();
@@ -8,6 +8,12 @@ import "../agency/ICaveatEnforcer.sol";
8
8
  * @notice Restricts delegated calls to a specific set of target contracts.
9
9
  * @dev terms = abi.encode(address[] allowedTargets)
10
10
  * Follows ERC-7710 / MetaMask DeleGator beforeHook/afterHook pattern.
11
+ *
12
+ * R6.7 — Stateless validator; no `whenNotPaused` modifier needed
13
+ * (hooks are `pure`, contract has zero storage). The DM-side pause
14
+ * check in `DelegationManager.redeemDelegation` gates all
15
+ * reachable enforcer dispatch. See `docs/audits/r6-contracts-recon-2026-05-31.md`
16
+ * § 4.4 + `test/EnforcerPauseInvariantR67.t.sol`.
11
17
  */
12
18
  contract AllowedTargetsEnforcer is ICaveatEnforcer {
13
19
  error TargetNotAllowed();
@@ -0,0 +1,61 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ import "../agency/ICaveatEnforcer.sol";
5
+
6
+ /**
7
+ * @title CallDataHashEnforcer
8
+ * @notice Pins a delegation to EXACTLY one calldata payload — the exact-call
9
+ * binding ADR-0027 / spec 249 (RW1-4b) needs for sensitive ops.
10
+ * @dev terms = abi.encode(bytes32 expectedHash), where
11
+ * `expectedHash == keccak256(callData)` for the single call this
12
+ * delegation authorizes. `beforeHook` recomputes `keccak256(callData)`
13
+ * and reverts on mismatch.
14
+ *
15
+ * Ported from smart-agent's `CallDataHashEnforcer` (the "P4 — sensitive
16
+ * ops require exact-call sub-delegation" doctrine). Combined with
17
+ * `AllowedTargetsEnforcer` (target) + a tight `TimestampEnforcer`
18
+ * window + `ValueEnforcer`, a consent sub-delegation can pin the precise
19
+ * call — not merely its selector — so a registry that checks
20
+ * `DelegationManager.verifyAuthorizationForCall(...)` binds consent to the
21
+ * exact assertion being stored (RW1-1).
22
+ *
23
+ * R6.7 — Stateless validator: hooks are `pure`, the contract has zero
24
+ * storage, so no `whenNotPaused` is needed; the DM-side pause check in
25
+ * `DelegationManager.redeemDelegation` gates all reachable enforcer
26
+ * dispatch, and the read-only `verifyAuthorizationForCall` evaluates it
27
+ * via `staticcall`.
28
+ */
29
+ contract CallDataHashEnforcer is ICaveatEnforcer {
30
+ error BadTermsLength();
31
+ error CallDataMismatch(bytes32 expected, bytes32 actual);
32
+
33
+ function beforeHook(
34
+ bytes calldata terms,
35
+ bytes calldata,
36
+ bytes32,
37
+ address,
38
+ address,
39
+ address,
40
+ uint256,
41
+ bytes calldata callData
42
+ ) external pure override {
43
+ if (terms.length != 32) revert BadTermsLength();
44
+ bytes32 expected = abi.decode(terms, (bytes32));
45
+ bytes32 actual = keccak256(callData);
46
+ if (expected != actual) revert CallDataMismatch(expected, actual);
47
+ }
48
+
49
+ function afterHook(
50
+ bytes calldata,
51
+ bytes calldata,
52
+ bytes32,
53
+ address,
54
+ address,
55
+ address,
56
+ uint256,
57
+ bytes calldata
58
+ ) external pure override {
59
+ // No post-execution check needed.
60
+ }
61
+ }
@@ -68,6 +68,14 @@ import "../libraries/SignatureSlotRecovery.sol";
68
68
  * Only the first `threshold` slots are checked. Excess entries
69
69
  * beyond the threshold are ignored — callers SHOULD NOT pad blobs
70
70
  * unnecessarily as calldata cost scales linearly.
71
+ *
72
+ * R6.7 — Stateless validator; no `whenNotPaused` modifier needed.
73
+ * `beforeHook` is `external view` (reads `block.chainid`), `afterHook`
74
+ * is `external pure` (empty body); contract has zero storage
75
+ * variables. The DM-side pause check in
76
+ * `DelegationManager.redeemDelegation` gates all reachable enforcer
77
+ * dispatch. See `docs/audits/r6-contracts-recon-2026-05-31.md`
78
+ * § 4.4 + `test/EnforcerPauseInvariantR67.t.sol`.
71
79
  */
72
80
  contract QuorumEnforcer is ICaveatEnforcer {
73
81
  error InsufficientQuorum(uint256 supplied, uint8 threshold);
@@ -8,6 +8,13 @@ import "../agency/ICaveatEnforcer.sol";
8
8
  * @notice Enforces a time window — delegation is only valid between two timestamps.
9
9
  * @dev terms = abi.encode(uint256 validAfter, uint256 validUntil)
10
10
  * Follows ERC-7710 / MetaMask DeleGator beforeHook/afterHook pattern.
11
+ *
12
+ * R6.7 — Stateless validator; no `whenNotPaused` modifier needed.
13
+ * `beforeHook` is `external view` (reads `block.timestamp`); contract
14
+ * has zero storage. The DM-side pause check in
15
+ * `DelegationManager.redeemDelegation` gates all reachable enforcer
16
+ * dispatch. See `docs/audits/r6-contracts-recon-2026-05-31.md`
17
+ * § 4.4 + `test/EnforcerPauseInvariantR67.t.sol`.
11
18
  */
12
19
  contract TimestampEnforcer is ICaveatEnforcer {
13
20
  error TimestampNotYetValid();
@@ -8,6 +8,12 @@ import "../agency/ICaveatEnforcer.sol";
8
8
  * @notice Enforces a maximum ETH value per call.
9
9
  * @dev terms = abi.encode(uint256 maxValue)
10
10
  * Follows ERC-7710 / MetaMask DeleGator beforeHook/afterHook pattern.
11
+ *
12
+ * R6.7 — Stateless validator; no `whenNotPaused` modifier needed
13
+ * (hooks are `pure`, contract has zero storage). The DM-side pause
14
+ * check in `DelegationManager.redeemDelegation` gates all
15
+ * reachable enforcer dispatch. See `docs/audits/r6-contracts-recon-2026-05-31.md`
16
+ * § 4.4 + `test/EnforcerPauseInvariantR67.t.sol`.
11
17
  */
12
18
  contract ValueEnforcer is ICaveatEnforcer {
13
19
  error ValueExceedsLimit();