@agenticprimitives/contracts 1.0.0-alpha.24 → 1.0.0-alpha.26
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 +5 -0
- package/README.md +2 -0
- package/deployments-faithchain.json +2 -1
- package/dist/abi/AgentAccount.json +45 -0
- package/dist/abi/AgentAccountFactory.json +18 -0
- package/dist/abi/ApprovedHashRegistry.json +47 -2
- package/dist/abi/CustodyPolicy.json +11 -0
- package/dist/abi/DigestBindingEnforcer.json +125 -3
- package/dist/abi/ICustodyPolicyAccountModuleView.json +31 -0
- package/dist/abi/ReceiptAnchorRegistry.json +153 -0
- package/dist/abi/index.d.ts +2 -0
- package/dist/abi/index.js +2 -0
- package/dist/deployments/faithchain.d.ts +1 -0
- package/dist/deployments/faithchain.js +1 -0
- package/package.json +16 -5
- package/src/AgentAccount.sol +67 -6
- package/src/AgentAccountFactory.sol +17 -10
- package/src/ApprovedHashRegistry.sol +41 -6
- package/src/custody/CustodyPolicy.sol +20 -0
- package/src/enforcers/DigestBindingEnforcer.sol +45 -10
- package/src/provenance/ReceiptAnchorRegistry.sol +55 -0
package/AUDIT.md
CHANGED
|
@@ -52,6 +52,11 @@ This package ships the **on-chain enforcement layer** for the agenticprimitives
|
|
|
52
52
|
| 2.12 | **CustodyPolicy reinstall is permanently forbidden.** | ✅ CLOSED audit C-11 | `CustodyPolicy.sol:onUninstall` + `onInstall` reinstall gate; tested in `CustodyPolicyWave2C.t.sol` |
|
|
53
53
|
| 2.13 | **`SmartAgentPaymaster.getHash` binds `address(entryPoint)` + chainId.** | ✅ CLOSED H7-C.7 / CON-PAYMASTER-004 | `SmartAgentPaymaster.sol:206-222`; tested in `SmartAgentPaymasterValidateR610.t.sol` |
|
|
54
54
|
| 2.14 | **`SmartAgentPaymaster` constructor takes EXPLICIT `devMode` (no implicit fail-open).** | ✅ CLOSED R5.7 / PKG-PAYMASTER-002 / external audit P0-2 | `SmartAgentPaymaster.sol:126-141`; tested in `SmartAgentPaymaster.t.sol::test_R5_7_*` |
|
|
55
|
+
| 2.15 | **The account's `execute` never reaches its own custody-tier contracts from below the custody tier.** `CustodyPolicy.onUninstall` reverts `StillInstalledOn` unless `uninstallModule` already dropped the module; a delegation stack (`_delegationExecCtx`) may not target `custodyPolicy` or `approvedHashRegistry` at all. | ✅ CLOSED R917-C-1 (spec 408 §1.1) | `CustodyPolicy.sol:onUninstall`, `AgentAccount.sol:_isAccountTrustedTarget`; `test/HardeningR917.t.sol` |
|
|
56
|
+
| 2.16 | **An approved hash dies with the custody set that approved it.** The `0x03` sentinel consults `isApproved(this, keccak256(hash ‖ custodyEpoch))`; `custodyEpoch` bumps on every credential retirement (`removeCustodian` / `removePasskey`); `ApprovedHashRegistry.revokeAll()` voids per signer. | ✅ CLOSED R917-C-2 (spec 408 §1.2) | `AgentAccount.sol:approvedHashKey`, `ApprovedHashRegistry.sol`; `test/HardeningR917.t.sol`, `test/ApprovedHashIsValidSignature.t.sol` |
|
|
57
|
+
| 2.17 | **A digest-bound redemption is single-use per step.** `DigestBindingEnforcer` is stateful + DM-only: `(delegator, delegationHash, kind, stepNonce)` consumed in `beforeHook`. | ✅ CLOSED R917-C-4 (spec 408 §1.4) | `enforcers/DigestBindingEnforcer.sol`; `test/DigestBindingEnforcer.t.sol` |
|
|
58
|
+
| 2.18 | **Network posture is a constructor fact, never a chain-id list.** `AgentAccountFactory.relaxedT4Floor` (immutable) is the only way to lift the 1 h T4 floor; recorded in the deployments JSON; refused by `check:production-deploy` off anvil. | ✅ CLOSED R917-C-5 (spec 408 §1.3) | `AgentAccountFactory.sol:_custodyInitData`; `test/AgentAccountFactoryMode.t.sol` |
|
|
59
|
+
| 2.19 | **Receipt anchors are keyed by who anchored.** `(anchoredBy, receiptDigest)` — no squat on a public mempool. | ✅ CLOSED R917-C-3 (spec 408 §1.5) | `provenance/ReceiptAnchorRegistry.sol`; `test/provenance/ReceiptAnchorRegistry.t.sol` |
|
|
55
60
|
|
|
56
61
|
## 3. Per-contract notes for the external review
|
|
57
62
|
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @agenticprimitives/contracts
|
|
2
2
|
|
|
3
|
+
> Part of **[Agentic Primitives](https://agenticprimitives.dev)** — the open-source trust substrate for agentic applications: identity that can sign, authority checked at act time, evidence the owner carries. [Developer kit](https://github.com/agentictrustlabs/agentic-primitives) · [All packages](https://agenticprimitives.dev/developers)
|
|
4
|
+
|
|
3
5
|
**Contracts and SDK are one artifact.**
|
|
4
6
|
|
|
5
7
|
This is the on-chain enforcement layer of the agenticprimitives substrate — 42 Solidity sources, all present and audited under `src/`, none of them stubs. ERC-4337 v0.7 Smart Agent core, ERC-7710-style delegation manager with caveat enforcers, an ERC-7579 modular CustodyPolicy, a three-mode paymaster, `.agent` TLD naming with hierarchical subregistries, ontology and profile resolvers, identity facets, and an ERC-6492-aware UniversalSignatureValidator. The package publishes ABI subpaths, per-network deployment JSON, flattened sources, and the Solidity itself — so a TypeScript consumer pulls addresses and ABIs from the same artifact the auditors read.
|
|
@@ -45,5 +45,6 @@
|
|
|
45
45
|
"household": "0x9c3373E8F28739Df3aA478C18209fD7eeD769F5b"
|
|
46
46
|
},
|
|
47
47
|
"agentRegistryBase": "0x0Be172F1b5cfD1d084CEc2cF2Af0A0cE9023Deb1",
|
|
48
|
-
"digestBindingEnforcer": "0xA3bb9BCC9b2F6F2419E1aBe5ED6Fd5399b9E68e1"
|
|
48
|
+
"digestBindingEnforcer": "0xA3bb9BCC9b2F6F2419E1aBe5ED6Fd5399b9E68e1",
|
|
49
|
+
"receiptAnchorRegistry": "0x0e339334B4438F7546FD53837FDC8943cB3Cf54C"
|
|
49
50
|
}
|
|
@@ -99,6 +99,25 @@
|
|
|
99
99
|
"outputs": [],
|
|
100
100
|
"stateMutability": "nonpayable"
|
|
101
101
|
},
|
|
102
|
+
{
|
|
103
|
+
"type": "function",
|
|
104
|
+
"name": "approvedHashKey",
|
|
105
|
+
"inputs": [
|
|
106
|
+
{
|
|
107
|
+
"name": "hash",
|
|
108
|
+
"type": "bytes32",
|
|
109
|
+
"internalType": "bytes32"
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
"outputs": [
|
|
113
|
+
{
|
|
114
|
+
"name": "",
|
|
115
|
+
"type": "bytes32",
|
|
116
|
+
"internalType": "bytes32"
|
|
117
|
+
}
|
|
118
|
+
],
|
|
119
|
+
"stateMutability": "view"
|
|
120
|
+
},
|
|
102
121
|
{
|
|
103
122
|
"type": "function",
|
|
104
123
|
"name": "bundlerSigner",
|
|
@@ -138,6 +157,19 @@
|
|
|
138
157
|
],
|
|
139
158
|
"stateMutability": "view"
|
|
140
159
|
},
|
|
160
|
+
{
|
|
161
|
+
"type": "function",
|
|
162
|
+
"name": "custodyEpoch",
|
|
163
|
+
"inputs": [],
|
|
164
|
+
"outputs": [
|
|
165
|
+
{
|
|
166
|
+
"name": "",
|
|
167
|
+
"type": "uint64",
|
|
168
|
+
"internalType": "uint64"
|
|
169
|
+
}
|
|
170
|
+
],
|
|
171
|
+
"stateMutability": "view"
|
|
172
|
+
},
|
|
141
173
|
{
|
|
142
174
|
"type": "function",
|
|
143
175
|
"name": "decodeWebAuthnAssertion",
|
|
@@ -1071,6 +1103,19 @@
|
|
|
1071
1103
|
],
|
|
1072
1104
|
"anonymous": false
|
|
1073
1105
|
},
|
|
1106
|
+
{
|
|
1107
|
+
"type": "event",
|
|
1108
|
+
"name": "CustodyEpochBumped",
|
|
1109
|
+
"inputs": [
|
|
1110
|
+
{
|
|
1111
|
+
"name": "epoch",
|
|
1112
|
+
"type": "uint64",
|
|
1113
|
+
"indexed": false,
|
|
1114
|
+
"internalType": "uint64"
|
|
1115
|
+
}
|
|
1116
|
+
],
|
|
1117
|
+
"anonymous": false
|
|
1118
|
+
},
|
|
1074
1119
|
{
|
|
1075
1120
|
"type": "event",
|
|
1076
1121
|
"name": "DelegationManagerRotated",
|
|
@@ -36,6 +36,11 @@
|
|
|
36
36
|
"name": "approvedHashRegistry_",
|
|
37
37
|
"type": "address",
|
|
38
38
|
"internalType": "address"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "relaxedT4Floor_",
|
|
42
|
+
"type": "bool",
|
|
43
|
+
"internalType": "bool"
|
|
39
44
|
}
|
|
40
45
|
],
|
|
41
46
|
"stateMutability": "nonpayable"
|
|
@@ -289,6 +294,19 @@
|
|
|
289
294
|
],
|
|
290
295
|
"stateMutability": "view"
|
|
291
296
|
},
|
|
297
|
+
{
|
|
298
|
+
"type": "function",
|
|
299
|
+
"name": "relaxedT4Floor",
|
|
300
|
+
"inputs": [],
|
|
301
|
+
"outputs": [
|
|
302
|
+
{
|
|
303
|
+
"name": "",
|
|
304
|
+
"type": "bool",
|
|
305
|
+
"internalType": "bool"
|
|
306
|
+
}
|
|
307
|
+
],
|
|
308
|
+
"stateMutability": "view"
|
|
309
|
+
},
|
|
292
310
|
{
|
|
293
311
|
"type": "function",
|
|
294
312
|
"name": "sessionIssuer",
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
"name": "approved",
|
|
18
18
|
"inputs": [
|
|
19
19
|
{
|
|
20
|
-
"name": "",
|
|
20
|
+
"name": "signer",
|
|
21
21
|
"type": "address",
|
|
22
22
|
"internalType": "address"
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
|
-
"name": "",
|
|
25
|
+
"name": "hash",
|
|
26
26
|
"type": "bytes32",
|
|
27
27
|
"internalType": "bytes32"
|
|
28
28
|
}
|
|
@@ -36,6 +36,25 @@
|
|
|
36
36
|
],
|
|
37
37
|
"stateMutability": "view"
|
|
38
38
|
},
|
|
39
|
+
{
|
|
40
|
+
"type": "function",
|
|
41
|
+
"name": "epochOf",
|
|
42
|
+
"inputs": [
|
|
43
|
+
{
|
|
44
|
+
"name": "signer",
|
|
45
|
+
"type": "address",
|
|
46
|
+
"internalType": "address"
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
"outputs": [
|
|
50
|
+
{
|
|
51
|
+
"name": "",
|
|
52
|
+
"type": "uint64",
|
|
53
|
+
"internalType": "uint64"
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"stateMutability": "view"
|
|
57
|
+
},
|
|
39
58
|
{
|
|
40
59
|
"type": "function",
|
|
41
60
|
"name": "isApproved",
|
|
@@ -60,6 +79,13 @@
|
|
|
60
79
|
],
|
|
61
80
|
"stateMutability": "view"
|
|
62
81
|
},
|
|
82
|
+
{
|
|
83
|
+
"type": "function",
|
|
84
|
+
"name": "revokeAll",
|
|
85
|
+
"inputs": [],
|
|
86
|
+
"outputs": [],
|
|
87
|
+
"stateMutability": "nonpayable"
|
|
88
|
+
},
|
|
63
89
|
{
|
|
64
90
|
"type": "function",
|
|
65
91
|
"name": "revokeHash",
|
|
@@ -73,6 +99,25 @@
|
|
|
73
99
|
"outputs": [],
|
|
74
100
|
"stateMutability": "nonpayable"
|
|
75
101
|
},
|
|
102
|
+
{
|
|
103
|
+
"type": "event",
|
|
104
|
+
"name": "AllHashesRevoked",
|
|
105
|
+
"inputs": [
|
|
106
|
+
{
|
|
107
|
+
"name": "signer",
|
|
108
|
+
"type": "address",
|
|
109
|
+
"indexed": true,
|
|
110
|
+
"internalType": "address"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"name": "epoch",
|
|
114
|
+
"type": "uint64",
|
|
115
|
+
"indexed": false,
|
|
116
|
+
"internalType": "uint64"
|
|
117
|
+
}
|
|
118
|
+
],
|
|
119
|
+
"anonymous": false
|
|
120
|
+
},
|
|
76
121
|
{
|
|
77
122
|
"type": "event",
|
|
78
123
|
"name": "HashApproved",
|
|
@@ -1076,6 +1076,17 @@
|
|
|
1076
1076
|
}
|
|
1077
1077
|
]
|
|
1078
1078
|
},
|
|
1079
|
+
{
|
|
1080
|
+
"type": "error",
|
|
1081
|
+
"name": "StillInstalledOn",
|
|
1082
|
+
"inputs": [
|
|
1083
|
+
{
|
|
1084
|
+
"name": "account",
|
|
1085
|
+
"type": "address",
|
|
1086
|
+
"internalType": "address"
|
|
1087
|
+
}
|
|
1088
|
+
]
|
|
1089
|
+
},
|
|
1079
1090
|
{
|
|
1080
1091
|
"type": "error",
|
|
1081
1092
|
"name": "SystemPaused",
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"type": "constructor",
|
|
4
|
+
"inputs": [
|
|
5
|
+
{
|
|
6
|
+
"name": "delegationManager_",
|
|
7
|
+
"type": "address",
|
|
8
|
+
"internalType": "address"
|
|
9
|
+
}
|
|
10
|
+
],
|
|
11
|
+
"stateMutability": "nonpayable"
|
|
12
|
+
},
|
|
2
13
|
{
|
|
3
14
|
"type": "function",
|
|
4
15
|
"name": "afterHook",
|
|
@@ -62,12 +73,12 @@
|
|
|
62
73
|
"internalType": "bytes"
|
|
63
74
|
},
|
|
64
75
|
{
|
|
65
|
-
"name": "",
|
|
76
|
+
"name": "delegationHash",
|
|
66
77
|
"type": "bytes32",
|
|
67
78
|
"internalType": "bytes32"
|
|
68
79
|
},
|
|
69
80
|
{
|
|
70
|
-
"name": "",
|
|
81
|
+
"name": "delegator",
|
|
71
82
|
"type": "address",
|
|
72
83
|
"internalType": "address"
|
|
73
84
|
},
|
|
@@ -93,7 +104,91 @@
|
|
|
93
104
|
}
|
|
94
105
|
],
|
|
95
106
|
"outputs": [],
|
|
96
|
-
"stateMutability": "
|
|
107
|
+
"stateMutability": "nonpayable"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"type": "function",
|
|
111
|
+
"name": "delegationManager",
|
|
112
|
+
"inputs": [],
|
|
113
|
+
"outputs": [
|
|
114
|
+
{
|
|
115
|
+
"name": "",
|
|
116
|
+
"type": "address",
|
|
117
|
+
"internalType": "address"
|
|
118
|
+
}
|
|
119
|
+
],
|
|
120
|
+
"stateMutability": "view"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"type": "function",
|
|
124
|
+
"name": "isUsed",
|
|
125
|
+
"inputs": [
|
|
126
|
+
{
|
|
127
|
+
"name": "delegator",
|
|
128
|
+
"type": "address",
|
|
129
|
+
"internalType": "address"
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"name": "delegationHash",
|
|
133
|
+
"type": "bytes32",
|
|
134
|
+
"internalType": "bytes32"
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"name": "kind",
|
|
138
|
+
"type": "bytes32",
|
|
139
|
+
"internalType": "bytes32"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"name": "stepNonce",
|
|
143
|
+
"type": "bytes32",
|
|
144
|
+
"internalType": "bytes32"
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
"outputs": [
|
|
148
|
+
{
|
|
149
|
+
"name": "",
|
|
150
|
+
"type": "bool",
|
|
151
|
+
"internalType": "bool"
|
|
152
|
+
}
|
|
153
|
+
],
|
|
154
|
+
"stateMutability": "view"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"type": "event",
|
|
158
|
+
"name": "DigestBindingRedeemed",
|
|
159
|
+
"inputs": [
|
|
160
|
+
{
|
|
161
|
+
"name": "delegator",
|
|
162
|
+
"type": "address",
|
|
163
|
+
"indexed": true,
|
|
164
|
+
"internalType": "address"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"name": "delegationHash",
|
|
168
|
+
"type": "bytes32",
|
|
169
|
+
"indexed": true,
|
|
170
|
+
"internalType": "bytes32"
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"name": "kind",
|
|
174
|
+
"type": "bytes32",
|
|
175
|
+
"indexed": true,
|
|
176
|
+
"internalType": "bytes32"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"name": "presented",
|
|
180
|
+
"type": "bytes32",
|
|
181
|
+
"indexed": false,
|
|
182
|
+
"internalType": "bytes32"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"name": "stepNonce",
|
|
186
|
+
"type": "bytes32",
|
|
187
|
+
"indexed": false,
|
|
188
|
+
"internalType": "bytes32"
|
|
189
|
+
}
|
|
190
|
+
],
|
|
191
|
+
"anonymous": false
|
|
97
192
|
},
|
|
98
193
|
{
|
|
99
194
|
"type": "error",
|
|
@@ -125,5 +220,32 @@
|
|
|
125
220
|
"internalType": "bytes32"
|
|
126
221
|
}
|
|
127
222
|
]
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"type": "error",
|
|
226
|
+
"name": "NotDelegationManager",
|
|
227
|
+
"inputs": [
|
|
228
|
+
{
|
|
229
|
+
"name": "caller",
|
|
230
|
+
"type": "address",
|
|
231
|
+
"internalType": "address"
|
|
232
|
+
}
|
|
233
|
+
]
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"type": "error",
|
|
237
|
+
"name": "StepNonceReused",
|
|
238
|
+
"inputs": [
|
|
239
|
+
{
|
|
240
|
+
"name": "kind",
|
|
241
|
+
"type": "bytes32",
|
|
242
|
+
"internalType": "bytes32"
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
"name": "stepNonce",
|
|
246
|
+
"type": "bytes32",
|
|
247
|
+
"internalType": "bytes32"
|
|
248
|
+
}
|
|
249
|
+
]
|
|
128
250
|
}
|
|
129
251
|
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"type": "function",
|
|
4
|
+
"name": "isModuleInstalled",
|
|
5
|
+
"inputs": [
|
|
6
|
+
{
|
|
7
|
+
"name": "moduleTypeId",
|
|
8
|
+
"type": "uint256",
|
|
9
|
+
"internalType": "uint256"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"name": "module",
|
|
13
|
+
"type": "address",
|
|
14
|
+
"internalType": "address"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "additionalContext",
|
|
18
|
+
"type": "bytes",
|
|
19
|
+
"internalType": "bytes"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"outputs": [
|
|
23
|
+
{
|
|
24
|
+
"name": "",
|
|
25
|
+
"type": "bool",
|
|
26
|
+
"internalType": "bool"
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"stateMutability": "view"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"type": "function",
|
|
4
|
+
"name": "anchor",
|
|
5
|
+
"inputs": [
|
|
6
|
+
{
|
|
7
|
+
"name": "receiptDigest",
|
|
8
|
+
"type": "bytes32",
|
|
9
|
+
"internalType": "bytes32"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"name": "intentDigest",
|
|
13
|
+
"type": "bytes32",
|
|
14
|
+
"internalType": "bytes32"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "mandateRef",
|
|
18
|
+
"type": "bytes32",
|
|
19
|
+
"internalType": "bytes32"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"outputs": [],
|
|
23
|
+
"stateMutability": "nonpayable"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"type": "function",
|
|
27
|
+
"name": "anchorOf",
|
|
28
|
+
"inputs": [
|
|
29
|
+
{
|
|
30
|
+
"name": "anchoredBy",
|
|
31
|
+
"type": "address",
|
|
32
|
+
"internalType": "address"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "receiptDigest",
|
|
36
|
+
"type": "bytes32",
|
|
37
|
+
"internalType": "bytes32"
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"outputs": [
|
|
41
|
+
{
|
|
42
|
+
"name": "",
|
|
43
|
+
"type": "tuple",
|
|
44
|
+
"internalType": "struct ReceiptAnchorRegistry.Anchor",
|
|
45
|
+
"components": [
|
|
46
|
+
{
|
|
47
|
+
"name": "anchoredBy",
|
|
48
|
+
"type": "address",
|
|
49
|
+
"internalType": "address"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"name": "at",
|
|
53
|
+
"type": "uint64",
|
|
54
|
+
"internalType": "uint64"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "intentDigest",
|
|
58
|
+
"type": "bytes32",
|
|
59
|
+
"internalType": "bytes32"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "mandateRef",
|
|
63
|
+
"type": "bytes32",
|
|
64
|
+
"internalType": "bytes32"
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"stateMutability": "view"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"type": "function",
|
|
73
|
+
"name": "isAnchored",
|
|
74
|
+
"inputs": [
|
|
75
|
+
{
|
|
76
|
+
"name": "anchoredBy",
|
|
77
|
+
"type": "address",
|
|
78
|
+
"internalType": "address"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"name": "receiptDigest",
|
|
82
|
+
"type": "bytes32",
|
|
83
|
+
"internalType": "bytes32"
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
"outputs": [
|
|
87
|
+
{
|
|
88
|
+
"name": "",
|
|
89
|
+
"type": "bool",
|
|
90
|
+
"internalType": "bool"
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
"stateMutability": "view"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"type": "event",
|
|
97
|
+
"name": "ReceiptAnchored",
|
|
98
|
+
"inputs": [
|
|
99
|
+
{
|
|
100
|
+
"name": "receiptDigest",
|
|
101
|
+
"type": "bytes32",
|
|
102
|
+
"indexed": true,
|
|
103
|
+
"internalType": "bytes32"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"name": "anchoredBy",
|
|
107
|
+
"type": "address",
|
|
108
|
+
"indexed": true,
|
|
109
|
+
"internalType": "address"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"name": "intentDigest",
|
|
113
|
+
"type": "bytes32",
|
|
114
|
+
"indexed": true,
|
|
115
|
+
"internalType": "bytes32"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"name": "mandateRef",
|
|
119
|
+
"type": "bytes32",
|
|
120
|
+
"indexed": false,
|
|
121
|
+
"internalType": "bytes32"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"name": "at",
|
|
125
|
+
"type": "uint64",
|
|
126
|
+
"indexed": false,
|
|
127
|
+
"internalType": "uint64"
|
|
128
|
+
}
|
|
129
|
+
],
|
|
130
|
+
"anonymous": false
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"type": "error",
|
|
134
|
+
"name": "AlreadyAnchored",
|
|
135
|
+
"inputs": [
|
|
136
|
+
{
|
|
137
|
+
"name": "receiptDigest",
|
|
138
|
+
"type": "bytes32",
|
|
139
|
+
"internalType": "bytes32"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"name": "by",
|
|
143
|
+
"type": "address",
|
|
144
|
+
"internalType": "address"
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"type": "error",
|
|
150
|
+
"name": "ZeroDigest",
|
|
151
|
+
"inputs": []
|
|
152
|
+
}
|
|
153
|
+
]
|
package/dist/abi/index.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ export declare const IAgenticPrimitivesAgentAccount: Abi;
|
|
|
34
34
|
export declare const IApprovedHashRegistryView: Abi;
|
|
35
35
|
export declare const IAttributeReader: Abi;
|
|
36
36
|
export declare const ICaveatEnforcer: Abi;
|
|
37
|
+
export declare const ICustodyPolicyAccountModuleView: Abi;
|
|
37
38
|
export declare const ICustodyPolicyFactoryView: Abi;
|
|
38
39
|
export declare const ICustodyPolicyPauseView: Abi;
|
|
39
40
|
export declare const IDelegationManager: Abi;
|
|
@@ -54,6 +55,7 @@ export declare const PaymentEscrow: Abi;
|
|
|
54
55
|
export declare const PaymentReceiptRegistry: Abi;
|
|
55
56
|
export declare const PermissionlessSubregistry: Abi;
|
|
56
57
|
export declare const QuorumEnforcer: Abi;
|
|
58
|
+
export declare const ReceiptAnchorRegistry: Abi;
|
|
57
59
|
export declare const RelationshipTypeRegistry: Abi;
|
|
58
60
|
export declare const ShapeRegistry: Abi;
|
|
59
61
|
export declare const SignatureSlotRecovery: Abi;
|
package/dist/abi/index.js
CHANGED
|
@@ -34,6 +34,7 @@ export { default as IAgenticPrimitivesAgentAccount } from './IAgenticPrimitivesA
|
|
|
34
34
|
export { default as IApprovedHashRegistryView } from './IApprovedHashRegistryView.json' with { type: 'json' };
|
|
35
35
|
export { default as IAttributeReader } from './IAttributeReader.json' with { type: 'json' };
|
|
36
36
|
export { default as ICaveatEnforcer } from './ICaveatEnforcer.json' with { type: 'json' };
|
|
37
|
+
export { default as ICustodyPolicyAccountModuleView } from './ICustodyPolicyAccountModuleView.json' with { type: 'json' };
|
|
37
38
|
export { default as ICustodyPolicyFactoryView } from './ICustodyPolicyFactoryView.json' with { type: 'json' };
|
|
38
39
|
export { default as ICustodyPolicyPauseView } from './ICustodyPolicyPauseView.json' with { type: 'json' };
|
|
39
40
|
export { default as IDelegationManager } from './IDelegationManager.json' with { type: 'json' };
|
|
@@ -54,6 +55,7 @@ export { default as PaymentEscrow } from './PaymentEscrow.json' with { type: 'js
|
|
|
54
55
|
export { default as PaymentReceiptRegistry } from './PaymentReceiptRegistry.json' with { type: 'json' };
|
|
55
56
|
export { default as PermissionlessSubregistry } from './PermissionlessSubregistry.json' with { type: 'json' };
|
|
56
57
|
export { default as QuorumEnforcer } from './QuorumEnforcer.json' with { type: 'json' };
|
|
58
|
+
export { default as ReceiptAnchorRegistry } from './ReceiptAnchorRegistry.json' with { type: 'json' };
|
|
57
59
|
export { default as RelationshipTypeRegistry } from './RelationshipTypeRegistry.json' with { type: 'json' };
|
|
58
60
|
export { default as ShapeRegistry } from './ShapeRegistry.json' with { type: 'json' };
|
|
59
61
|
export { default as SignatureSlotRecovery } from './SignatureSlotRecovery.json' with { type: 'json' };
|
|
@@ -36,6 +36,7 @@ export interface Deployments {
|
|
|
36
36
|
readonly "valueEnforcer": Address;
|
|
37
37
|
readonly "agentRegistryBase": Address;
|
|
38
38
|
readonly "digestBindingEnforcer": Address;
|
|
39
|
+
readonly "receiptAnchorRegistry": Address;
|
|
39
40
|
readonly "chainId": number;
|
|
40
41
|
readonly "permissionlessSubregistries": string;
|
|
41
42
|
readonly "deploymentEpoch": string;
|
|
@@ -48,6 +48,7 @@ export const CONTRACTS = {
|
|
|
48
48
|
},
|
|
49
49
|
"agentRegistryBase": "0x0Be172F1b5cfD1d084CEc2cF2Af0A0cE9023Deb1",
|
|
50
50
|
"digestBindingEnforcer": "0xA3bb9BCC9b2F6F2419E1aBe5ED6Fd5399b9E68e1",
|
|
51
|
+
"receiptAnchorRegistry": "0x0e339334B4438F7546FD53837FDC8943cB3Cf54C",
|
|
51
52
|
"deploymentEpoch": "ep_80afd31afaeacbf9"
|
|
52
53
|
};
|
|
53
54
|
export default CONTRACTS;
|
package/package.json
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agenticprimitives/contracts",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.26",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Solidity contracts + ABIs + flattened sources + per-network deployment addresses for the agenticprimitives stack.",
|
|
6
|
+
"homepage": "https://agenticprimitives.dev",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/agentictrustlabs/agenticprimitives/issues"
|
|
9
|
+
},
|
|
10
|
+
"author": {
|
|
11
|
+
"name": "Agentic Trust Labs",
|
|
12
|
+
"url": "https://agenticprimitives.dev"
|
|
13
|
+
},
|
|
6
14
|
"license": "MIT",
|
|
7
15
|
"repository": {
|
|
8
16
|
"type": "git",
|
|
9
17
|
"url": "git+https://github.com/agentictrustlabs/agenticprimitives.git",
|
|
10
18
|
"directory": "packages/contracts"
|
|
11
19
|
},
|
|
12
|
-
"homepage": "https://github.com/agentictrustlabs/agenticprimitives/tree/master/packages/contracts",
|
|
13
|
-
"bugs": {
|
|
14
|
-
"url": "https://github.com/agentictrustlabs/agenticprimitives/issues"
|
|
15
|
-
},
|
|
16
20
|
"exports": {
|
|
17
21
|
"./abi": "./dist/abi/index.js",
|
|
18
22
|
"./abi/*": "./dist/abi/*.json",
|
|
@@ -48,6 +52,13 @@
|
|
|
48
52
|
"peerDependencies": {
|
|
49
53
|
"viem": "^2.52.2"
|
|
50
54
|
},
|
|
55
|
+
"keywords": [
|
|
56
|
+
"agentic-primitives",
|
|
57
|
+
"agenticprimitives",
|
|
58
|
+
"ai-agents",
|
|
59
|
+
"agent-authority",
|
|
60
|
+
"agentic-trust"
|
|
61
|
+
],
|
|
51
62
|
"scripts": {
|
|
52
63
|
"setup": "bash setup.sh",
|
|
53
64
|
"build": "forge build && pnpm run build:abi && pnpm run build:deployments",
|
package/src/AgentAccount.sol
CHANGED
|
@@ -189,12 +189,23 @@ contract AgentAccount is
|
|
|
189
189
|
/// large delay that bricks future migrations.
|
|
190
190
|
uint256 internal constant MAX_UPGRADE_TIMELOCK = 30 days;
|
|
191
191
|
|
|
192
|
+
/// @dev R917-C-2 (spec 408 §1.2) — THE CUSTODY EPOCH. Incremented by every path that RETIRES a
|
|
193
|
+
/// credential (`removeCustodian`, `removePasskey` — T6 recovery and a full rotation remove through
|
|
194
|
+
/// these). The `0x03` approved-hash sentinel keys every approval to the epoch it was made under
|
|
195
|
+
/// (`approvedHashKey`), so an approval planted by a custodian who is later removed is void the
|
|
196
|
+
/// moment the custody set no longer contains that custodian — an ECDSA signature dies with its key,
|
|
197
|
+
/// and an approved hash now dies with its custody set. Adding a credential does not bump it: an
|
|
198
|
+
/// added key cannot launder an approval a still-present key planted, and voiding a person's
|
|
199
|
+
/// standing wires every time she enrols a device would be the wrong invariant. Takes one slot
|
|
200
|
+
/// from the gap (49 remain).
|
|
201
|
+
uint64 private _custodyEpoch;
|
|
202
|
+
|
|
192
203
|
/// @dev Spec 007 Phase A.5 (SC7 § 3.1) — storage gap reserves 50
|
|
193
204
|
/// slots after the last linear-layout state variable. Future
|
|
194
205
|
/// additive upgrades shrink the gap by however many slots they
|
|
195
206
|
/// add. ERC-7201 namespaced state (passkeys, modules) lives at
|
|
196
207
|
/// keccak-computed slots and is NOT in this linear layout.
|
|
197
|
-
uint256[
|
|
208
|
+
uint256[49] private __gap;
|
|
198
209
|
|
|
199
210
|
// ─── Errors ─────────────────────────────────────────────────────
|
|
200
211
|
|
|
@@ -943,6 +954,13 @@ contract AgentAccount is
|
|
|
943
954
|
revert ModuleNotInstalled(moduleTypeId, module);
|
|
944
955
|
}
|
|
945
956
|
|
|
957
|
+
// R917-C-1: effects BEFORE the interaction. The module's `onUninstall` may verify that this account
|
|
958
|
+
// no longer lists it (CustodyPolicy does — that is how it tells `uninstallModule` apart from a
|
|
959
|
+
// custodian's `execute(custodyPolicy, onUninstall)`), so the flag is cleared first. A revert below
|
|
960
|
+
// still rolls the whole call back.
|
|
961
|
+
$.installed[moduleTypeId][module] = false;
|
|
962
|
+
_removeFromList($.installedList[moduleTypeId], module);
|
|
963
|
+
|
|
946
964
|
// Loud uninstall — if the module reverts in onUninstall, we revert
|
|
947
965
|
// too so the caller sees the failure. (Owner can force-uninstall
|
|
948
966
|
// by passing deInitData the module can handle, or by re-deploying
|
|
@@ -953,9 +971,6 @@ contract AgentAccount is
|
|
|
953
971
|
revert ModuleOnUninstallFailed(reason);
|
|
954
972
|
}
|
|
955
973
|
|
|
956
|
-
$.installed[moduleTypeId][module] = false;
|
|
957
|
-
_removeFromList($.installedList[moduleTypeId], module);
|
|
958
|
-
|
|
959
974
|
emit ModuleUninstalled(moduleTypeId, module);
|
|
960
975
|
}
|
|
961
976
|
|
|
@@ -1297,14 +1312,32 @@ contract AgentAccount is
|
|
|
1297
1312
|
/// `DM → execute(self, executeBatch(...))` nest, so a nested governance
|
|
1298
1313
|
/// self-call is caught regardless of mode.
|
|
1299
1314
|
function _rejectDelegatedGovernance(address target, bytes calldata data) internal view {
|
|
1315
|
+
if (!_delegationExecCtx) return;
|
|
1300
1316
|
if (
|
|
1301
|
-
_delegationExecCtx &&
|
|
1302
1317
|
target == address(this) &&
|
|
1303
1318
|
data.length >= 4 &&
|
|
1304
1319
|
_isGovernanceSelector(bytes4(data[0:4]))
|
|
1305
1320
|
) {
|
|
1306
1321
|
revert DelegatedGovernanceForbidden();
|
|
1307
1322
|
}
|
|
1323
|
+
// R917-C-1/C-2 (spec 408 §1.1): a delegation stack may not speak AS THE ACCOUNT to the account's own
|
|
1324
|
+
// custody-tier contracts. `CustodyPolicy` and `ApprovedHashRegistry` treat `msg.sender == account` as
|
|
1325
|
+
// the principal — a delegate reaching them through `execute` would uninstall the custody policy
|
|
1326
|
+
// (bricking the account) or plant approved-hash root delegations that outlive the delegate's own
|
|
1327
|
+
// caveats. Any selector: there is no legitimate delegated call to either.
|
|
1328
|
+
if (_isAccountTrustedTarget(target)) revert DelegatedGovernanceForbidden();
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/// @dev The contracts that take THIS ACCOUNT as principal when it is `msg.sender` — reachable only from
|
|
1332
|
+
/// the account's own governance paths (`uninstallModule`, a custodian-signed userOp), never from a
|
|
1333
|
+
/// delegation. The custody policy is read from the factory (the canonical one; a legacy direct
|
|
1334
|
+
/// deploy with no factory has none); the registry is the immutable this account was built with.
|
|
1335
|
+
function _isAccountTrustedTarget(address target) internal view returns (bool) {
|
|
1336
|
+
if (target == _approvedHashRegistry && target != address(0)) return true;
|
|
1337
|
+
address f = _factory;
|
|
1338
|
+
if (f == address(0)) return false;
|
|
1339
|
+
address cp = IAgentAccountFactoryView(f).custodyPolicy();
|
|
1340
|
+
return cp != address(0) && target == cp;
|
|
1308
1341
|
}
|
|
1309
1342
|
|
|
1310
1343
|
/// @dev The account-governance selector set — mutators that change custody,
|
|
@@ -1349,9 +1382,11 @@ contract AgentAccount is
|
|
|
1349
1382
|
// branch is unreachable from `_validateSignature`/`_validateSig`
|
|
1350
1383
|
// (the userOp-auth path), where 0x03 is an unknown type → false.
|
|
1351
1384
|
// Fails CLOSED if the registry is unset/not-a-contract (ADR-0013).
|
|
1385
|
+
// R917-C-2: the registry is consulted for the EPOCH-BOUND key, never the raw hash — an approval
|
|
1386
|
+
// made under a custody set that has since retired a credential is not this account's approval.
|
|
1352
1387
|
if (signature.length == 1 && uint8(signature[0]) == SIG_TYPE_APPROVED_HASH) {
|
|
1353
1388
|
if (_approvedHashRegistry.code.length == 0) return bytes4(0xffffffff);
|
|
1354
|
-
return IApprovedHashRegistryView(_approvedHashRegistry).isApproved(address(this), hash)
|
|
1389
|
+
return IApprovedHashRegistryView(_approvedHashRegistry).isApproved(address(this), approvedHashKey(hash))
|
|
1355
1390
|
? ERC1271_MAGIC_VALUE
|
|
1356
1391
|
: bytes4(0xffffffff);
|
|
1357
1392
|
}
|
|
@@ -1611,6 +1646,7 @@ contract AgentAccount is
|
|
|
1611
1646
|
_externalCustodians[owner] = false;
|
|
1612
1647
|
_externalCustodianCount--;
|
|
1613
1648
|
emit CustodianRemoved(owner);
|
|
1649
|
+
_bumpCustodyEpoch();
|
|
1614
1650
|
}
|
|
1615
1651
|
|
|
1616
1652
|
// ─── Passkey (WebAuthn P-256) management ──────────────────────
|
|
@@ -1659,6 +1695,8 @@ contract AgentAccount is
|
|
|
1659
1695
|
/// (ADR-0012/0013; INV-302-3).
|
|
1660
1696
|
event PasskeyAdded(bytes32 indexed credentialIdDigest);
|
|
1661
1697
|
event PasskeyRemoved(bytes32 indexed credentialIdDigest);
|
|
1698
|
+
/// @dev R917-C-2 — a credential was retired; every approved hash keyed to the previous epoch is void.
|
|
1699
|
+
event CustodyEpochBumped(uint64 epoch);
|
|
1662
1700
|
|
|
1663
1701
|
/// @notice Register a new WebAuthn credential bound to a specific RP.
|
|
1664
1702
|
/// `onlySelf` — callable via a UserOp signed by any existing signer
|
|
@@ -1715,6 +1753,7 @@ contract AgentAccount is
|
|
|
1715
1753
|
$.count -= 1;
|
|
1716
1754
|
emit PasskeyRemoved(credentialIdDigest);
|
|
1717
1755
|
emit CustodianRemoved(pia);
|
|
1756
|
+
_bumpCustodyEpoch();
|
|
1718
1757
|
}
|
|
1719
1758
|
|
|
1720
1759
|
/// @notice Whether a passkey is registered on this account.
|
|
@@ -1741,6 +1780,28 @@ contract AgentAccount is
|
|
|
1741
1780
|
return _passkeyStorage().count;
|
|
1742
1781
|
}
|
|
1743
1782
|
|
|
1783
|
+
// ─── Custody epoch (R917-C-2, spec 408 §1.2) ─────────────────────
|
|
1784
|
+
|
|
1785
|
+
/// @notice The current custody epoch — bumped whenever a credential is retired.
|
|
1786
|
+
function custodyEpoch() external view returns (uint64) {
|
|
1787
|
+
return _custodyEpoch;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
/// @notice THE KEY an SDK approves in the `ApprovedHashRegistry` for `hash` to validate through this
|
|
1791
|
+
/// account's `0x03` sentinel NOW: `keccak256(abi.encode(hash, custodyEpoch))`. A counterfactual
|
|
1792
|
+
/// account (not yet deployed) is at epoch 0, so its key is computable before deploy — which is
|
|
1793
|
+
/// what spec 253's batched deploy-and-approve relies on.
|
|
1794
|
+
function approvedHashKey(bytes32 hash) public view returns (bytes32) {
|
|
1795
|
+
return keccak256(abi.encode(hash, _custodyEpoch));
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
/// @dev Retire-a-credential paths call this; see `_custodyEpoch`.
|
|
1799
|
+
function _bumpCustodyEpoch() internal {
|
|
1800
|
+
uint64 next = _custodyEpoch + 1;
|
|
1801
|
+
_custodyEpoch = next;
|
|
1802
|
+
emit CustodyEpochBumped(next);
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1744
1805
|
// ─── Custody policy + scheduled changes ───────────────────────────
|
|
1745
1806
|
//
|
|
1746
1807
|
// The schedule / apply / cancel custody-change surface, CustodyAction
|
|
@@ -55,6 +55,14 @@ contract AgentAccountFactory is GovernanceManaged {
|
|
|
55
55
|
/// sentinel consults it; it is NOT a mutable factory-view.
|
|
56
56
|
address public immutable approvedHashRegistry;
|
|
57
57
|
|
|
58
|
+
/// @notice R917-C-5 (spec 408 §1.3) — whether the T4 (admin) safety-delay floor is LIFTED to zero for
|
|
59
|
+
/// accounts this factory deploys. A DEPLOYMENT decision, stated by the deployer in the
|
|
60
|
+
/// constructor and recorded in the deployments JSON, never a `block.chainid` list in bytecode:
|
|
61
|
+
/// the previous carve-out named the estate's own chain (34348) and silently switched the
|
|
62
|
+
/// EXT-TL-FLOOR-1 invariant off where it mattered. `check:production-deploy` refuses a
|
|
63
|
+
/// production/testnet record with this `true`. T5 (24 h) / T6 (48 h) are never relaxed.
|
|
64
|
+
bool public immutable relaxedT4Floor;
|
|
65
|
+
|
|
58
66
|
address public bundlerSigner;
|
|
59
67
|
address public sessionIssuer;
|
|
60
68
|
|
|
@@ -127,11 +135,13 @@ contract AgentAccountFactory is GovernanceManaged {
|
|
|
127
135
|
address bundlerSigner_,
|
|
128
136
|
address sessionIssuer_,
|
|
129
137
|
address governance_,
|
|
130
|
-
address approvedHashRegistry_
|
|
138
|
+
address approvedHashRegistry_,
|
|
139
|
+
bool relaxedT4Floor_
|
|
131
140
|
) GovernanceManaged(governance_) {
|
|
132
141
|
if (custodyPolicy_ == address(0)) revert ZeroAddress();
|
|
133
142
|
accountImplementation = new AgentAccount(entryPoint_, approvedHashRegistry_);
|
|
134
143
|
approvedHashRegistry = approvedHashRegistry_;
|
|
144
|
+
relaxedT4Floor = relaxedT4Floor_;
|
|
135
145
|
delegationManager = delegationManager_;
|
|
136
146
|
custodyPolicy = custodyPolicy_;
|
|
137
147
|
bundlerSigner = bundlerSigner_;
|
|
@@ -357,15 +367,12 @@ contract AgentAccountFactory is GovernanceManaged {
|
|
|
357
367
|
if (thresholds[5] > sodCap) thresholds[5] = sodCap;
|
|
358
368
|
}
|
|
359
369
|
|
|
360
|
-
//
|
|
361
|
-
// so a demo can drive an admin custody action's
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
|
|
366
|
-
// CustodyPolicy requires a non-zero timelock for those tiers, and the demo only exercises T4.
|
|
367
|
-
bool devChain = block.chainid == 31337 || block.chainid == 1337 || block.chainid == 34348;
|
|
368
|
-
uint32 floor4 = devChain ? 0 : uint32(1 hours);
|
|
370
|
+
// R917-C-5 (spec 408 §1.3): the T4 floor is lifted ONLY when the deployer said so in the constructor
|
|
371
|
+
// (`relaxedT4Floor`) — a local development posture so a demo can drive an admin custody action's
|
|
372
|
+
// schedule→apply back-to-back. It is not inferred from `block.chainid`: the estate's own chain was
|
|
373
|
+
// once on that list, and the audited EXT-TL-FLOOR-1 invariant was off where it mattered. T5 (24h)
|
|
374
|
+
// and T6 (recovery, 48h) are NOT relaxed under any posture.
|
|
375
|
+
uint32 floor4 = relaxedT4Floor ? 0 : uint32(1 hours);
|
|
369
376
|
|
|
370
377
|
uint32[7] memory timelocks;
|
|
371
378
|
timelocks[4] = timelockOverrides[4] == 0 ? floor4 : timelockOverrides[4];
|
|
@@ -26,13 +26,26 @@ pragma solidity ^0.8.28;
|
|
|
26
26
|
* Pairs with `MultiSendCallOnly` (atomic batch) so a passkey
|
|
27
27
|
* smart account can `approveHash` + perform the delegated action
|
|
28
28
|
* in one userOp.
|
|
29
|
+
*
|
|
30
|
+
* R917-C-2 (spec 408 §1.2): an `AgentAccount`'s `0x03` sentinel does NOT approve the raw hash —
|
|
31
|
+
* it approves `AgentAccount.approvedHashKey(hash)` = `keccak256(abi.encode(hash, custodyEpoch))`,
|
|
32
|
+
* so an approval planted by a custodian who is later removed is void once the account's custody
|
|
33
|
+
* epoch moves. This registry is agnostic to that: it stores whatever 32 bytes the signer approves.
|
|
29
34
|
*/
|
|
30
35
|
contract ApprovedHashRegistry {
|
|
31
|
-
/// signer
|
|
32
|
-
|
|
36
|
+
/// @dev R917-C-2 (spec 408 §1.2): an approval is recorded with the signer's epoch at the time
|
|
37
|
+
/// (`epoch + 1`, so a set slot is never zero); `revokeAll()` bumps the epoch and voids every
|
|
38
|
+
/// approval the signer ever made in one call. `isApproved` is the ONE read: the `0x03` sentinel
|
|
39
|
+
/// and `QuorumEnforcer`'s `v == 1` slot both consult it.
|
|
40
|
+
/// signer => hash => (epoch at approval + 1), 0 = never approved
|
|
41
|
+
mapping(address => mapping(bytes32 => uint64)) private _approvedAt;
|
|
42
|
+
/// signer => current epoch
|
|
43
|
+
mapping(address => uint64) private _epoch;
|
|
33
44
|
|
|
34
45
|
event HashApproved(address indexed signer, bytes32 indexed hash);
|
|
35
46
|
event HashRevoked(address indexed signer, bytes32 indexed hash);
|
|
47
|
+
/// @dev R917-C-2 — every approval `signer` made before this event is void.
|
|
48
|
+
event AllHashesRevoked(address indexed signer, uint64 epoch);
|
|
36
49
|
|
|
37
50
|
/**
|
|
38
51
|
* @notice Pre-approve a hash. `msg.sender` is the signer; only
|
|
@@ -40,18 +53,40 @@ contract ApprovedHashRegistry {
|
|
|
40
53
|
* for this hash.
|
|
41
54
|
*/
|
|
42
55
|
function approveHash(bytes32 hash) external {
|
|
43
|
-
|
|
56
|
+
_approvedAt[msg.sender][hash] = _epoch[msg.sender] + 1;
|
|
44
57
|
emit HashApproved(msg.sender, hash);
|
|
45
58
|
}
|
|
46
59
|
|
|
47
60
|
/// @notice Revoke a previously-approved hash before redemption.
|
|
48
61
|
function revokeHash(bytes32 hash) external {
|
|
49
|
-
|
|
62
|
+
_approvedAt[msg.sender][hash] = 0;
|
|
50
63
|
emit HashRevoked(msg.sender, hash);
|
|
51
64
|
}
|
|
52
65
|
|
|
53
|
-
/// @notice
|
|
66
|
+
/// @notice R917-C-2 — revoke EVERY hash `msg.sender` ever approved, in one call. The per-hash
|
|
67
|
+
/// approvals are unenumerable on chain; this is how a signer that suspects a planted approval
|
|
68
|
+
/// voids all of them without knowing which.
|
|
69
|
+
function revokeAll() external {
|
|
70
|
+
uint64 next = _epoch[msg.sender] + 1;
|
|
71
|
+
_epoch[msg.sender] = next;
|
|
72
|
+
emit AllHashesRevoked(msg.sender, next);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// @notice The signer's current epoch (informational; `isApproved` already accounts for it).
|
|
76
|
+
function epochOf(address signer) external view returns (uint64) {
|
|
77
|
+
return _epoch[signer];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// @notice Whether `signer` approved `hash` under its CURRENT epoch. External view used by the
|
|
81
|
+
/// `0x03` sentinel and by `QuorumEnforcer`'s `v == 1` path.
|
|
54
82
|
function isApproved(address signer, bytes32 hash) external view returns (bool) {
|
|
55
|
-
|
|
83
|
+
uint64 at = _approvedAt[signer][hash];
|
|
84
|
+
return at != 0 && at == _epoch[signer] + 1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// @notice Back-compat read shape of the former public mapping: true iff currently approved.
|
|
88
|
+
function approved(address signer, bytes32 hash) external view returns (bool) {
|
|
89
|
+
uint64 at = _approvedAt[signer][hash];
|
|
90
|
+
return at != 0 && at == _epoch[signer] + 1;
|
|
56
91
|
}
|
|
57
92
|
}
|
|
@@ -20,6 +20,13 @@ interface ICustodyPolicyPauseView {
|
|
|
20
20
|
function isPaused() external view returns (bool);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/// @dev R917-C-1 — the account's ERC-7579 module view. `onUninstall` refuses while the account still lists
|
|
24
|
+
/// this module as installed, so the only path to it is `AgentAccount.uninstallModule` (onlySelf +
|
|
25
|
+
/// custody-quorum), which clears the flag BEFORE calling out.
|
|
26
|
+
interface ICustodyPolicyAccountModuleView {
|
|
27
|
+
function isModuleInstalled(uint256 moduleTypeId, address module, bytes calldata additionalContext) external view returns (bool);
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
/**
|
|
24
31
|
* @title CustodyPolicy
|
|
25
32
|
* @notice ERC-7579 module that owns the schedule / apply / cancel
|
|
@@ -346,6 +353,11 @@ contract CustodyPolicy {
|
|
|
346
353
|
/// @dev Audit C-11: reinstall over a previously-uninstalled config
|
|
347
354
|
/// is forbidden — stale mappings would compose unpredictably.
|
|
348
355
|
error ReinstallForbidden(address account);
|
|
356
|
+
/// @dev R917-C-1: `onUninstall` was called while the account still has this module installed — i.e. by
|
|
357
|
+
/// `execute(custodyPolicy, onUninstall)` under a single custodian or a delegation, not by
|
|
358
|
+
/// `uninstallModule`. Accepting it would leave the account governed by a policy that refuses every
|
|
359
|
+
/// change (installed on the account, uninstalled here, reinstall forbidden) — bricked forever.
|
|
360
|
+
error StillInstalledOn(address account);
|
|
349
361
|
|
|
350
362
|
// ─── ERC-7579 lifecycle ───────────────────────────────────────────
|
|
351
363
|
|
|
@@ -452,6 +464,14 @@ contract CustodyPolicy {
|
|
|
452
464
|
address account = msg.sender;
|
|
453
465
|
Config storage c = _configs[account];
|
|
454
466
|
if (!c.installed) revert NotInstalledOn(account);
|
|
467
|
+
// R917-C-1: the account must have ALREADY dropped this module. `AgentAccount.uninstallModule` (onlySelf
|
|
468
|
+
// + `_requireCustodyGoverned`) clears its installed flag before this call; any other caller that is the
|
|
469
|
+
// account — a 1-of-N custodian userOp `execute(custodyPolicy, onUninstall)`, a delegation redeemed
|
|
470
|
+
// through the DM — still lists the module and is refused here. Fail closed on the read as well: a
|
|
471
|
+
// caller that cannot answer the view is not an account that uninstalled us.
|
|
472
|
+
if (ICustodyPolicyAccountModuleView(account).isModuleInstalled(MODULE_TYPE_EXECUTOR, address(this), "")) {
|
|
473
|
+
revert StillInstalledOn(account);
|
|
474
|
+
}
|
|
455
475
|
c.installed = false;
|
|
456
476
|
// Audit C-11: lock against ever re-installing on this account.
|
|
457
477
|
// The mapping state (trustees, thresholds, proposals) is left
|
|
@@ -10,14 +10,15 @@ import "../agency/ICaveatEnforcer.sol";
|
|
|
10
10
|
* §2): authority *for a reason*, not authority for any purpose until revoked.
|
|
11
11
|
*
|
|
12
12
|
* @dev terms = abi.encode(bytes32 kind, bytes32 digest)
|
|
13
|
-
* args = abi.encode(bytes32 presented)
|
|
13
|
+
* args = abi.encode(bytes32 presented, bytes32 stepNonce) — supplied by the redeemer at redemption
|
|
14
14
|
*
|
|
15
15
|
* `kind` names WHICH artifact the digest is of, so an off-chain reader can tell an intent binding
|
|
16
16
|
* from a projection binding without guessing:
|
|
17
17
|
* keccak256("ap:intent") — the canonical intent (RFC 8785 JCS bytes)
|
|
18
18
|
* keccak256("ap:projection") — what was disclosed to the grantee (spec 336 §4)
|
|
19
19
|
* keccak256("ap:offer") — the accepted offer (spec 336 §5)
|
|
20
|
-
*
|
|
20
|
+
* keccak256("ap:plan") — the admitted plan (spec 408 §2.3)
|
|
21
|
+
* One enforcer serves all four: the mechanics are identical and the kind belongs in the terms,
|
|
21
22
|
* where it is signed, rather than in which contract address was chosen.
|
|
22
23
|
*
|
|
23
24
|
* WHAT THIS ENFORCES, HONESTLY. The chain cannot know what an intent *means*; it can only hold
|
|
@@ -30,33 +31,67 @@ import "../agency/ICaveatEnforcer.sol";
|
|
|
30
31
|
* 2. the presented digest lands in the transaction, so the receipt's `intentDigest` and the
|
|
31
32
|
* chain's record of what the redeemer claimed to be doing are the same bytes — evidence a
|
|
32
33
|
* third party can check without trusting either side.
|
|
33
|
-
* A redeemer that copies `terms` into `args` satisfies the check; that is the point — it has
|
|
34
|
+
* A redeemer that copies `terms` into `args` satisfies the digest check; that is the point — it has
|
|
34
35
|
* now committed, on chain, to that intent, and a step whose real intent differs is caught by the
|
|
35
36
|
* verifier that computed the digest from the step rather than from the caveat.
|
|
36
37
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
38
|
+
* R917-C-4 (spec 408 §1.4) — SINGLE-USE PER STEP. A digest-bound redemption is consumed under
|
|
39
|
+
* `(delegator, delegationHash, kind, stepNonce)`: the harness derives `stepNonce` from the intent
|
|
40
|
+
* digest and the step's reference (the same derivation as `PaymentEnforcer`'s nonce), so ONE step
|
|
41
|
+
* redeems once and a replay of it reverts `StepNonceReused`. Which steps exist is bound off chain by
|
|
42
|
+
* the plan digest the mandate carries; the chain holds each of them to one redemption. The hooks
|
|
43
|
+
* therefore mutate state and are `onlyDelegationManager` (EXT-PMT-1's rule for stateful enforcers):
|
|
44
|
+
* a stranger cannot burn a step's nonce ahead of the legitimate redemption.
|
|
39
45
|
*/
|
|
40
46
|
contract DigestBindingEnforcer is ICaveatEnforcer {
|
|
41
47
|
error BadTermsLength();
|
|
42
48
|
error BadArgsLength();
|
|
43
49
|
error DigestMismatch(bytes32 kind, bytes32 bound, bytes32 presented);
|
|
50
|
+
error StepNonceReused(bytes32 kind, bytes32 stepNonce);
|
|
51
|
+
error NotDelegationManager(address caller);
|
|
52
|
+
|
|
53
|
+
/// @notice The ONLY authorized caller of `beforeHook` — it consumes a nonce (R917-C-4).
|
|
54
|
+
address public immutable delegationManager;
|
|
55
|
+
|
|
56
|
+
/// delegator => delegationHash => keccak256(kind, stepNonce) => consumed
|
|
57
|
+
mapping(address => mapping(bytes32 => mapping(bytes32 => bool))) private _used;
|
|
58
|
+
|
|
59
|
+
event DigestBindingRedeemed(address indexed delegator, bytes32 indexed delegationHash, bytes32 indexed kind, bytes32 presented, bytes32 stepNonce);
|
|
60
|
+
|
|
61
|
+
constructor(address delegationManager_) {
|
|
62
|
+
require(delegationManager_ != address(0), "DigestBindingEnforcer: zero DelegationManager");
|
|
63
|
+
delegationManager = delegationManager_;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
modifier onlyDelegationManager() {
|
|
67
|
+
if (msg.sender != delegationManager) revert NotDelegationManager(msg.sender);
|
|
68
|
+
_;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// @notice Whether `stepNonce` was already consumed for this binding (a reader's replay check).
|
|
72
|
+
function isUsed(address delegator, bytes32 delegationHash, bytes32 kind, bytes32 stepNonce) external view returns (bool) {
|
|
73
|
+
return _used[delegator][delegationHash][keccak256(abi.encode(kind, stepNonce))];
|
|
74
|
+
}
|
|
44
75
|
|
|
45
76
|
function beforeHook(
|
|
46
77
|
bytes calldata terms,
|
|
47
78
|
bytes calldata args,
|
|
48
|
-
bytes32,
|
|
49
|
-
address,
|
|
79
|
+
bytes32 delegationHash,
|
|
80
|
+
address delegator,
|
|
50
81
|
address,
|
|
51
82
|
address,
|
|
52
83
|
uint256,
|
|
53
84
|
bytes calldata
|
|
54
|
-
) external
|
|
85
|
+
) external override onlyDelegationManager {
|
|
55
86
|
if (terms.length != 64) revert BadTermsLength();
|
|
56
|
-
if (args.length !=
|
|
87
|
+
if (args.length != 64) revert BadArgsLength();
|
|
57
88
|
(bytes32 kind, bytes32 bound) = abi.decode(terms, (bytes32, bytes32));
|
|
58
|
-
bytes32 presented = abi.decode(args, (bytes32));
|
|
89
|
+
(bytes32 presented, bytes32 stepNonce) = abi.decode(args, (bytes32, bytes32));
|
|
59
90
|
if (presented != bound) revert DigestMismatch(kind, bound, presented);
|
|
91
|
+
bytes32 slot = keccak256(abi.encode(kind, stepNonce));
|
|
92
|
+
if (_used[delegator][delegationHash][slot]) revert StepNonceReused(kind, stepNonce);
|
|
93
|
+
_used[delegator][delegationHash][slot] = true;
|
|
94
|
+
emit DigestBindingRedeemed(delegator, delegationHash, kind, presented, stepNonce);
|
|
60
95
|
}
|
|
61
96
|
|
|
62
97
|
function afterHook(
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @title ReceiptAnchorRegistry
|
|
6
|
+
* @notice The on-chain ANCHOR for a harness receipt (spec 351 P0.6, spec 406 W2): a run's record digest, bound
|
|
7
|
+
* to the intent digest the mandate was over and the mandate's reference, anchored once by the acting
|
|
8
|
+
* runtime's agent. A counterparty who holds the record recomputes its digest and reads the anchor here —
|
|
9
|
+
* no trust in the runtime's logs, no vendor, no vault: the chain says a runtime with THIS agent's key
|
|
10
|
+
* committed to THESE bytes at THIS block, and the intent digest says which ask it was under.
|
|
11
|
+
*
|
|
12
|
+
* @dev DIGESTS AND ADDRESSES ONLY — nothing of what the run was about reaches the chain (the S1 firewall of
|
|
13
|
+
* spec 395 holds by construction). Per-row storage, so a reader uses `readContract` (ADR-0012: no log
|
|
14
|
+
* walks in product paths); the event is for indexers. Anchoring is permissionless: WHO anchored is the
|
|
15
|
+
* evidence (`anchoredBy`), never a gate — a stranger anchoring random digests proves nothing about a
|
|
16
|
+
* run whose receipt names a different agent. A digest anchors ONCE PER ANCHORER (R917-C-3, spec 408
|
|
17
|
+
* §1.5): rows are keyed by `(anchoredBy, receiptDigest)`, so a front-runner on a public mempool cannot
|
|
18
|
+
* squat the digest ahead of the agent, and an anchor cannot be re-attributed after the fact.
|
|
19
|
+
*/
|
|
20
|
+
contract ReceiptAnchorRegistry {
|
|
21
|
+
struct Anchor {
|
|
22
|
+
address anchoredBy;
|
|
23
|
+
uint64 at;
|
|
24
|
+
bytes32 intentDigest;
|
|
25
|
+
bytes32 mandateRef;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// anchoredBy => receiptDigest => anchor (R917-C-3: keyed by WHO anchored, so a stranger's anchor of
|
|
29
|
+
/// the same digest is a different row — it can neither pre-empt the agent's nor be mistaken for it).
|
|
30
|
+
mapping(address => mapping(bytes32 => Anchor)) private _anchors;
|
|
31
|
+
|
|
32
|
+
event ReceiptAnchored(bytes32 indexed receiptDigest, address indexed anchoredBy, bytes32 indexed intentDigest, bytes32 mandateRef, uint64 at);
|
|
33
|
+
|
|
34
|
+
error ZeroDigest();
|
|
35
|
+
error AlreadyAnchored(bytes32 receiptDigest, address by);
|
|
36
|
+
|
|
37
|
+
/// @notice Anchor a receipt digest, once per anchorer. `intentDigest` and `mandateRef` may be zero for a run under no mandate (a read).
|
|
38
|
+
function anchor(bytes32 receiptDigest, bytes32 intentDigest, bytes32 mandateRef) external {
|
|
39
|
+
if (receiptDigest == bytes32(0)) revert ZeroDigest();
|
|
40
|
+
Anchor storage a = _anchors[msg.sender][receiptDigest];
|
|
41
|
+
if (a.anchoredBy != address(0)) revert AlreadyAnchored(receiptDigest, a.anchoredBy);
|
|
42
|
+
_anchors[msg.sender][receiptDigest] = Anchor({ anchoredBy: msg.sender, at: uint64(block.timestamp), intentDigest: intentDigest, mandateRef: mandateRef });
|
|
43
|
+
emit ReceiptAnchored(receiptDigest, msg.sender, intentDigest, mandateRef, uint64(block.timestamp));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// @notice The anchor `anchoredBy` made of a digest, or a zero struct when none — the reader names the
|
|
47
|
+
/// agent from the receipt it holds, and decides what absence means.
|
|
48
|
+
function anchorOf(address anchoredBy, bytes32 receiptDigest) external view returns (Anchor memory) {
|
|
49
|
+
return _anchors[anchoredBy][receiptDigest];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isAnchored(address anchoredBy, bytes32 receiptDigest) external view returns (bool) {
|
|
53
|
+
return _anchors[anchoredBy][receiptDigest].anchoredBy != address(0);
|
|
54
|
+
}
|
|
55
|
+
}
|