@agenticprimitives/contracts 1.0.0-alpha.4 → 1.0.0-alpha.5
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.
- package/AUDIT.md +304 -47
- package/README.md +19 -8
- package/deployments-base-sepolia.json +1 -0
- package/dist/abi/AgentAccount.json +1647 -0
- package/dist/abi/AgentAccountFactory.json +472 -0
- package/dist/abi/AgentNameRegistry.json +46 -0
- package/dist/abi/AgreementRegistry.json +432 -0
- package/dist/abi/AttestationRegistry.json +436 -0
- package/dist/abi/CallDataHashEnforcer.json +119 -0
- package/dist/abi/CustodyPolicy.json +1111 -0
- package/dist/abi/DelegationManager.json +645 -0
- package/dist/abi/GeoFeatureRegistry.json +625 -0
- package/dist/abi/IAgentAccount.json +262 -0
- package/dist/abi/IAgentAccountFactoryAccessor.json +15 -0
- package/dist/abi/IAgentAccountFactoryView.json +41 -0
- package/dist/abi/IAgentAccountPauseView.json +15 -0
- package/dist/abi/IAgenticPrimitivesAgentAccount.json +15 -0
- package/dist/abi/IApprovedHashRegistryView.json +26 -0
- package/dist/abi/ICustodyPolicyFactoryView.json +15 -0
- package/dist/abi/ICustodyPolicyPauseView.json +15 -0
- package/dist/abi/IDelegationManager.json +171 -0
- package/dist/abi/IERC7579HookLike.json +44 -0
- package/dist/abi/IERC7579ModuleLike.json +28 -0
- package/dist/abi/PermissionlessSubregistry.json +5 -0
- package/dist/abi/QuorumEnforcer.json +295 -0
- package/dist/abi/SignatureSlotRecovery.json +88 -0
- package/dist/abi/SkillDefinitionRegistry.json +491 -0
- package/dist/abi/SmartAgentPaymaster.json +664 -0
- package/dist/abi/UniversalSignatureValidator.json +65 -0
- package/dist/abi/index.d.ts +23 -0
- package/dist/abi/index.js +23 -0
- package/dist/deployments/base-sepolia.d.ts +37 -0
- package/dist/deployments/base-sepolia.js +35 -0
- package/dist/deployments/index.d.ts +8 -0
- package/dist/deployments/index.js +13 -0
- package/package.json +20 -3
- package/src/AgentAccount.sol +130 -9
- package/src/AgentAccountFactory.sol +34 -5
- package/src/SmartAgentPaymaster.sol +2 -0
- package/src/UniversalSignatureValidator.sol +3 -0
- package/src/agency/DelegationManager.sol +155 -0
- package/src/agency/IDelegationManager.sol +35 -0
- package/src/agreement/AgreementRegistry.sol +306 -0
- package/src/attestation/AttestationRegistry.sol +320 -0
- package/src/custody/CustodyPolicy.sol +79 -2
- package/src/enforcers/AllowedMethodsEnforcer.sol +6 -0
- package/src/enforcers/AllowedTargetsEnforcer.sol +6 -0
- package/src/enforcers/CallDataHashEnforcer.sol +61 -0
- package/src/enforcers/QuorumEnforcer.sol +8 -0
- package/src/enforcers/TimestampEnforcer.sol +7 -0
- package/src/enforcers/ValueEnforcer.sol +6 -0
- package/src/geo/GeoFeatureRegistry.sol +190 -0
- package/src/naming/AgentNameRegistry.sol +25 -13
- package/src/naming/AgentNameUniversalResolver.sol +1 -1
- package/src/naming/PermissionlessSubregistry.sol +19 -2
- package/src/relationships/AgentRelationship.sol +8 -0
- package/src/skills/SkillDefinitionRegistry.sol +181 -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)
|
|
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();
|