@agenticprimitives/contracts 0.1.0-alpha.2 → 1.0.0-alpha.10
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 +324 -47
- package/README.md +45 -10
- package/deployments-base-sepolia.json +36 -1
- package/deployments-registries-base-sepolia.json +1 -0
- package/dist/abi/AgentAccount.json +1670 -0
- package/dist/abi/AgentAccountFactory.json +477 -0
- package/dist/abi/AgentNameRegistry.json +51 -0
- package/dist/abi/AgenticGovernance.json +32 -0
- package/dist/abi/AgreementRegistry.json +427 -0
- package/dist/abi/AttestationRegistry.json +436 -0
- package/dist/abi/CallDataHashEnforcer.json +119 -0
- package/dist/abi/CustodyPolicy.json +1122 -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/IPaymentReceiptRegistry.json +50 -0
- package/dist/abi/MockUSDC.json +621 -0
- package/dist/abi/PaymentEnforcer.json +407 -0
- package/dist/abi/PaymentEscrow.json +299 -0
- package/dist/abi/PaymentReceiptRegistry.json +182 -0
- package/dist/abi/PermissionlessSubregistry.json +5 -0
- package/dist/abi/QuorumEnforcer.json +311 -0
- package/dist/abi/SignatureSlotRecovery.json +88 -0
- package/dist/abi/SkillDefinitionRegistry.json +491 -0
- package/dist/abi/SmartAgentPaymaster.json +873 -0
- package/dist/abi/UniversalSignatureValidator.json +65 -0
- package/dist/abi/index.d.ts +28 -0
- package/dist/abi/index.js +28 -0
- package/dist/deployments/base-sepolia.d.ts +41 -0
- package/dist/deployments/base-sepolia.js +39 -0
- package/dist/deployments/index.d.ts +9 -0
- package/dist/deployments/index.js +16 -0
- package/dist/deployments/registries-base-sepolia.d.ts +9 -0
- package/dist/deployments/registries-base-sepolia.js +7 -0
- package/package.json +28 -7
- package/src/AgentAccount.sol +191 -11
- package/src/AgentAccountFactory.sol +66 -12
- package/src/SmartAgentPaymaster.sol +186 -20
- package/src/UniversalSignatureValidator.sol +3 -0
- package/src/agency/DelegationManager.sol +170 -0
- package/src/agency/IDelegationManager.sol +38 -0
- package/src/agreement/AgreementRegistry.sol +331 -0
- package/src/attestation/AttestationRegistry.sol +384 -0
- package/src/custody/CustodyPolicy.sol +138 -8
- package/src/enforcers/AllowedMethodsEnforcer.sol +6 -0
- package/src/enforcers/AllowedTargetsEnforcer.sol +6 -0
- package/src/enforcers/CallDataHashEnforcer.sol +61 -0
- package/src/enforcers/PaymentEnforcer.sol +194 -0
- package/src/enforcers/QuorumEnforcer.sol +20 -0
- package/src/enforcers/TimestampEnforcer.sol +7 -0
- package/src/enforcers/ValueEnforcer.sol +6 -0
- package/src/geo/GeoFeatureRegistry.sol +197 -0
- package/src/governance/AgenticGovernance.sol +26 -4
- package/src/libraries/SignatureSlotRecovery.sol +8 -3
- package/src/libraries/WebAuthnLib.sol +13 -0
- package/src/mocks/MockUSDC.sol +152 -0
- package/src/naming/AgentNameRegistry.sol +52 -15
- package/src/naming/AgentNameUniversalResolver.sol +1 -1
- package/src/naming/PermissionlessSubregistry.sol +19 -2
- package/src/payments/PaymentEscrow.sol +137 -0
- package/src/payments/PaymentReceiptRegistry.sol +67 -0
- package/src/relationships/AgentRelationship.sol +8 -0
- package/src/skills/SkillDefinitionRegistry.sol +187 -0
- package/deployments-anvil.json +0 -1
package/src/AgentAccount.sol
CHANGED
|
@@ -22,6 +22,33 @@ import "./libraries/WebAuthnLib.sol";
|
|
|
22
22
|
interface IAgentAccountFactoryView {
|
|
23
23
|
function bundlerSigner() external view returns (address);
|
|
24
24
|
function sessionIssuer() external view returns (address);
|
|
25
|
+
/// @dev R6.5 — the factory inherits `GovernanceManaged`; its
|
|
26
|
+
/// `governance()` getter returns the AgenticGovernance pointer
|
|
27
|
+
/// shared across every account it creates. AgentAccount reads
|
|
28
|
+
/// this address to call `IGovernanceView.isPaused()` from
|
|
29
|
+
/// `whenNotPaused`-guarded entrypoints.
|
|
30
|
+
function governance() external view returns (address);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// @dev R6.5 — minimal interface for the pause read. Mirrors the same
|
|
34
|
+
/// selector `GovernanceManaged._pausedSafe()` uses so an EOA or
|
|
35
|
+
/// non-conforming governance contract is treated as "not paused"
|
|
36
|
+
/// (legacy / test deploys still work). Production deploys ALWAYS
|
|
37
|
+
/// use `AgenticGovernance` which implements this interface.
|
|
38
|
+
interface IAgentAccountPauseView {
|
|
39
|
+
function isPaused() external view returns (bool);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// @dev Spec 253 — minimal read surface of `ApprovedHashRegistry`. The
|
|
43
|
+
/// account consults it ONLY from `isValidSignature` (the ERC-1271
|
|
44
|
+
/// entrypoint), under the `0x03` sentinel, so a hash the account
|
|
45
|
+
/// itself pre-approved (inside its own custody-gated execute/batch)
|
|
46
|
+
/// validates as an ERC-1271 signature. Address is bound IMMUTABLE in
|
|
47
|
+
/// the impl (see constructor) — never an owner-mutable view — so a
|
|
48
|
+
/// factory/governance key compromise cannot re-point it at an
|
|
49
|
+
/// attacker registry to forge signatures.
|
|
50
|
+
interface IApprovedHashRegistryView {
|
|
51
|
+
function isApproved(address signer, bytes32 hash) external view returns (bool);
|
|
25
52
|
}
|
|
26
53
|
|
|
27
54
|
/**
|
|
@@ -60,6 +87,14 @@ contract AgentAccount is
|
|
|
60
87
|
/// @dev The ERC-4337 EntryPoint contract
|
|
61
88
|
IEntryPoint private immutable _entryPoint;
|
|
62
89
|
|
|
90
|
+
/// @dev Spec 253 — the `ApprovedHashRegistry` consulted by the `0x03`
|
|
91
|
+
/// sentinel branch of `isValidSignature`. IMMUTABLE (baked into
|
|
92
|
+
/// impl bytecode, shared by every proxy, changeable only via a
|
|
93
|
+
/// custody-gated UUPS upgrade). May be `address(0)` on legacy/test
|
|
94
|
+
/// deploys that don't use the sentinel — in which case the
|
|
95
|
+
/// sentinel fails CLOSED (see `isValidSignature`).
|
|
96
|
+
address private immutable _approvedHashRegistry;
|
|
97
|
+
|
|
63
98
|
/// @dev Authorized DelegationManager (ERC-7710 executor)
|
|
64
99
|
address private _delegationManager;
|
|
65
100
|
|
|
@@ -108,9 +143,21 @@ contract AgentAccount is
|
|
|
108
143
|
}
|
|
109
144
|
|
|
110
145
|
/// @dev Spec 007 Phase A.5 — current pending upgrade (set by
|
|
111
|
-
/// `
|
|
146
|
+
/// `scheduleUpgrade` when `_upgradeTimelock > 0`).
|
|
112
147
|
PendingUpgrade private _pendingUpgrade;
|
|
113
148
|
|
|
149
|
+
/// @dev CA-1 — transient "the current call stack is an authorized upgrade
|
|
150
|
+
/// context" flag. Set while (a) a module action executes
|
|
151
|
+
/// (custody-governed, e.g. `CustodyPolicy.ApplySystemUpdate`, which
|
|
152
|
+
/// carries its own T5 quorum + timelock) or (b) a matured owner-queued
|
|
153
|
+
/// upgrade fires (`executePendingUpgrade`). `_authorizeUpgrade` reads it
|
|
154
|
+
/// so that, once an account configures `_upgradeTimelock`, a DIRECT owner
|
|
155
|
+
/// `upgradeToAndCall` is refused (must `scheduleUpgrade` then
|
|
156
|
+
/// `executePendingUpgrade`), while the custody path keeps working with no
|
|
157
|
+
/// double delay. Transient ⇒ tx-global (visible in the nested
|
|
158
|
+
/// `_authorizeUpgrade` frame) AND absent from the storage layout.
|
|
159
|
+
bool private transient _upgradeAuthorizedCtx;
|
|
160
|
+
|
|
114
161
|
/// @dev Spec 007 Phase A.5 — maximum permitted upgrade timelock to
|
|
115
162
|
/// protect against an owner accidentally setting an absurdly
|
|
116
163
|
/// large delay that bricks future migrations.
|
|
@@ -127,6 +174,9 @@ contract AgentAccount is
|
|
|
127
174
|
|
|
128
175
|
error NotFromSelf();
|
|
129
176
|
error NotCustodianOrSelf();
|
|
177
|
+
/// @notice R6.5 — thrown when a `whenNotPaused`-guarded entrypoint
|
|
178
|
+
/// is called while `AgenticGovernance.isPaused() == true`.
|
|
179
|
+
error SystemPaused();
|
|
130
180
|
error CustodianAlreadyExists(address owner);
|
|
131
181
|
error CustodianDoesNotExist(address owner);
|
|
132
182
|
error CannotRemoveLastCustodian();
|
|
@@ -160,6 +210,13 @@ contract AgentAccount is
|
|
|
160
210
|
error UpgradeNotReady(uint64 readyAt, uint256 nowTs);
|
|
161
211
|
error PendingUpgradeMismatch(address pending, address attempted);
|
|
162
212
|
error UpgradePending();
|
|
213
|
+
/// @notice CA-1 — a direct `upgradeToAndCall` was attempted while a per-account
|
|
214
|
+
/// upgrade timelock is set; route through `scheduleUpgrade` +
|
|
215
|
+
/// `executePendingUpgrade` (or the custody module path) instead.
|
|
216
|
+
error DirectUpgradeBlocked();
|
|
217
|
+
/// @notice CA-1 — `scheduleUpgrade` called with no upgrade timelock configured
|
|
218
|
+
/// (nothing to queue against — upgrade directly).
|
|
219
|
+
error UpgradeTimelockNotSet();
|
|
163
220
|
|
|
164
221
|
// Wave 2A — on-chain authority closure (contract audit C-1..C-3).
|
|
165
222
|
error ValidatorRequired();
|
|
@@ -200,10 +257,53 @@ contract AgentAccount is
|
|
|
200
257
|
_;
|
|
201
258
|
}
|
|
202
259
|
|
|
260
|
+
// ─── R6.5 / CON-AgentAccount-005 — system-wide pause modifier ──
|
|
261
|
+
//
|
|
262
|
+
// When `AgenticGovernance.isPaused() == true`, every mutating
|
|
263
|
+
// external entrypoint guarded by this modifier reverts with
|
|
264
|
+
// `SystemPaused`. Recovery-shaped entrypoints
|
|
265
|
+
// (`uninstallModule`, `cancelPendingUpgrade`, `removeCustodian`,
|
|
266
|
+
// the three `onlySelf` ceremonies) are deliberately NOT guarded
|
|
267
|
+
// — an operator must always be able to remove attack surface
|
|
268
|
+
// even during an incident.
|
|
269
|
+
//
|
|
270
|
+
// Reads `governance()` through the factory at each check so a
|
|
271
|
+
// factory-level governance rotation propagates to every account
|
|
272
|
+
// without per-account migration. Legacy / direct-deploy accounts
|
|
273
|
+
// (no factory) and EOA-governance / non-conforming-governance
|
|
274
|
+
// deploys are treated as "not paused" so existing test fixtures
|
|
275
|
+
// keep working — mirrors `GovernanceManaged._pausedSafe()`.
|
|
276
|
+
modifier whenNotPaused() {
|
|
277
|
+
if (_systemPaused()) revert SystemPaused();
|
|
278
|
+
_;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/// @dev R6.5 — `staticcall` chain: factory → governance.isPaused().
|
|
282
|
+
/// Returns `false` on any non-conforming hop (legacy
|
|
283
|
+
/// compatibility). Production deploys MUST pass a real
|
|
284
|
+
/// `AgenticGovernance` to the factory; the production-deploy
|
|
285
|
+
/// preflight enforces that invariant.
|
|
286
|
+
function _systemPaused() internal view returns (bool) {
|
|
287
|
+
address f = _factory;
|
|
288
|
+
if (f == address(0)) return false;
|
|
289
|
+
(bool okF, bytes memory dataF) = f.staticcall(
|
|
290
|
+
abi.encodeWithSelector(IAgentAccountFactoryView.governance.selector)
|
|
291
|
+
);
|
|
292
|
+
if (!okF || dataF.length < 32) return false;
|
|
293
|
+
address g = abi.decode(dataF, (address));
|
|
294
|
+
if (g == address(0) || g.code.length == 0) return false;
|
|
295
|
+
(bool okG, bytes memory dataG) = g.staticcall(
|
|
296
|
+
abi.encodeWithSelector(IAgentAccountPauseView.isPaused.selector)
|
|
297
|
+
);
|
|
298
|
+
if (!okG || dataG.length < 32) return false;
|
|
299
|
+
return abi.decode(dataG, (bool));
|
|
300
|
+
}
|
|
301
|
+
|
|
203
302
|
// ─── Constructor / Initializer ──────────────────────────────────
|
|
204
303
|
|
|
205
|
-
constructor(IEntryPoint entryPoint_) {
|
|
304
|
+
constructor(IEntryPoint entryPoint_, address approvedHashRegistry_) {
|
|
206
305
|
_entryPoint = entryPoint_;
|
|
306
|
+
_approvedHashRegistry = approvedHashRegistry_;
|
|
207
307
|
_disableInitializers();
|
|
208
308
|
}
|
|
209
309
|
|
|
@@ -331,7 +431,20 @@ contract AgentAccount is
|
|
|
331
431
|
* will revert here. Master / bundler / session-issuer
|
|
332
432
|
* therefore cannot upgrade even by submitting the tx.
|
|
333
433
|
*/
|
|
334
|
-
function _authorizeUpgrade(address) internal view override onlySelf {
|
|
434
|
+
function _authorizeUpgrade(address) internal view override onlySelf {
|
|
435
|
+
// CA-1: the per-account upgrade timelock is now ENFORCED here (it was
|
|
436
|
+
// previously dead code — set via `setUpgradeTimelock` but never consulted,
|
|
437
|
+
// so a direct `upgradeToAndCall` fired immediately). When a timelock is
|
|
438
|
+
// configured, a direct owner upgrade is refused; the owner must
|
|
439
|
+
// `scheduleUpgrade` then `executePendingUpgrade` after the delay. Authorized
|
|
440
|
+
// contexts — a custody-module action (its own T5 quorum + timelock) or the
|
|
441
|
+
// matured owner queue — set `_upgradeAuthorizedCtx` and pass through, so the
|
|
442
|
+
// custody path is not double-delayed. A zero timelock keeps the
|
|
443
|
+
// immediate-upgrade default (backward-compatible).
|
|
444
|
+
if (_upgradeTimelock != 0 && !_upgradeAuthorizedCtx) {
|
|
445
|
+
revert DirectUpgradeBlocked();
|
|
446
|
+
}
|
|
447
|
+
}
|
|
335
448
|
|
|
336
449
|
/**
|
|
337
450
|
* @notice DEPRECATED — single-signature upgrade path (contract audit C-3).
|
|
@@ -358,10 +471,25 @@ contract AgentAccount is
|
|
|
358
471
|
revert LegacyUpgradePathDisabled();
|
|
359
472
|
}
|
|
360
473
|
|
|
474
|
+
/// @notice CA-1 — queue an implementation upgrade subject to the per-account
|
|
475
|
+
/// upgrade timelock. The owner signs a userOp that calls this
|
|
476
|
+
/// (`onlySelf`); the bound implementation can then be applied via
|
|
477
|
+
/// `executePendingUpgrade` once `_upgradeTimelock` has elapsed. Only
|
|
478
|
+
/// meaningful when a timelock is configured — with none set, upgrade
|
|
479
|
+
/// directly. Overwrites any existing pending upgrade.
|
|
480
|
+
function scheduleUpgrade(address newImplementation) external onlySelf {
|
|
481
|
+
if (newImplementation == address(0)) revert ZeroAddress();
|
|
482
|
+
uint256 tl = _upgradeTimelock;
|
|
483
|
+
if (tl == 0) revert UpgradeTimelockNotSet();
|
|
484
|
+
uint64 readyAt = uint64(block.timestamp + tl);
|
|
485
|
+
_pendingUpgrade = PendingUpgrade({newImplementation: newImplementation, readyAt: readyAt});
|
|
486
|
+
emit UpgradeProposed(newImplementation, readyAt);
|
|
487
|
+
}
|
|
488
|
+
|
|
361
489
|
/// @notice Execute a previously-queued upgrade. Permissionless once
|
|
362
490
|
/// the timelock has expired — anyone can pay the gas, but
|
|
363
491
|
/// the implementation address was bound at queue time.
|
|
364
|
-
function executePendingUpgrade() external {
|
|
492
|
+
function executePendingUpgrade() external whenNotPaused {
|
|
365
493
|
PendingUpgrade memory p = _pendingUpgrade;
|
|
366
494
|
if (p.readyAt == 0) revert NoPendingUpgrade();
|
|
367
495
|
if (block.timestamp < p.readyAt) revert UpgradeNotReady(p.readyAt, block.timestamp);
|
|
@@ -369,7 +497,12 @@ contract AgentAccount is
|
|
|
369
497
|
// impl can't replay this state.
|
|
370
498
|
delete _pendingUpgrade;
|
|
371
499
|
emit UpgradeAuthorized(p.newImplementation);
|
|
500
|
+
// CA-1: authorize the nested `_authorizeUpgrade` for this matured queue
|
|
501
|
+
// (transient, tx-global). Cleared after so a later same-tx direct upgrade
|
|
502
|
+
// isn't inadvertently authorized.
|
|
503
|
+
_upgradeAuthorizedCtx = true;
|
|
372
504
|
this.upgradeToAndCall(p.newImplementation, "");
|
|
505
|
+
_upgradeAuthorizedCtx = false;
|
|
373
506
|
}
|
|
374
507
|
|
|
375
508
|
/// @notice Cancel a queued upgrade during the timelock window. Owner
|
|
@@ -689,7 +822,7 @@ contract AgentAccount is
|
|
|
689
822
|
uint256 moduleTypeId,
|
|
690
823
|
address module,
|
|
691
824
|
bytes calldata initData
|
|
692
|
-
) external onlySelfOrFactoryInit {
|
|
825
|
+
) external onlySelfOrFactoryInit whenNotPaused {
|
|
693
826
|
if (module == address(0)) revert ZeroAddress();
|
|
694
827
|
if (!_isSupportedModuleType(moduleTypeId)) revert UnsupportedModuleType(moduleTypeId);
|
|
695
828
|
|
|
@@ -837,6 +970,7 @@ contract AgentAccount is
|
|
|
837
970
|
function executeFromModule(address target, uint256 value, bytes calldata data)
|
|
838
971
|
external
|
|
839
972
|
nonReentrant
|
|
973
|
+
whenNotPaused
|
|
840
974
|
returns (bytes memory)
|
|
841
975
|
{
|
|
842
976
|
ModulesStorage storage $ = _modulesStorage();
|
|
@@ -844,7 +978,14 @@ contract AgentAccount is
|
|
|
844
978
|
revert NotInstalledExecutor(msg.sender);
|
|
845
979
|
}
|
|
846
980
|
emit ModuleExecuted(msg.sender, target, value);
|
|
981
|
+
// CA-1: a module action is a custody-governed context (only an installed
|
|
982
|
+
// EXECUTOR — the CustodyPolicy — reaches here, gated upstream by its own
|
|
983
|
+
// T5 quorum + timelock). Authorize any upgrade it dispatches so the custody
|
|
984
|
+
// path is exempt from the per-account `_upgradeTimelock` (no double delay).
|
|
985
|
+
// Transient + nonReentrant, cleared after the call.
|
|
986
|
+
_upgradeAuthorizedCtx = true;
|
|
847
987
|
(bool ok, bytes memory ret) = target.call{value: value}(data);
|
|
988
|
+
_upgradeAuthorizedCtx = false;
|
|
848
989
|
if (!ok) {
|
|
849
990
|
assembly { revert(add(ret, 32), mload(ret)) }
|
|
850
991
|
}
|
|
@@ -891,7 +1032,7 @@ contract AgentAccount is
|
|
|
891
1032
|
/// EntryPoint / self / DelegationManager; the guard hardens
|
|
892
1033
|
/// against a malicious target re-entering through one of those
|
|
893
1034
|
/// callers.
|
|
894
|
-
function execute(address target, uint256 value, bytes calldata data) external override nonReentrant {
|
|
1035
|
+
function execute(address target, uint256 value, bytes calldata data) external override nonReentrant whenNotPaused {
|
|
895
1036
|
_requireForExecute();
|
|
896
1037
|
|
|
897
1038
|
ModulesStorage storage $ = _modulesStorage();
|
|
@@ -944,7 +1085,7 @@ contract AgentAccount is
|
|
|
944
1085
|
/// OUTER `execute` already holds the guard, so external re-entry
|
|
945
1086
|
/// is blocked; `_requireForExecute` restricts entry to
|
|
946
1087
|
/// EntryPoint / self / DM (DM has its own `nonReentrant`).
|
|
947
|
-
function executeBatch(Call[] calldata calls) external override {
|
|
1088
|
+
function executeBatch(Call[] calldata calls) external override whenNotPaused {
|
|
948
1089
|
_requireForExecute();
|
|
949
1090
|
|
|
950
1091
|
ModulesStorage storage $ = _modulesStorage();
|
|
@@ -1018,6 +1159,21 @@ contract AgentAccount is
|
|
|
1018
1159
|
bytes32 hash,
|
|
1019
1160
|
bytes calldata signature
|
|
1020
1161
|
) external view override(IAgentAccount, IERC1271) returns (bytes4) {
|
|
1162
|
+
// Spec 253 — approved-hash sentinel (ERC-1271 ONLY). A bare 1-byte
|
|
1163
|
+
// 0x03 validates iff THIS account pre-approved `hash` in the
|
|
1164
|
+
// (immutable) ApprovedHashRegistry — letting an org batch its
|
|
1165
|
+
// outbound-delegation approvals into one deploy userOp. Checked on
|
|
1166
|
+
// the RAW signature before the ERC-6492 unwrap: a 1-byte sig can't
|
|
1167
|
+
// carry a 32-byte 6492 suffix, so there's no smuggling path. This
|
|
1168
|
+
// branch is unreachable from `_validateSignature`/`_validateSig`
|
|
1169
|
+
// (the userOp-auth path), where 0x03 is an unknown type → false.
|
|
1170
|
+
// Fails CLOSED if the registry is unset/not-a-contract (ADR-0013).
|
|
1171
|
+
if (signature.length == 1 && uint8(signature[0]) == SIG_TYPE_APPROVED_HASH) {
|
|
1172
|
+
if (_approvedHashRegistry.code.length == 0) return bytes4(0xffffffff);
|
|
1173
|
+
return IApprovedHashRegistryView(_approvedHashRegistry).isApproved(address(this), hash)
|
|
1174
|
+
? ERC1271_MAGIC_VALUE
|
|
1175
|
+
: bytes4(0xffffffff);
|
|
1176
|
+
}
|
|
1021
1177
|
bytes memory inner = signature;
|
|
1022
1178
|
if (signature.length >= 32 && bytes32(signature[signature.length - 32:]) == ERC6492_MAGIC) {
|
|
1023
1179
|
(, , bytes memory unwrapped) = abi.decode(
|
|
@@ -1033,6 +1189,12 @@ contract AgentAccount is
|
|
|
1033
1189
|
|
|
1034
1190
|
uint8 internal constant SIG_TYPE_ECDSA = 0x00;
|
|
1035
1191
|
uint8 internal constant SIG_TYPE_WEBAUTHN = 0x01;
|
|
1192
|
+
/// @dev Spec 253 — approved-hash sentinel. Honored ONLY by
|
|
1193
|
+
/// `isValidSignature` (ERC-1271), NEVER by `_validateSig` /
|
|
1194
|
+
/// `_validateSignature` (the userOp-auth path) — there `0x03` is an
|
|
1195
|
+
/// unknown type and returns false. A bare 1-byte `0x03` means
|
|
1196
|
+
/// "this hash is pre-approved in the ApprovedHashRegistry".
|
|
1197
|
+
uint8 internal constant SIG_TYPE_APPROVED_HASH = 0x03;
|
|
1036
1198
|
|
|
1037
1199
|
/// @dev Internal dispatcher. Accepts plain 65-byte ECDSA sigs as legacy
|
|
1038
1200
|
/// form AND type-prefixed sigs (first byte = SIG_TYPE_*).
|
|
@@ -1061,11 +1223,14 @@ contract AgentAccount is
|
|
|
1061
1223
|
|
|
1062
1224
|
function _verifyEcdsa(bytes32 hash, bytes memory sig) internal view returns (bool) {
|
|
1063
1225
|
// Try raw hash first — matches EntryPoint v0.8 (EIP-712 userOpHash signed directly).
|
|
1226
|
+
// R6.3: third return (sigVersion) intentionally discarded; we rely on `err` + custodian-set membership.
|
|
1227
|
+
// slither-disable-next-line unused-return
|
|
1064
1228
|
(address recovered, ECDSA.RecoverError err,) = ECDSA.tryRecover(hash, sig);
|
|
1065
1229
|
if (err == ECDSA.RecoverError.NoError && _externalCustodians[recovered]) return true;
|
|
1066
1230
|
// Fall back to eth-signed-message wrap — matches v0.7 and legacy ERC-1271
|
|
1067
1231
|
// callers that pre-prefix the digest.
|
|
1068
1232
|
bytes32 ethSigned = hash.toEthSignedMessageHash();
|
|
1233
|
+
// slither-disable-next-line unused-return
|
|
1069
1234
|
(recovered, err,) = ECDSA.tryRecover(ethSigned, sig);
|
|
1070
1235
|
return err == ECDSA.RecoverError.NoError && _externalCustodians[recovered];
|
|
1071
1236
|
}
|
|
@@ -1079,9 +1244,12 @@ contract AgentAccount is
|
|
|
1079
1244
|
bytes memory sig,
|
|
1080
1245
|
address expected
|
|
1081
1246
|
) internal pure returns (bool) {
|
|
1247
|
+
// R6.3: third return (sigVersion) intentionally discarded; the `err` discriminant + `recovered == expected` is the auth.
|
|
1248
|
+
// slither-disable-next-line unused-return
|
|
1082
1249
|
(address recovered, ECDSA.RecoverError err,) = ECDSA.tryRecover(hash, sig);
|
|
1083
1250
|
if (err == ECDSA.RecoverError.NoError && recovered == expected) return true;
|
|
1084
1251
|
bytes32 ethSigned = MessageHashUtils.toEthSignedMessageHash(hash);
|
|
1252
|
+
// slither-disable-next-line unused-return
|
|
1085
1253
|
(recovered, err,) = ECDSA.tryRecover(ethSigned, sig);
|
|
1086
1254
|
return err == ECDSA.RecoverError.NoError && recovered == expected;
|
|
1087
1255
|
}
|
|
@@ -1105,11 +1273,23 @@ contract AgentAccount is
|
|
|
1105
1273
|
PasskeyEntry storage key = $.keys[a.credentialIdDigest];
|
|
1106
1274
|
if (key.x == 0 && key.y == 0) return false;
|
|
1107
1275
|
// H7-C.1 / CON-WEBAUTHN-001: pin per-credential rpIdHash + require UP.
|
|
1108
|
-
// UV
|
|
1109
|
-
//
|
|
1276
|
+
// R8.2 / ATL-SEC-03: require UV (User Verification) at the library
|
|
1277
|
+
// layer. Wave H1 made the JS-side ceremony pass
|
|
1278
|
+
// `userVerification: 'required'` for custody-grade flows
|
|
1279
|
+
// (demo-sso, demo-sso-next, demo-org, demo-web-recovery); the
|
|
1280
|
+
// third-party audit (2026-05-31) correctly observed that the
|
|
1281
|
+
// contract still passed `requireUv: false` here. That meant a
|
|
1282
|
+
// permissive authenticator could produce a UP-only assertion
|
|
1283
|
+
// and we'd accept it. R8.2 makes UV strictly required on every
|
|
1284
|
+
// ERC-1271 verify. Browser ceremonies that pass
|
|
1285
|
+
// `userVerification: 'preferred'` will be REJECTED at the
|
|
1286
|
+
// signature step — those apps (demo-web, demo-web-pro) MUST
|
|
1287
|
+
// upgrade to `'required'` for production deployments. Tests
|
|
1288
|
+
// exercising lower-assurance flows can install a separate
|
|
1289
|
+
// passkey-validator module that relaxes this check.
|
|
1110
1290
|
bytes32 rpIdHash = $.rpIdHashOf[a.credentialIdDigest];
|
|
1111
1291
|
if (rpIdHash == bytes32(0)) return false;
|
|
1112
|
-
return WebAuthnLib.verify(a, hash, key.x, key.y, rpIdHash,
|
|
1292
|
+
return WebAuthnLib.verify(a, hash, key.x, key.y, rpIdHash, /*requireUv*/ true);
|
|
1113
1293
|
} catch {
|
|
1114
1294
|
return false;
|
|
1115
1295
|
}
|
|
@@ -1178,7 +1358,7 @@ contract AgentAccount is
|
|
|
1178
1358
|
/// agenticprimitives AgentAccount is forbidden — enforced via
|
|
1179
1359
|
/// the ERC-165 marker. Passkey custodians are not added here;
|
|
1180
1360
|
/// use `addPasskey` instead.
|
|
1181
|
-
function addCustodian(address owner) external override onlySelf {
|
|
1361
|
+
function addCustodian(address owner) external override onlySelf whenNotPaused {
|
|
1182
1362
|
if (owner == address(0)) revert ZeroAddress();
|
|
1183
1363
|
if (_externalCustodians[owner]) revert CustodianAlreadyExists(owner);
|
|
1184
1364
|
if (_isAgenticPrimitivesAgent(owner)) {
|
|
@@ -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_;
|
|
@@ -134,10 +156,13 @@ contract AgentAccountFactory is GovernanceManaged {
|
|
|
134
156
|
// timelock (see AgenticGovernance).
|
|
135
157
|
_validateInitParams(params);
|
|
136
158
|
|
|
137
|
-
|
|
159
|
+
// CA-F1 (audit 2026-06-10): derive the deploy salt from the FULL custody config so the
|
|
160
|
+
// counterfactual address commits to mode/trustees/timelocks too — not just custodians/passkey.
|
|
161
|
+
bytes32 deploySalt = _effectiveSalt(params, timelockOverrides, salt);
|
|
162
|
+
address addr = _getAddressFromSalt(params, deploySalt);
|
|
138
163
|
if (addr.code.length > 0) return AgentAccount(payable(addr));
|
|
139
164
|
|
|
140
|
-
ERC1967Proxy proxy = new ERC1967Proxy{salt:
|
|
165
|
+
ERC1967Proxy proxy = new ERC1967Proxy{salt: deploySalt}(
|
|
141
166
|
address(accountImplementation),
|
|
142
167
|
_initData(params)
|
|
143
168
|
);
|
|
@@ -159,12 +184,16 @@ contract AgentAccountFactory is GovernanceManaged {
|
|
|
159
184
|
}
|
|
160
185
|
|
|
161
186
|
/// @notice Counterfactual address for `createAgentAccount`.
|
|
187
|
+
/// @dev CA-F1: takes `timelockOverrides` because the address now commits to it. Callers MUST
|
|
188
|
+
/// predict with the SAME (params, timelockOverrides, salt) they will deploy with, else the
|
|
189
|
+
/// predicted address won't match the deployed one.
|
|
162
190
|
function getAddressForAgentAccount(
|
|
163
191
|
AgentAccountInitParams calldata params,
|
|
192
|
+
uint32[7] calldata timelockOverrides,
|
|
164
193
|
uint256 salt
|
|
165
194
|
) external view returns (address) {
|
|
166
195
|
_validateInitParams(params);
|
|
167
|
-
return
|
|
196
|
+
return _getAddressFromSalt(params, _effectiveSalt(params, timelockOverrides, salt));
|
|
168
197
|
}
|
|
169
198
|
|
|
170
199
|
// ─── Internals ──────────────────────────────────────────────────────
|
|
@@ -187,22 +216,40 @@ contract AgentAccountFactory is GovernanceManaged {
|
|
|
187
216
|
);
|
|
188
217
|
}
|
|
189
218
|
|
|
190
|
-
|
|
219
|
+
/// @dev CA-F1 (audit 2026-06-10) — the deploy salt commits to the FULL custody configuration:
|
|
220
|
+
/// mode, trustees, and per-tier timelock overrides. `_initData` already folds custodians +
|
|
221
|
+
/// the initial passkey into the proxy bytecode hash (and thus the address); those, plus the
|
|
222
|
+
/// mode/trustees/timelocks bound here, mean the canonical address IS its complete custody
|
|
223
|
+
/// config. Two `createAgentAccount` calls that differ in ANY of these resolve to DIFFERENT
|
|
224
|
+
/// addresses, so an attacker can no longer front-run a victim's counterfactual address with a
|
|
225
|
+
/// weaker mode, attacker-controlled recovery trustees, or shortened timelocks. The
|
|
226
|
+
/// occupied-address branch is consequently reachable only by an identical-config request —
|
|
227
|
+
/// which also closes CA-F2 (silent adoption of a mismatched pre-existing account) by
|
|
228
|
+
/// construction, with no separate config-equality assertion needed.
|
|
229
|
+
function _effectiveSalt(
|
|
191
230
|
AgentAccountInitParams calldata params,
|
|
231
|
+
uint32[7] calldata timelockOverrides,
|
|
192
232
|
uint256 salt
|
|
233
|
+
) internal pure returns (bytes32) {
|
|
234
|
+
return keccak256(abi.encode(salt, params.mode, params.trustees, timelockOverrides));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function _getAddressFromSalt(
|
|
238
|
+
AgentAccountInitParams calldata params,
|
|
239
|
+
bytes32 deploySalt
|
|
193
240
|
) internal view returns (address) {
|
|
194
241
|
bytes memory initData = _initData(params);
|
|
195
|
-
return _create2Address(initData,
|
|
242
|
+
return _create2Address(initData, deploySalt);
|
|
196
243
|
}
|
|
197
244
|
|
|
198
|
-
function _create2Address(bytes memory initData,
|
|
245
|
+
function _create2Address(bytes memory initData, bytes32 salt) internal view returns (address) {
|
|
199
246
|
bytes memory proxyBytecode = abi.encodePacked(
|
|
200
247
|
type(ERC1967Proxy).creationCode,
|
|
201
248
|
abi.encode(address(accountImplementation), initData)
|
|
202
249
|
);
|
|
203
250
|
bytes32 bytecodeHash = keccak256(proxyBytecode);
|
|
204
251
|
return address(uint160(uint256(keccak256(
|
|
205
|
-
abi.encodePacked(bytes1(0xff), address(this),
|
|
252
|
+
abi.encodePacked(bytes1(0xff), address(this), salt, bytecodeHash)
|
|
206
253
|
))));
|
|
207
254
|
}
|
|
208
255
|
|
|
@@ -219,6 +266,13 @@ contract AgentAccountFactory is GovernanceManaged {
|
|
|
219
266
|
|
|
220
267
|
bool hasPasskey = params.initialPasskeyX != 0 && params.initialPasskeyY != 0;
|
|
221
268
|
if (params.custodians.length == 0 && !hasPasskey) revert NoInitialSigner();
|
|
269
|
+
// R9.6 / ATL-SEC-05: cap the initial custodian set so
|
|
270
|
+
// `_buildValidatorInitData`'s `uint8(nSigners)` cast can't
|
|
271
|
+
// silently truncate. 32 is well above any rational config and
|
|
272
|
+
// well below the uint8 boundary.
|
|
273
|
+
if (params.custodians.length > MAX_INITIAL_CUSTODIANS) {
|
|
274
|
+
revert TooManyInitialCustodians(params.custodians.length, MAX_INITIAL_CUSTODIANS);
|
|
275
|
+
}
|
|
222
276
|
|
|
223
277
|
if (params.mode == 0) return;
|
|
224
278
|
|