@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
|
@@ -10,7 +10,7 @@ import "./governance/GovernanceManaged.sol";
|
|
|
10
10
|
/**
|
|
11
11
|
* @title AgentAccountFactory
|
|
12
12
|
* @notice Single-entry factory for AgentAccount proxies with
|
|
13
|
-
* deterministic CREATE2 addresses. Phase 6f.5
|
|
13
|
+
* deterministic CREATE2 addresses. Phase 6f.5 -- collapsed
|
|
14
14
|
* createPersonAgent + createMultiSigSmartAgent into one
|
|
15
15
|
* entry point so every account on chain has the same shape;
|
|
16
16
|
* the only axis is `mode` on the init params.
|
|
@@ -32,7 +32,7 @@ import "./governance/GovernanceManaged.sol";
|
|
|
32
32
|
* marker check).
|
|
33
33
|
*
|
|
34
34
|
* The canonical CustodyPolicy address is wired at construction
|
|
35
|
-
* (factory-immutable)
|
|
35
|
+
* (factory-immutable) -- there is no per-call validator
|
|
36
36
|
* override. Swapping CustodyPolicy versions means redeploying
|
|
37
37
|
* the factory, which is the same blast radius as a CREATE2
|
|
38
38
|
* salt bump.
|
|
@@ -41,7 +41,7 @@ import "./governance/GovernanceManaged.sol";
|
|
|
41
41
|
* `bundlerSigner` and `sessionIssuer` are factory-level capability
|
|
42
42
|
* addresses. They are NEVER custodians of any deployed account; each
|
|
43
43
|
* account reads them off this factory on demand. Phase A.5 made both
|
|
44
|
-
* mutable under governance
|
|
44
|
+
* mutable under governance -- rotation flows through a governance
|
|
45
45
|
* proposal + 48h timelock and propagates automatically to every
|
|
46
46
|
* existing AgentAccount.
|
|
47
47
|
*/
|
|
@@ -49,6 +49,11 @@ contract AgentAccountFactory is GovernanceManaged {
|
|
|
49
49
|
AgentAccount public immutable accountImplementation;
|
|
50
50
|
address public immutable delegationManager;
|
|
51
51
|
address public immutable custodyPolicy;
|
|
52
|
+
/// @dev Spec 253 — the ApprovedHashRegistry baked (immutable) into the
|
|
53
|
+
/// AgentAccount impl this factory deploys; surfaced here for
|
|
54
|
+
/// deploy-time auditing. The account's `isValidSignature` 0x03
|
|
55
|
+
/// sentinel consults it; it is NOT a mutable factory-view.
|
|
56
|
+
address public immutable approvedHashRegistry;
|
|
52
57
|
|
|
53
58
|
address public bundlerSigner;
|
|
54
59
|
address public sessionIssuer;
|
|
@@ -73,6 +78,21 @@ contract AgentAccountFactory is GovernanceManaged {
|
|
|
73
78
|
error InsufficientTrusteesForMode(uint8 mode, uint256 actual, uint256 required);
|
|
74
79
|
error TrusteesRequiredForRecoverableMode(uint8 mode);
|
|
75
80
|
error ZeroAddress();
|
|
81
|
+
/// @notice R9.6 / ATL-SEC-05 -- thrown when `params.custodians.length`
|
|
82
|
+
/// exceeds `MAX_INITIAL_CUSTODIANS`. Without this cap,
|
|
83
|
+
/// `_buildValidatorInitData` casts `nSigners` to `uint8`
|
|
84
|
+
/// and silently truncates at 256, computing a wrong default
|
|
85
|
+
/// threshold for a misconfigured account. A self-hurt
|
|
86
|
+
/// footgun, not a cross-account vulnerability -- capped so
|
|
87
|
+
/// the misconfiguration fails closed at construction.
|
|
88
|
+
error TooManyInitialCustodians(uint256 actual, uint256 max);
|
|
89
|
+
|
|
90
|
+
/// @notice R9.6 / ATL-SEC-05 -- upper bound on initial custodians
|
|
91
|
+
/// passed via `AgentAccountInitParams`. 32 is deliberately
|
|
92
|
+
/// generous (every smart-account substrate we surveyed
|
|
93
|
+
/// caps well below 16) but well under the `uint8` truncation
|
|
94
|
+
/// threshold at `_buildValidatorInitData:252`.
|
|
95
|
+
uint256 public constant MAX_INITIAL_CUSTODIANS = 32;
|
|
76
96
|
|
|
77
97
|
constructor(
|
|
78
98
|
IEntryPoint entryPoint_,
|
|
@@ -80,10 +100,12 @@ contract AgentAccountFactory is GovernanceManaged {
|
|
|
80
100
|
address custodyPolicy_,
|
|
81
101
|
address bundlerSigner_,
|
|
82
102
|
address sessionIssuer_,
|
|
83
|
-
address governance_
|
|
103
|
+
address governance_,
|
|
104
|
+
address approvedHashRegistry_
|
|
84
105
|
) GovernanceManaged(governance_) {
|
|
85
106
|
if (custodyPolicy_ == address(0)) revert ZeroAddress();
|
|
86
|
-
accountImplementation = new AgentAccount(entryPoint_);
|
|
107
|
+
accountImplementation = new AgentAccount(entryPoint_, approvedHashRegistry_);
|
|
108
|
+
approvedHashRegistry = approvedHashRegistry_;
|
|
87
109
|
delegationManager = delegationManager_;
|
|
88
110
|
custodyPolicy = custodyPolicy_;
|
|
89
111
|
bundlerSigner = bundlerSigner_;
|
|
@@ -219,6 +241,13 @@ contract AgentAccountFactory is GovernanceManaged {
|
|
|
219
241
|
|
|
220
242
|
bool hasPasskey = params.initialPasskeyX != 0 && params.initialPasskeyY != 0;
|
|
221
243
|
if (params.custodians.length == 0 && !hasPasskey) revert NoInitialSigner();
|
|
244
|
+
// R9.6 / ATL-SEC-05: cap the initial custodian set so
|
|
245
|
+
// `_buildValidatorInitData`'s `uint8(nSigners)` cast can't
|
|
246
|
+
// silently truncate. 32 is well above any rational config and
|
|
247
|
+
// well below the uint8 boundary.
|
|
248
|
+
if (params.custodians.length > MAX_INITIAL_CUSTODIANS) {
|
|
249
|
+
revert TooManyInitialCustodians(params.custodians.length, MAX_INITIAL_CUSTODIANS);
|
|
250
|
+
}
|
|
222
251
|
|
|
223
252
|
if (params.mode == 0) return;
|
|
224
253
|
|
|
@@ -263,6 +263,8 @@ contract SmartAgentPaymaster is BasePaymaster {
|
|
|
263
263
|
// EIP-191 wrap matches what KMS-backed `signMessage({raw})`
|
|
264
264
|
// produces via the v0.7 reference paymaster convention.
|
|
265
265
|
bytes32 ethHash = hash.toEthSignedMessageHash();
|
|
266
|
+
// R6.3: third return (sigVersion) intentionally discarded; the explicit `err` + `recovered != verifyingSigner` revert IS the auth.
|
|
267
|
+
// slither-disable-next-line unused-return
|
|
266
268
|
(address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(ethHash, signature);
|
|
267
269
|
if (err != ECDSA.RecoverError.NoError || recovered != verifyingSigner) {
|
|
268
270
|
// Returning sigFailed=true (per EntryPoint convention)
|
|
@@ -125,11 +125,14 @@ contract UniversalSignatureValidator {
|
|
|
125
125
|
) private pure returns (bool) {
|
|
126
126
|
if (sig.length != 65) return false;
|
|
127
127
|
// Try raw hash first.
|
|
128
|
+
// R6.3: third return (sigVersion) intentionally discarded; the `err` + `recovered == signer` is the auth.
|
|
129
|
+
// slither-disable-next-line unused-return
|
|
128
130
|
(address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, sig);
|
|
129
131
|
if (err == ECDSA.RecoverError.NoError && recovered == signer) return true;
|
|
130
132
|
// Then try the eth-signed prefix variant — many wallets sign this
|
|
131
133
|
// form even when the caller passes a raw digest.
|
|
132
134
|
bytes32 prefixed = hash.toEthSignedMessageHash();
|
|
135
|
+
// slither-disable-next-line unused-return
|
|
133
136
|
(recovered, err, ) = ECDSA.tryRecover(prefixed, sig);
|
|
134
137
|
return err == ECDSA.RecoverError.NoError && recovered == signer;
|
|
135
138
|
}
|
|
@@ -86,6 +86,11 @@ contract DelegationManager is IDelegationManager, ReentrancyGuard {
|
|
|
86
86
|
"Caveat(address enforcer,bytes terms)"
|
|
87
87
|
);
|
|
88
88
|
|
|
89
|
+
/// @dev Gas cap for the read-only caveat evaluation in `verifyAuthorizationForCall`.
|
|
90
|
+
/// Generous for any pure/view enforcer; bounds the gas a stateful enforcer would
|
|
91
|
+
/// burn before reverting under staticcall, so on-chain callers get a cheap `false`.
|
|
92
|
+
uint256 private constant CAVEAT_VIEW_GAS = 1_000_000;
|
|
93
|
+
|
|
89
94
|
/// @dev Revoked delegation hashes
|
|
90
95
|
mapping(bytes32 => bool) private _revoked;
|
|
91
96
|
|
|
@@ -242,6 +247,156 @@ contract DelegationManager is IDelegationManager, ReentrancyGuard {
|
|
|
242
247
|
return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash));
|
|
243
248
|
}
|
|
244
249
|
|
|
250
|
+
/// @notice View-only verification of a delegation chain. Returns whether
|
|
251
|
+
/// the chain is well-formed (signatures, authority chain, not
|
|
252
|
+
/// revoked) WITHOUT executing it.
|
|
253
|
+
///
|
|
254
|
+
/// Spec 242 §6 (PD-9): the `attestations` package uses this to verify
|
|
255
|
+
/// bilateral-consent delegations as signed authorization predicates,
|
|
256
|
+
/// rather than redeeming them as cross-account execution. Returning
|
|
257
|
+
/// `(true, "")` means the delegation chain authorizes the named call from
|
|
258
|
+
/// `sender`. Caveat predicates (CalldataHashEnforcer, AllowedTargets,
|
|
259
|
+
/// AllowedMethods, Timestamp) are NOT executed here — callers are expected
|
|
260
|
+
/// to pin those into the delegation's caveats at issuance time, where
|
|
261
|
+
/// they're hash-bound to the delegation signature.
|
|
262
|
+
///
|
|
263
|
+
/// @param delegations Chain (leaf first; root authority last)
|
|
264
|
+
/// @param sender The address that would be the redeemer
|
|
265
|
+
/// @return ok True if every link verifies
|
|
266
|
+
/// @return reason Empty if ok; otherwise a short rejection reason
|
|
267
|
+
function verifyAuthorization(
|
|
268
|
+
Delegation[] calldata delegations,
|
|
269
|
+
address sender
|
|
270
|
+
) external view returns (bool ok, string memory reason) {
|
|
271
|
+
if (delegations.length == 0) return (false, "empty-chain");
|
|
272
|
+
|
|
273
|
+
for (uint256 i = 0; i < delegations.length; i++) {
|
|
274
|
+
(bool valid, string memory r) = _validateDelegationView(delegations, i, sender);
|
|
275
|
+
if (!valid) return (false, r);
|
|
276
|
+
}
|
|
277
|
+
return (true, "");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/// @notice View verifier for a SPECIFIC call (ADR-0027 / spec 249, RW1-4). Validates the
|
|
281
|
+
/// delegation chain AND evaluates every caveat's `beforeHook` against the exact
|
|
282
|
+
/// `(target, value, data)` — read-only, FAIL-CLOSED.
|
|
283
|
+
/// @dev `verifyAuthorization` checks chain/signature/authority/revocation but NOT the
|
|
284
|
+
/// caveats — too weak to authorize a specific call. This evaluates each caveat via
|
|
285
|
+
/// `staticcall` (the hook is `external`/non-view because stateful enforcers like
|
|
286
|
+
/// Quorum exist). Returns `true` ONLY if the chain validates and EVERY caveat passes
|
|
287
|
+
/// read-only. A caveat that reverts (denied) OR cannot be evaluated read-only (a
|
|
288
|
+
/// stateful enforcer attempting a write under staticcall) yields `false` — so `true`
|
|
289
|
+
/// is a genuine authorization guarantee for this exact call, and `false` means "use
|
|
290
|
+
/// live redemption" (which evaluates stateful caveats properly). The caveat
|
|
291
|
+
/// evaluation mirrors `_runBeforeHooks` exactly (same args, same `sender`-as-redeemer),
|
|
292
|
+
/// so an unknown/no-code enforcer is a no-op here just as it is in redemption. Pure
|
|
293
|
+
/// addition — `redeemDelegation` is untouched.
|
|
294
|
+
/// @param delegations Chain (leaf first; root authority last)
|
|
295
|
+
/// @param sender The address that would be the redeemer
|
|
296
|
+
/// @param target The exact target the delegated call would hit
|
|
297
|
+
/// @param value The exact ETH value
|
|
298
|
+
/// @param data The exact calldata
|
|
299
|
+
function verifyAuthorizationForCall(
|
|
300
|
+
Delegation[] calldata delegations,
|
|
301
|
+
address sender,
|
|
302
|
+
address target,
|
|
303
|
+
uint256 value,
|
|
304
|
+
bytes calldata data
|
|
305
|
+
) external view returns (bool ok, string memory reason) {
|
|
306
|
+
if (delegations.length == 0) return (false, "empty-chain");
|
|
307
|
+
|
|
308
|
+
for (uint256 i = 0; i < delegations.length; i++) {
|
|
309
|
+
(bool valid, string memory r) = _validateDelegationView(delegations, i, sender);
|
|
310
|
+
if (!valid) return (false, r);
|
|
311
|
+
|
|
312
|
+
Delegation calldata d = delegations[i];
|
|
313
|
+
bytes32 dHash = hashDelegation(d);
|
|
314
|
+
for (uint256 j = 0; j < d.caveats.length; j++) {
|
|
315
|
+
// staticcall the same hook redemption would call; success == satisfied read-only.
|
|
316
|
+
// Gas-bounded: a stateful enforcer (which would revert on its SSTORE under
|
|
317
|
+
// staticcall) is capped so an on-chain caller (e.g. RW1-1) sees a cheap, predictable
|
|
318
|
+
// `false`, not an out-of-gas of the whole call. CAVEAT_VIEW_GAS is generous for any
|
|
319
|
+
// pure/view enforcer (timestamp/value/targets/methods are all far below it).
|
|
320
|
+
(bool passed, ) = d.caveats[j].enforcer.staticcall{gas: CAVEAT_VIEW_GAS}(
|
|
321
|
+
abi.encodeWithSelector(
|
|
322
|
+
ICaveatEnforcer.beforeHook.selector,
|
|
323
|
+
d.caveats[j].terms,
|
|
324
|
+
d.caveats[j].args,
|
|
325
|
+
dHash,
|
|
326
|
+
d.delegator,
|
|
327
|
+
sender,
|
|
328
|
+
target,
|
|
329
|
+
value,
|
|
330
|
+
data
|
|
331
|
+
)
|
|
332
|
+
);
|
|
333
|
+
if (!passed) return (false, "caveat-failed");
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return (true, "");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/// @dev View-only sibling of `_validateDelegation`. Same checks, no state
|
|
340
|
+
/// writes, no events. Used by `verifyAuthorization`.
|
|
341
|
+
function _validateDelegationView(
|
|
342
|
+
Delegation[] calldata delegations,
|
|
343
|
+
uint256 i,
|
|
344
|
+
address sender
|
|
345
|
+
) internal view returns (bool ok, string memory reason) {
|
|
346
|
+
Delegation calldata d = delegations[i];
|
|
347
|
+
bytes32 dHash = hashDelegation(d);
|
|
348
|
+
|
|
349
|
+
if (_revoked[dHash]) return (false, "revoked");
|
|
350
|
+
|
|
351
|
+
// Delegate chain
|
|
352
|
+
if (i == 0) {
|
|
353
|
+
if (d.delegate != OPEN_DELEGATION && d.delegate != sender) {
|
|
354
|
+
return (false, "invalid-delegate");
|
|
355
|
+
}
|
|
356
|
+
} else {
|
|
357
|
+
if (d.delegate != OPEN_DELEGATION && d.delegate != delegations[i - 1].delegator) {
|
|
358
|
+
return (false, "invalid-delegate-chain");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Authority chain
|
|
363
|
+
if (d.authority != ROOT_AUTHORITY) {
|
|
364
|
+
if (i + 1 >= delegations.length) return (false, "invalid-authority");
|
|
365
|
+
bytes32 parentHash = hashDelegation(delegations[i + 1]);
|
|
366
|
+
if (d.authority != parentHash) return (false, "invalid-authority-chain");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Signature
|
|
370
|
+
if (!_isValidSignatureBool(d.delegator, dHash, d.signature)) {
|
|
371
|
+
return (false, "invalid-signature");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return (true, "");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/// @dev Bool-returning sibling of `_validateSignature`. Returns false
|
|
378
|
+
/// instead of reverting, so view-only callers can produce a typed
|
|
379
|
+
/// `(ok, reason)` instead of bubbling a revert.
|
|
380
|
+
function _isValidSignatureBool(
|
|
381
|
+
address signer,
|
|
382
|
+
bytes32 digest,
|
|
383
|
+
bytes calldata signature
|
|
384
|
+
) internal view returns (bool) {
|
|
385
|
+
// ERC-1271 path for smart accounts
|
|
386
|
+
if (signer.code.length > 0) {
|
|
387
|
+
try IERC1271(signer).isValidSignature(digest, signature) returns (bytes4 result) {
|
|
388
|
+
return result == IERC1271.isValidSignature.selector;
|
|
389
|
+
} catch {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// EOA — tryRecover the eth-signed message hash
|
|
394
|
+
bytes32 ethHash = digest.toEthSignedMessageHash();
|
|
395
|
+
(address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(ethHash, signature);
|
|
396
|
+
if (err != ECDSA.RecoverError.NoError) return false;
|
|
397
|
+
return recovered == signer;
|
|
398
|
+
}
|
|
399
|
+
|
|
245
400
|
// ─── Internal: Validation ──────────────────────────────────────────
|
|
246
401
|
|
|
247
402
|
function _validateDelegation(
|
|
@@ -66,4 +66,39 @@ interface IDelegationManager {
|
|
|
66
66
|
|
|
67
67
|
/// @notice Check if a delegation has been revoked.
|
|
68
68
|
function isRevoked(bytes32 delegationHash) external view returns (bool);
|
|
69
|
+
|
|
70
|
+
/// @notice View-only verification of a delegation chain. Returns whether
|
|
71
|
+
/// the chain authorizes `sender` to redeem WITHOUT executing it.
|
|
72
|
+
/// Used by trust-substrate contracts (e.g. AttestationRegistry's
|
|
73
|
+
/// bilateral-consent path per spec 242 PD-9) to validate a packed
|
|
74
|
+
/// delegation as a signed authorization predicate.
|
|
75
|
+
/// @param delegations Delegation chain, leaf first
|
|
76
|
+
/// @param sender The address that would be the redeemer
|
|
77
|
+
/// @return ok True if the chain verifies
|
|
78
|
+
/// @return reason Empty if ok; otherwise short rejection reason
|
|
79
|
+
function verifyAuthorization(
|
|
80
|
+
Delegation[] calldata delegations,
|
|
81
|
+
address sender
|
|
82
|
+
) external view returns (bool ok, string memory reason);
|
|
83
|
+
|
|
84
|
+
/// @notice View-only authorization of a SPECIFIC call (ADR-0027 / spec 249 RW1-4):
|
|
85
|
+
/// validates the chain AND evaluates every caveat's `beforeHook` against the exact
|
|
86
|
+
/// `(target, value, data)` read-only, fail-closed. `true` ONLY if the chain validates
|
|
87
|
+
/// and every caveat passes under `staticcall`; a denied OR non-view-evaluable caveat
|
|
88
|
+
/// yields `false` ("use live redemption"). Lets a registry bind consent to the exact
|
|
89
|
+
/// call inside its own transaction. Caveat evaluation mirrors `redeemDelegation`.
|
|
90
|
+
/// @param delegations Delegation chain, leaf first
|
|
91
|
+
/// @param sender The address that would be the redeemer
|
|
92
|
+
/// @param target The exact target of the delegated call
|
|
93
|
+
/// @param value The exact ETH value
|
|
94
|
+
/// @param data The exact calldata
|
|
95
|
+
/// @return ok True if the chain + every caveat authorize this exact call
|
|
96
|
+
/// @return reason Empty if ok; otherwise short rejection reason
|
|
97
|
+
function verifyAuthorizationForCall(
|
|
98
|
+
Delegation[] calldata delegations,
|
|
99
|
+
address sender,
|
|
100
|
+
address target,
|
|
101
|
+
uint256 value,
|
|
102
|
+
bytes calldata data
|
|
103
|
+
) external view returns (bool ok, string memory reason);
|
|
69
104
|
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
import "@openzeppelin/contracts/interfaces/IERC1271.sol";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @title AgreementRegistry
|
|
8
|
+
* @notice Commitment-only registry per spec 241 + ADR-0024 Decision 2.
|
|
9
|
+
*
|
|
10
|
+
* ─── Substrate role ───
|
|
11
|
+
*
|
|
12
|
+
* Layer 8 of the coordination spine — Agreement / Commitment. Stores the
|
|
13
|
+
* commitment HASH only (no agreement bodies, no party SAs). Bodies live in
|
|
14
|
+
* party Joint Vaults; party SAs surface only through joint assertions
|
|
15
|
+
* registered in the AttestationRegistry (which references our `commitmentHash`).
|
|
16
|
+
*
|
|
17
|
+
* ─── Key invariants (AR-*) ───
|
|
18
|
+
*
|
|
19
|
+
* AR-01 register() recomputes the commitment from supplied components
|
|
20
|
+
* AR-02 issuer EIP-712 attestation verified via ERC-1271
|
|
21
|
+
* AR-03 schema validated against (off-chain) ShapeRegistry
|
|
22
|
+
* AR-04 status state-machine: ACTIVE → COMPLETED | DISPUTED | REVOKED
|
|
23
|
+
* AR-05 bilateral signing required for ACTIVE→COMPLETED, ACTIVE→REVOKED
|
|
24
|
+
* AR-06 either-party for ACTIVE→DISPUTED
|
|
25
|
+
* AR-07 NO issuerRevoke / issuerStatusUpdate entrypoint — by static analysis
|
|
26
|
+
* AR-08 nullifier set prevents replay on status transitions
|
|
27
|
+
* AR-09 epoch-bucket timestamps; raw block.timestamp never stored
|
|
28
|
+
* AR-10 isAssertableCommitment view gateway for AttestationRegistry
|
|
29
|
+
* AR-11 party SA addresses NEVER appear in register() calldata
|
|
30
|
+
* AR-12 single on-chain row per agreement (D-37)
|
|
31
|
+
*/
|
|
32
|
+
contract AgreementRegistry {
|
|
33
|
+
// ─── Constants ──────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
uint64 public constant EPOCH_SECONDS = 3600;
|
|
36
|
+
|
|
37
|
+
uint8 public constant STATUS_NONE = 0;
|
|
38
|
+
uint8 public constant STATUS_ACTIVE = 1;
|
|
39
|
+
uint8 public constant STATUS_COMPLETED = 2;
|
|
40
|
+
uint8 public constant STATUS_DISPUTED = 3;
|
|
41
|
+
uint8 public constant STATUS_REVOKED = 4;
|
|
42
|
+
|
|
43
|
+
/// @dev RW1-3 (ADR-0027): the contract RECOMPUTES the transition digest the
|
|
44
|
+
/// parties sign from (agreementCommitment, toStatus, nullifier) — it no
|
|
45
|
+
/// longer trusts a caller-supplied `transitionStructHash`. One canonical
|
|
46
|
+
/// digest model; the TS side derives the same constant (cross-stack
|
|
47
|
+
/// typehash-equality gate). Plain struct hash (no domain separator),
|
|
48
|
+
/// matching the attestation registry's JOINT_CONSENT_TYPEHASH.
|
|
49
|
+
bytes32 internal constant TRANSITION_TYPEHASH =
|
|
50
|
+
keccak256("AgreementTransition(bytes32 agreementCommitment,uint8 toStatus,bytes32 nullifier)");
|
|
51
|
+
|
|
52
|
+
// ─── Errors ─────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
error AlreadyRegistered();
|
|
55
|
+
error NotRegistered();
|
|
56
|
+
error InvalidStatus();
|
|
57
|
+
error InvalidIssuerSignature();
|
|
58
|
+
error InvalidTransitionSignature();
|
|
59
|
+
error NullifierUsed();
|
|
60
|
+
error CommitmentMismatch();
|
|
61
|
+
error SignerNotParty();
|
|
62
|
+
|
|
63
|
+
// ─── Storage ────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
struct AgreementRow {
|
|
66
|
+
bytes32 agreementCommitment; // canonical commitment hash
|
|
67
|
+
bytes32 schemaHash; // SHACL shape hash from ShapeRegistry
|
|
68
|
+
address issuer; // SA whose EIP-712 attestation backs the commitment
|
|
69
|
+
uint8 status;
|
|
70
|
+
uint64 createdEpochBucket;
|
|
71
|
+
uint64 lastTransitionEpochBucket;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// @dev key = agreementCommitment hash
|
|
75
|
+
mapping(bytes32 => AgreementRow) private _rows;
|
|
76
|
+
|
|
77
|
+
/// @dev AR-08: nullifier set for replay protection on status transitions.
|
|
78
|
+
mapping(bytes32 => bool) private _nullifiers;
|
|
79
|
+
|
|
80
|
+
// ─── Requests ───────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
/// @notice The components the contract recomputes the commitment from
|
|
83
|
+
/// (per spec 241 §3). Party SAs are intentionally NOT in this
|
|
84
|
+
/// payload — AR-11.
|
|
85
|
+
struct AgreementIssuancePayload {
|
|
86
|
+
bytes32 schemaHash;
|
|
87
|
+
address issuer;
|
|
88
|
+
/// @dev EIP-712 attestation hash by the issuer (over agreementCommitment + schemaHash).
|
|
89
|
+
bytes32 attestationStructHash;
|
|
90
|
+
/// @dev issuer's signature over `attestationStructHash`.
|
|
91
|
+
bytes issuerSignature;
|
|
92
|
+
/// @dev The asserted canonical commitment hash; we verify recomputation.
|
|
93
|
+
bytes32 agreementCommitment;
|
|
94
|
+
/// @dev Component commitments for AR-01 recomputation:
|
|
95
|
+
/// keccak256(partySet, issuerCommitment, termsCommitment, scheduleCommitment, salt) == agreementCommitment
|
|
96
|
+
bytes32 partySetCommitment;
|
|
97
|
+
bytes32 issuerCommitment;
|
|
98
|
+
bytes32 termsCommitment;
|
|
99
|
+
bytes32 scheduleCommitment;
|
|
100
|
+
uint256 salt;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
struct StatusUpdatePayload {
|
|
104
|
+
bytes32 agreementCommitment;
|
|
105
|
+
uint8 toStatus;
|
|
106
|
+
bytes32 nullifier;
|
|
107
|
+
/// @dev Signatures over the RECOMPUTED transition digest (RW1-3): 1 for
|
|
108
|
+
/// either-party (DISPUTED), 2 for bilateral (COMPLETED/REVOKED).
|
|
109
|
+
bytes signature1;
|
|
110
|
+
bytes signature2;
|
|
111
|
+
/// @dev Party SAs that signed. For DISPUTED: signer1 only. For COMPLETED/REVOKED: both.
|
|
112
|
+
address signer1;
|
|
113
|
+
address signer2;
|
|
114
|
+
/// @dev RW1-2 (ADR-0027): the agreement's parties + commitment components, REVEALED at
|
|
115
|
+
/// transition time so the contract can recompute the commitment and PROVE the signers
|
|
116
|
+
/// are the parties. (Registration stays commitment-only — AR-12; the transition is
|
|
117
|
+
/// already a public state change that names its signers.)
|
|
118
|
+
address party1;
|
|
119
|
+
address party2;
|
|
120
|
+
bytes32 issuerCommitment;
|
|
121
|
+
bytes32 termsCommitment;
|
|
122
|
+
bytes32 scheduleCommitment;
|
|
123
|
+
uint256 commitmentSalt;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── Events ─────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
event AgreementRegistered(
|
|
129
|
+
bytes32 indexed agreementCommitment,
|
|
130
|
+
address indexed issuer,
|
|
131
|
+
bytes32 indexed schemaHash,
|
|
132
|
+
uint64 epochBucket
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
event StatusUpdated(
|
|
136
|
+
bytes32 indexed agreementCommitment,
|
|
137
|
+
uint8 indexed fromStatus,
|
|
138
|
+
uint8 indexed toStatus,
|
|
139
|
+
uint64 epochBucket
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// ─── Public surface ─────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
/// @notice Register an agreement by its commitment hash.
|
|
145
|
+
function register(AgreementIssuancePayload calldata p) external returns (bytes32) {
|
|
146
|
+
// AR-01 — recompute commitment from components
|
|
147
|
+
bytes32 recomputed = keccak256(
|
|
148
|
+
abi.encode(p.partySetCommitment, p.issuerCommitment, p.termsCommitment, p.scheduleCommitment, p.salt)
|
|
149
|
+
);
|
|
150
|
+
if (recomputed != p.agreementCommitment) revert CommitmentMismatch();
|
|
151
|
+
|
|
152
|
+
if (_rows[p.agreementCommitment].agreementCommitment != bytes32(0)) {
|
|
153
|
+
revert AlreadyRegistered();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// AR-02 — verify issuer signature over the attestation hash
|
|
157
|
+
if (!_isValidSignatureBool(p.issuer, p.attestationStructHash, p.issuerSignature)) {
|
|
158
|
+
revert InvalidIssuerSignature();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
uint64 bucket = uint64(block.timestamp) / EPOCH_SECONDS;
|
|
162
|
+
|
|
163
|
+
_rows[p.agreementCommitment] = AgreementRow({
|
|
164
|
+
agreementCommitment: p.agreementCommitment,
|
|
165
|
+
schemaHash: p.schemaHash,
|
|
166
|
+
issuer: p.issuer,
|
|
167
|
+
status: STATUS_ACTIVE,
|
|
168
|
+
createdEpochBucket: bucket,
|
|
169
|
+
lastTransitionEpochBucket: bucket
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
emit AgreementRegistered(p.agreementCommitment, p.issuer, p.schemaHash, bucket);
|
|
173
|
+
return p.agreementCommitment;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// @notice Update the agreement status. Signing requirements (AR-04..AR-06):
|
|
177
|
+
/// - DISPUTED: signer1 (either party) only
|
|
178
|
+
/// - COMPLETED / REVOKED: both signer1 + signer2 (bilateral)
|
|
179
|
+
/// RW1-2 (ADR-0027): the caller REVEALS the parties + commitment
|
|
180
|
+
/// components; the contract recomputes the commitment (register's
|
|
181
|
+
/// exact formula) and requires each signer to BE one of the two
|
|
182
|
+
/// parties. The on-chain row still stores NO party SAs (AR-11 covers
|
|
183
|
+
/// register() calldata only); the parties surface here because a
|
|
184
|
+
/// status transition is already a public state change that names its
|
|
185
|
+
/// signers. Replay protection comes from the nullifier. RW1-3: the
|
|
186
|
+
/// transition digest the parties sign is RECOMPUTED here from
|
|
187
|
+
/// (agreementCommitment, toStatus, nullifier) — not caller-supplied.
|
|
188
|
+
function updateStatus(StatusUpdatePayload calldata p) external {
|
|
189
|
+
AgreementRow storage row = _rows[p.agreementCommitment];
|
|
190
|
+
if (row.agreementCommitment == bytes32(0)) revert NotRegistered();
|
|
191
|
+
|
|
192
|
+
// Status transition validity (AR-04)
|
|
193
|
+
if (row.status != STATUS_ACTIVE && row.status != STATUS_DISPUTED) revert InvalidStatus();
|
|
194
|
+
if (
|
|
195
|
+
p.toStatus != STATUS_COMPLETED &&
|
|
196
|
+
p.toStatus != STATUS_DISPUTED &&
|
|
197
|
+
p.toStatus != STATUS_REVOKED
|
|
198
|
+
) {
|
|
199
|
+
revert InvalidStatus();
|
|
200
|
+
}
|
|
201
|
+
// DISPUTED → COMPLETED / REVOKED needs bilateral.
|
|
202
|
+
// ACTIVE → DISPUTED: either-party allowed.
|
|
203
|
+
// ACTIVE → COMPLETED / REVOKED: bilateral.
|
|
204
|
+
|
|
205
|
+
// AR-08 — replay protection
|
|
206
|
+
if (_nullifiers[p.nullifier]) revert NullifierUsed();
|
|
207
|
+
_nullifiers[p.nullifier] = true;
|
|
208
|
+
|
|
209
|
+
// RW1-2 (ADR-0027): PROVE the signers are the agreement's parties. Recompute the agreement
|
|
210
|
+
// commitment from the REVEALED parties + components (register's exact formula) and require it
|
|
211
|
+
// to equal the row's commitment; then require each signer to BE a party. The caller-supplied
|
|
212
|
+
// signer set is no longer trusted (a nonzero commitment ref / arbitrary signer is not authority).
|
|
213
|
+
{
|
|
214
|
+
bytes32 partySet = keccak256(abi.encodePacked(p.party1, p.party2));
|
|
215
|
+
bytes32 recomputed = keccak256(
|
|
216
|
+
abi.encode(partySet, p.issuerCommitment, p.termsCommitment, p.scheduleCommitment, p.commitmentSalt)
|
|
217
|
+
);
|
|
218
|
+
if (recomputed != p.agreementCommitment) revert CommitmentMismatch();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// RW1-3 (ADR-0027): RECOMPUTE the transition digest the parties sign — do not trust a
|
|
222
|
+
// caller-supplied hash. The signed payload is canonically bound to (commitment, toStatus,
|
|
223
|
+
// nullifier) here, on chain.
|
|
224
|
+
bytes32 transitionDigest = keccak256(
|
|
225
|
+
abi.encode(TRANSITION_TYPEHASH, p.agreementCommitment, p.toStatus, p.nullifier)
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
bool bilateralRequired = (p.toStatus != STATUS_DISPUTED);
|
|
229
|
+
|
|
230
|
+
// signer1 must BE a party, and must have signed.
|
|
231
|
+
if (p.signer1 != p.party1 && p.signer1 != p.party2) revert SignerNotParty();
|
|
232
|
+
if (!_isValidSignatureBool(p.signer1, transitionDigest, p.signature1)) {
|
|
233
|
+
revert InvalidTransitionSignature();
|
|
234
|
+
}
|
|
235
|
+
if (bilateralRequired) {
|
|
236
|
+
// both DISTINCT parties must sign.
|
|
237
|
+
if (p.signer2 == address(0) || p.signer2 == p.signer1) revert InvalidTransitionSignature();
|
|
238
|
+
if (p.signer2 != p.party1 && p.signer2 != p.party2) revert SignerNotParty();
|
|
239
|
+
if (!_isValidSignatureBool(p.signer2, transitionDigest, p.signature2)) {
|
|
240
|
+
revert InvalidTransitionSignature();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
uint8 fromStatus = row.status;
|
|
245
|
+
row.status = p.toStatus;
|
|
246
|
+
row.lastTransitionEpochBucket = uint64(block.timestamp) / EPOCH_SECONDS;
|
|
247
|
+
|
|
248
|
+
emit StatusUpdated(p.agreementCommitment, fromStatus, p.toStatus, row.lastTransitionEpochBucket);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/// @notice Gateway for spec 242 AttestationRegistry.assertJointAgreement.
|
|
252
|
+
/// Returns (true, "") if the commitment exists and is in a state
|
|
253
|
+
/// that permits public joint assertion (currently any non-NONE
|
|
254
|
+
/// status; revoked agreements can still be jointly disclosed as
|
|
255
|
+
/// historical record).
|
|
256
|
+
function isAssertableCommitment(
|
|
257
|
+
bytes32 agreementCommitment,
|
|
258
|
+
address /* actor */
|
|
259
|
+
) external view returns (bool ok, string memory reason) {
|
|
260
|
+
AgreementRow memory row = _rows[agreementCommitment];
|
|
261
|
+
if (row.agreementCommitment == bytes32(0)) return (false, "not-registered");
|
|
262
|
+
return (true, "");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/// @notice Get the on-chain row.
|
|
266
|
+
function getRecord(bytes32 agreementCommitment) external view returns (AgreementRow memory) {
|
|
267
|
+
AgreementRow memory row = _rows[agreementCommitment];
|
|
268
|
+
if (row.agreementCommitment == bytes32(0)) revert NotRegistered();
|
|
269
|
+
return row;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/// @notice Has a nullifier been consumed?
|
|
273
|
+
function isNullifierUsed(bytes32 n) external view returns (bool) {
|
|
274
|
+
return _nullifiers[n];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ─── Internal ───────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
function _isValidSignatureBool(
|
|
280
|
+
address signer,
|
|
281
|
+
bytes32 digest,
|
|
282
|
+
bytes memory signature
|
|
283
|
+
) internal view returns (bool) {
|
|
284
|
+
if (signer.code.length > 0) {
|
|
285
|
+
try IERC1271(signer).isValidSignature(digest, signature) returns (bytes4 result) {
|
|
286
|
+
return result == IERC1271.isValidSignature.selector;
|
|
287
|
+
} catch {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest));
|
|
292
|
+
if (signature.length != 65) return false;
|
|
293
|
+
bytes32 r;
|
|
294
|
+
bytes32 s;
|
|
295
|
+
uint8 v;
|
|
296
|
+
// solhint-disable-next-line no-inline-assembly
|
|
297
|
+
assembly {
|
|
298
|
+
r := mload(add(signature, 0x20))
|
|
299
|
+
s := mload(add(signature, 0x40))
|
|
300
|
+
v := byte(0, mload(add(signature, 0x60)))
|
|
301
|
+
}
|
|
302
|
+
if (v < 27) v += 27;
|
|
303
|
+
address recovered = ecrecover(ethHash, v, r, s);
|
|
304
|
+
return recovered != address(0) && recovered == signer;
|
|
305
|
+
}
|
|
306
|
+
}
|