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

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.
@@ -1,35 +1,35 @@
1
1
  // Generated by scripts/build-deployments.mjs — do not edit by hand.
2
2
  // Source: deployments-base-sepolia.json
3
3
  export const CONTRACTS = {
4
- "agentAccountFactory": "0x6f7Fc9B36977F55666548e0a73D9063F0D88A760",
5
- "agentAccountImplementation": "0x11f1523D9883BdF382993d70C601D307ED0F1f13",
6
- "agentNameRegistry": "0x6e660e41CFd0B410827fb96D21c26600AF6abF2f",
7
- "agentNameResolver": "0x324F5A60c5DE45244c70a43cA3A6B1462d80fF5f",
8
- "agentNameUniversalResolver": "0x40587e3591F7051ba7534177aB1F0B366034040e",
9
- "agentProfileResolver": "0x748D005E8B3b366132F209F066821B2713A6e19e",
10
- "agentRelationship": "0x5c3bf13822a37c84Dc617C920dB61a8217f6311d",
11
- "agreementRegistry": "0x3B1DB6AC972b649E9E0960F0816F4D1EC3264c22",
12
- "allowedMethodsEnforcer": "0x1500601201aA339249e2B78c42952Cf26CD12Ea7",
13
- "allowedTargetsEnforcer": "0xfA1c40F9dC4ACC4d339D2eDE0003872154292CEa",
14
- "approvedHashRegistry": "0xE01c1356F4B10FEfa8Ab210020B802cf3e900759",
15
- "attestationRegistry": "0x16EB207C65A63E3ae256F49a4B11e785fE001139",
16
- "callDataHashEnforcer": "0x7267eF129fCf341717D7f3469DDf7A0135d00b61",
4
+ "agentAccountFactory": "0x3E68B72B45e7C9d35B210E4Ab06e5Cece85cEbE4",
5
+ "agentAccountImplementation": "0x338318ABf9C8b28E7e77ed96a91857cCE1Db1328",
6
+ "agentNameRegistry": "0x15F7ed064A230C011b0244A14fD9653f011d609B",
7
+ "agentNameResolver": "0xcC9855A23C7a4D4592E68Fe47E195C2f9D832473",
8
+ "agentNameUniversalResolver": "0x7d777d2d0bbc1806B9Cc779121C27fbaAaFDb60b",
9
+ "agentProfileResolver": "0xc448EB159B2a35F07f6E1814CC8d125244d7384B",
10
+ "agentRelationship": "0xE6B8F6C7F1c9857013dA356DF08D0384334A9607",
11
+ "agreementRegistry": "0x7f4d6A74eda1D6ce251D371566f1c6639F9B0DDa",
12
+ "allowedMethodsEnforcer": "0xf77cf06aCd8b9e2f3204fe4bE16449B3594c9633",
13
+ "allowedTargetsEnforcer": "0xe3909FdEfD8b74B7D006E045DCd678e97D469Bfd",
14
+ "approvedHashRegistry": "0x90735B5281434D1746926E2AA722Cc6DbF3EFEe6",
15
+ "attestationRegistry": "0xaE3861fd299c74D96F6FCA25f19c7ACf8b9020ed",
16
+ "callDataHashEnforcer": "0x9F12932F1b85A1F96c1Cb1c5A0645e3F0Ab478Dd",
17
17
  "chainId": 84532,
18
- "custodyPolicy": "0x5e3e3Bda33c799f2403a30aE35bf81deff236283",
19
- "delegationManager": "0xBC5AE6213E91E076232a6c8e3942746f06A50154",
18
+ "custodyPolicy": "0xDeD6542A55476745846370562d1c75D15DDf2572",
19
+ "delegationManager": "0x3a8E2cE74564f699b135db6f266ccDb563979C05",
20
20
  "deployer": "0x31ed17fb99e82E02085Ab4B3cbdaB05489098b44",
21
- "entryPoint": "0x7707C2b483CcADc3E72C2d39d216aC1C337FCd9d",
22
- "geoFeatureRegistry": "0x0A9f10cb72D6Fb5C89Aa5865f397957729cF71F0",
23
- "ontologyTermRegistry": "0xa9635EbfDb993395df6CD03b0DEE21E5b2A66Ec1",
24
- "permissionlessSubregistry": "0xF38fdcd8eC83360C2F73CEEa52ae419D37aCaB5F",
25
- "permissionlessSubregistryDemoAgent": "0x1c698cBFE2733301614a245a505E20eB5597a972",
26
- "quorumEnforcer": "0xeB88df4492FB8Eb254f6017eD0463d42CF6FC8e7",
27
- "relationshipTypeRegistry": "0x7c5FfD0aB55C52f6aa00c04D2a30457579A3083B",
28
- "shapeRegistry": "0xAe431c6F000235d30ae28f507493215815eF88E2",
29
- "skillDefinitionRegistry": "0xc0A26f8bac0482E0F8e3a61762F5862Bc21a2D11",
30
- "smartAgentPaymaster": "0x0e54A8Cc986C86530f0593817E8CafE01FC52983",
31
- "timestampEnforcer": "0xf9AeF60b8cb4939bF5B1AF48E777dad99Ef1084f",
32
- "universalSignatureValidator": "0x7A282fFf06E6DC73613A31F55345535e24CB6832",
33
- "valueEnforcer": "0x4be1B537b9583E69D9baBf8af1dc5371A4a750C5"
21
+ "entryPoint": "0x9B33De9d9597de82Eb56b5B26C478Dc0a6955388",
22
+ "geoFeatureRegistry": "0x78FdCae0aC9B0CD9943996282b8F39D77bec5b1c",
23
+ "ontologyTermRegistry": "0xf486Ba6e9FD2d687e68a1a7A77D0FA27bb1E66c3",
24
+ "permissionlessSubregistry": "0x1B8ED8693738e1A9DD653FEE5430d49e00202Bb7",
25
+ "permissionlessSubregistryDemoAgent": "0xc76b1cdB4340398f59fD9C5d6Cb65fCdC606B33b",
26
+ "quorumEnforcer": "0x76fb494aE991d1fe1603a91384BA4c1F890442a8",
27
+ "relationshipTypeRegistry": "0x526fBF5900b18B1D06201493bcE470b416FB71C3",
28
+ "shapeRegistry": "0x03AbD67A910abB2c5026f1973A05502e0473e316",
29
+ "skillDefinitionRegistry": "0x5ae94aba46D7daA1Ca9eaE0051043Ad05bcc392B",
30
+ "smartAgentPaymaster": "0x8eF92B9D62826052D8F7e6dcaB630dC3890bF540",
31
+ "timestampEnforcer": "0x7c452434314A5bf2048f9263aeCd0cC20Fa07965",
32
+ "universalSignatureValidator": "0xe8F3e531429F44D77dC3A331C18032bfa1550D84",
33
+ "valueEnforcer": "0x7c4533e0A9fB0B1cb5aa70327b222a80d72d52db"
34
34
  };
35
35
  export default CONTRACTS;
@@ -3,6 +3,7 @@ import type { Address } from 'viem';
3
3
  import type { Deployments } from './base-sepolia';
4
4
  export type { Deployments };
5
5
  export declare const BASE_SEPOLIA: Deployments;
6
+ export declare const REGISTRIES_BASE_SEPOLIA: Deployments;
6
7
 
7
- export type Network = 'base-sepolia';
8
+ export type Network = 'base-sepolia' | 'registries-base-sepolia';
8
9
  export declare function getDeployments(network: Network): Deployments;
@@ -1,9 +1,12 @@
1
1
  // Generated by scripts/build-deployments.mjs — do not edit by hand.
2
2
  import { CONTRACTS as BASE_SEPOLIA } from './base-sepolia.js';
3
3
  export { BASE_SEPOLIA };
4
+ import { CONTRACTS as REGISTRIES_BASE_SEPOLIA } from './registries-base-sepolia.js';
5
+ export { REGISTRIES_BASE_SEPOLIA };
4
6
 
5
7
  const TABLE = {
6
8
  "base-sepolia": BASE_SEPOLIA,
9
+ "registries-base-sepolia": REGISTRIES_BASE_SEPOLIA,
7
10
  };
8
11
 
9
12
  export function getDeployments(network) {
@@ -0,0 +1,9 @@
1
+ // Generated by scripts/build-deployments.mjs — do not edit by hand.
2
+ // Source: deployments-registries-base-sepolia.json
3
+ import type { Address } from 'viem';
4
+ export interface Deployments {
5
+ readonly "agreementRegistry": Address;
6
+ readonly "attestationRegistry": Address;
7
+ }
8
+ export declare const CONTRACTS: Deployments;
9
+ export default CONTRACTS;
@@ -0,0 +1,7 @@
1
+ // Generated by scripts/build-deployments.mjs — do not edit by hand.
2
+ // Source: deployments-registries-base-sepolia.json
3
+ export const CONTRACTS = {
4
+ "agreementRegistry": "0x9f2A085f0bd2d8Afb69490C0C98D88592093906A",
5
+ "attestationRegistry": "0x4a156CAaE69E894E3C5e6F3064fE028F138b6847"
6
+ };
7
+ export default CONTRACTS;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticprimitives/contracts",
3
- "version": "1.0.0-alpha.6",
3
+ "version": "1.0.0-alpha.7",
4
4
  "description": "Solidity contracts + ABIs + flattened sources + per-network deployment addresses for the agenticprimitives stack.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -59,6 +59,8 @@
59
59
  "deploy:base-sepolia": "DEPLOY_NETWORK=base-sepolia forge script script/Deploy.s.sol --rpc-url $BASE_SEPOLIA_RPC --broadcast --private-key $PRIVATE_KEY",
60
60
  "deploy:paymaster:base-sepolia": "DEPLOY_NETWORK=base-sepolia forge script script/DeployPaymaster.s.sol --rpc-url $BASE_SEPOLIA_RPC --broadcast --private-key $PRIVATE_KEY --sig 'run()'",
61
61
  "deploy:paymaster:anvil": "DEPLOY_NETWORK=anvil forge script script/DeployPaymaster.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --sig 'run()'",
62
+ "deploy:registries:base-sepolia": "DEPLOY_NETWORK=base-sepolia forge script script/DeployRegistries.s.sol --rpc-url $BASE_SEPOLIA_RPC --broadcast --private-key $PRIVATE_KEY --sig 'run()'",
63
+ "deploy:registries:merge": "node scripts/merge-registry-deploy.mjs",
62
64
  "verify:base-sepolia": "bash scripts/verify-base-sepolia.sh",
63
65
  "clean": "rm -rf out cache broadcast dist"
64
66
  }
@@ -143,9 +143,21 @@ contract AgentAccount is
143
143
  }
144
144
 
145
145
  /// @dev Spec 007 Phase A.5 — current pending upgrade (set by
146
- /// `upgradeToWithAuthorization` when `_upgradeTimelock > 0`).
146
+ /// `scheduleUpgrade` when `_upgradeTimelock > 0`).
147
147
  PendingUpgrade private _pendingUpgrade;
148
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
+
149
161
  /// @dev Spec 007 Phase A.5 — maximum permitted upgrade timelock to
150
162
  /// protect against an owner accidentally setting an absurdly
151
163
  /// large delay that bricks future migrations.
@@ -198,6 +210,13 @@ contract AgentAccount is
198
210
  error UpgradeNotReady(uint64 readyAt, uint256 nowTs);
199
211
  error PendingUpgradeMismatch(address pending, address attempted);
200
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();
201
220
 
202
221
  // Wave 2A — on-chain authority closure (contract audit C-1..C-3).
203
222
  error ValidatorRequired();
@@ -412,7 +431,20 @@ contract AgentAccount is
412
431
  * will revert here. Master / bundler / session-issuer
413
432
  * therefore cannot upgrade even by submitting the tx.
414
433
  */
415
- 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
+ }
416
448
 
417
449
  /**
418
450
  * @notice DEPRECATED — single-signature upgrade path (contract audit C-3).
@@ -439,6 +471,21 @@ contract AgentAccount is
439
471
  revert LegacyUpgradePathDisabled();
440
472
  }
441
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
+
442
489
  /// @notice Execute a previously-queued upgrade. Permissionless once
443
490
  /// the timelock has expired — anyone can pay the gas, but
444
491
  /// the implementation address was bound at queue time.
@@ -450,7 +497,12 @@ contract AgentAccount is
450
497
  // impl can't replay this state.
451
498
  delete _pendingUpgrade;
452
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;
453
504
  this.upgradeToAndCall(p.newImplementation, "");
505
+ _upgradeAuthorizedCtx = false;
454
506
  }
455
507
 
456
508
  /// @notice Cancel a queued upgrade during the timelock window. Owner
@@ -926,7 +978,14 @@ contract AgentAccount is
926
978
  revert NotInstalledExecutor(msg.sender);
927
979
  }
928
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;
929
987
  (bool ok, bytes memory ret) = target.call{value: value}(data);
988
+ _upgradeAuthorizedCtx = false;
930
989
  if (!ok) {
931
990
  assembly { revert(add(ret, 32), mload(ret)) }
932
991
  }
@@ -156,10 +156,13 @@ contract AgentAccountFactory is GovernanceManaged {
156
156
  // timelock (see AgenticGovernance).
157
157
  _validateInitParams(params);
158
158
 
159
- address addr = _getAddressForAgentAccount(params, salt);
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);
160
163
  if (addr.code.length > 0) return AgentAccount(payable(addr));
161
164
 
162
- ERC1967Proxy proxy = new ERC1967Proxy{salt: bytes32(salt)}(
165
+ ERC1967Proxy proxy = new ERC1967Proxy{salt: deploySalt}(
163
166
  address(accountImplementation),
164
167
  _initData(params)
165
168
  );
@@ -181,12 +184,16 @@ contract AgentAccountFactory is GovernanceManaged {
181
184
  }
182
185
 
183
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.
184
190
  function getAddressForAgentAccount(
185
191
  AgentAccountInitParams calldata params,
192
+ uint32[7] calldata timelockOverrides,
186
193
  uint256 salt
187
194
  ) external view returns (address) {
188
195
  _validateInitParams(params);
189
- return _getAddressForAgentAccount(params, salt);
196
+ return _getAddressFromSalt(params, _effectiveSalt(params, timelockOverrides, salt));
190
197
  }
191
198
 
192
199
  // ─── Internals ──────────────────────────────────────────────────────
@@ -209,22 +216,40 @@ contract AgentAccountFactory is GovernanceManaged {
209
216
  );
210
217
  }
211
218
 
212
- function _getAddressForAgentAccount(
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(
213
230
  AgentAccountInitParams calldata params,
231
+ uint32[7] calldata timelockOverrides,
214
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
215
240
  ) internal view returns (address) {
216
241
  bytes memory initData = _initData(params);
217
- return _create2Address(initData, salt);
242
+ return _create2Address(initData, deploySalt);
218
243
  }
219
244
 
220
- function _create2Address(bytes memory initData, uint256 salt) internal view returns (address) {
245
+ function _create2Address(bytes memory initData, bytes32 salt) internal view returns (address) {
221
246
  bytes memory proxyBytecode = abi.encodePacked(
222
247
  type(ERC1967Proxy).creationCode,
223
248
  abi.encode(address(accountImplementation), initData)
224
249
  );
225
250
  bytes32 bytecodeHash = keccak256(proxyBytecode);
226
251
  return address(uint160(uint256(keccak256(
227
- abi.encodePacked(bytes1(0xff), address(this), bytes32(salt), bytecodeHash)
252
+ abi.encodePacked(bytes1(0xff), address(this), salt, bytecodeHash)
228
253
  ))));
229
254
  }
230
255
 
@@ -71,6 +71,20 @@ import "./governance/IGovernance.sol";
71
71
  * `SenderNotAccepted` until governance explicitly opts a sender
72
72
  * in. That is the documented safe state.
73
73
  * 2. Monitor `getDeposit()` and alert below a runway threshold.
74
+ * 3. PM-2 — hand the Ownable owner to the governance TimelockController via
75
+ * `Ownable2Step` (`transferOwnership(governance)` then a timelocked
76
+ * `acceptOwnership`) once deploy-time staking/funding is done. The owner
77
+ * controls the INHERITED instant `withdrawTo`/stake drains; in production
78
+ * it MUST be the timelock, not the bootstrap deployer EOA. The
79
+ * governance-native, TIMELOCKED `scheduleDepositWithdrawal` /
80
+ * `executeDepositWithdrawal` path below is the sanctioned drain that stays
81
+ * coupled to governance + delayed regardless of the owner handoff state.
82
+ * The deployer-owned window is bootstrap-only (tracked under the
83
+ * governance-key custody item).
84
+ * 4. PM-1 — after governance pauses, call `syncPauseFromGovernance()` (or
85
+ * `setPauseMirror(true)` from governance) so the validation-time mirror
86
+ * halts sponsorship. Validation never reads governance storage directly
87
+ * (ERC-7562), so the mirror must be pushed/synced.
74
88
  *
75
89
  * @dev Inherits `addStake`, `unlockStake`, `withdrawStake`, `deposit`,
76
90
  * and `withdrawTo` from `BasePaymaster`. Ownable owner is set in
@@ -95,19 +109,62 @@ contract SmartAgentPaymaster is BasePaymaster {
95
109
  /// validation. Stored immutable.
96
110
  address public immutable governance;
97
111
 
112
+ /// @notice PM-1 — local mirror of the governance pause flag. This is the
113
+ /// ONLY pause signal read during ERC-4337 validation. Reading the
114
+ /// external governance contract's storage inside
115
+ /// `_validatePaymasterUserOp` violates the ERC-7562 validation-scope
116
+ /// rules (a paymaster may touch only its own / associated storage),
117
+ /// so reputable bundlers (Pimlico/Stackup/Alchemy) drop the sponsored
118
+ /// op during simulation. We read this own-storage mirror instead;
119
+ /// `syncPauseFromGovernance()` — a normal call OUTSIDE validation —
120
+ /// refreshes it from the canonical governance flag. The mirror is a
121
+ /// validation-time cache of governance's pause bit, never a second
122
+ /// independent source (ADR-0013: cache-of-canonical, no fallback).
123
+ bool private _pausedMirror;
124
+
125
+ /// @notice PM-2 — a scheduled, governance-authorized EntryPoint-deposit
126
+ /// withdrawal. The inherited owner `withdrawTo` is an INSTANT drain
127
+ /// gated only by the Ownable owner; this path couples a withdrawal to
128
+ /// governance (`onlyGovernance`) AND a fixed timelock, so the
129
+ /// sponsorship treasury cannot be emptied in a single transaction.
130
+ struct PendingWithdrawal {
131
+ address payable to;
132
+ uint64 eta;
133
+ uint256 amount;
134
+ }
135
+ PendingWithdrawal private _pendingWithdrawal;
136
+
137
+ /// @notice PM-2 — delay between scheduling and executing a governance
138
+ /// deposit withdrawal. Gives the guardian a window to pause if a
139
+ /// withdrawal is unexpected (incident response).
140
+ uint64 public constant DEPOSIT_WITHDRAWAL_TIMELOCK = 48 hours;
141
+
98
142
  error SenderNotAccepted(address sender);
99
143
  error SystemPaused();
100
144
  error ZeroGovernance();
101
145
  error NotGovernance();
102
146
  error PaymasterDataMalformed();
103
147
  error PaymasterSignatureInvalid();
148
+ /// @dev PM-2 withdrawal-lifecycle errors.
149
+ error NoPendingWithdrawal();
150
+ error WithdrawalNotReady(uint64 eta);
151
+ error WithdrawalAmountZero();
152
+ error WithdrawalToZero();
104
153
 
105
154
  event DevModeSet(bool dev);
106
155
  event SenderAcceptedSet(address indexed sender, bool accepted);
107
156
  event VerifyingSignerSet(address indexed oldSigner, address indexed newSigner);
157
+ /// @notice PM-1 — emitted whenever the local pause mirror is updated.
158
+ event PauseMirrorSynced(bool paused, address indexed by);
159
+ /// @notice PM-2 — deposit-withdrawal lifecycle.
160
+ event DepositWithdrawalScheduled(address indexed to, uint256 amount, uint64 eta);
161
+ event DepositWithdrawalExecuted(address indexed to, uint256 amount);
162
+ event DepositWithdrawalCancelled(address indexed to, uint256 amount);
108
163
 
109
164
  /// @dev Storage gap reserves slots for future state. Phase A.5 §3.1.
110
- uint256[49] private __gap;
165
+ /// PM-1/PM-2 consumed 3 slots (`_pausedMirror` packs into 1;
166
+ /// `PendingWithdrawal` is 2) — gap reduced 49 → 46 to preserve layout.
167
+ uint256[46] private __gap;
111
168
 
112
169
  /// @dev Length of the paymaster-data tail when in verifying mode:
113
170
  /// 6 (validUntil) + 6 (validAfter) + 65 (sig) = 77 bytes.
@@ -173,6 +230,90 @@ contract SmartAgentPaymaster is BasePaymaster {
173
230
  emit VerifyingSignerSet(old, newSigner);
174
231
  }
175
232
 
233
+ // ─── PM-1: pause mirror (own-storage, ERC-7562-compliant) ──────────
234
+
235
+ /// @notice PM-1 — refresh the local pause mirror from the canonical
236
+ /// governance flag. Permissionless ON PURPOSE: validation may not
237
+ /// read governance storage, so the mirror must be refreshable by
238
+ /// anyone (a keeper, a bundler, the guardian's incident response)
239
+ /// the moment governance pauses. This call is a NORMAL transaction
240
+ /// (not paymaster validation), so the ERC-7562 scope rules don't
241
+ /// apply to its cross-contract read. The mirror only ever holds the
242
+ /// canonical governance value — no independent pause source.
243
+ function syncPauseFromGovernance() external {
244
+ bool p = _governanceIsPaused();
245
+ _pausedMirror = p;
246
+ emit PauseMirrorSynced(p, msg.sender);
247
+ }
248
+
249
+ /// @notice PM-1 — direct governance push of the pause mirror, for when
250
+ /// governance wants to halt sponsorship without depending on a
251
+ /// third party to sync. `onlyGovernance` ⇒ inherits the 24h
252
+ /// timelock (or guardian-driven governance flow).
253
+ function setPauseMirror(bool paused_) external onlyGovernance {
254
+ _pausedMirror = paused_;
255
+ emit PauseMirrorSynced(paused_, msg.sender);
256
+ }
257
+
258
+ /// @notice The pause bit read during validation (the local mirror).
259
+ function paused() external view returns (bool) {
260
+ return _pausedMirror;
261
+ }
262
+
263
+ /// @dev Read the canonical governance pause flag. Used ONLY by the
264
+ /// out-of-band sync path, never during validation. Mirrors the prior
265
+ /// defensive decode: a non-conforming governance (EOA / missing fn)
266
+ /// reads as "not paused" so legacy / test deploys keep working.
267
+ function _governanceIsPaused() internal view returns (bool) {
268
+ if (governance.code.length == 0) return false;
269
+ (bool ok, bytes memory data) = governance.staticcall(
270
+ abi.encodeWithSelector(IGovernanceView.isPaused.selector)
271
+ );
272
+ return ok && data.length >= 32 && abi.decode(data, (bool));
273
+ }
274
+
275
+ // ─── PM-2: governance-timelocked deposit withdrawal ────────────────
276
+
277
+ /// @notice PM-2 — schedule a governance-authorized withdrawal of the
278
+ /// EntryPoint deposit. Coupled to governance AND delayed by
279
+ /// `DEPOSIT_WITHDRAWAL_TIMELOCK`, unlike the inherited owner
280
+ /// `withdrawTo` (instant, owner-only). Overwrites any prior pending
281
+ /// withdrawal (governance is the single scheduler).
282
+ function scheduleDepositWithdrawal(address payable to, uint256 amount) external onlyGovernance {
283
+ if (to == address(0)) revert WithdrawalToZero();
284
+ if (amount == 0) revert WithdrawalAmountZero();
285
+ uint64 eta = uint64(block.timestamp) + DEPOSIT_WITHDRAWAL_TIMELOCK;
286
+ _pendingWithdrawal = PendingWithdrawal({to: to, eta: eta, amount: amount});
287
+ emit DepositWithdrawalScheduled(to, amount, eta);
288
+ }
289
+
290
+ /// @notice PM-2 — execute a previously-scheduled withdrawal once its
291
+ /// timelock has elapsed. `onlyGovernance` so the same authority
292
+ /// that scheduled it confirms; the delay gives the guardian time to
293
+ /// pause if the withdrawal is unexpected.
294
+ function executeDepositWithdrawal() external onlyGovernance {
295
+ PendingWithdrawal memory w = _pendingWithdrawal;
296
+ if (w.eta == 0) revert NoPendingWithdrawal();
297
+ if (block.timestamp < w.eta) revert WithdrawalNotReady(w.eta);
298
+ delete _pendingWithdrawal;
299
+ entryPoint().withdrawTo(w.to, w.amount);
300
+ emit DepositWithdrawalExecuted(w.to, w.amount);
301
+ }
302
+
303
+ /// @notice PM-2 — cancel a pending withdrawal (governance de-escalation).
304
+ function cancelDepositWithdrawal() external onlyGovernance {
305
+ PendingWithdrawal memory w = _pendingWithdrawal;
306
+ if (w.eta == 0) revert NoPendingWithdrawal();
307
+ delete _pendingWithdrawal;
308
+ emit DepositWithdrawalCancelled(w.to, w.amount);
309
+ }
310
+
311
+ /// @notice PM-2 — view the pending withdrawal (zero `eta` ⇒ none).
312
+ function pendingWithdrawal() external view returns (address to, uint256 amount, uint64 eta) {
313
+ PendingWithdrawal memory w = _pendingWithdrawal;
314
+ return (w.to, w.amount, w.eta);
315
+ }
316
+
176
317
  // ─── Views ──────────────────────────────────────────────────────────
177
318
 
178
319
  function devMode() external view returns (bool) {
@@ -229,17 +370,13 @@ contract SmartAgentPaymaster is BasePaymaster {
229
370
  bytes32 /*userOpHash*/,
230
371
  uint256 /*maxCost*/
231
372
  ) internal view override returns (bytes memory context, uint256 validationData) {
232
- // H7-C.10 / EXT3-010: gated behind system-wide governance pause.
233
- // Skipped when `governance` is an EOA or non-conforming contract
234
- // (legacy / test deploys); production deploys MUST pass an
235
- // AgenticGovernance address, which the production-deploy preflight
236
- // (`check:production-deploy`) enforces.
237
- if (governance.code.length > 0) {
238
- (bool ok, bytes memory data) = governance.staticcall(
239
- abi.encodeWithSelector(IGovernanceView.isPaused.selector)
240
- );
241
- if (ok && data.length >= 32 && abi.decode(data, (bool))) revert SystemPaused();
242
- }
373
+ // PM-1 / H7-C.10 / EXT3-010: gated behind the system-wide governance
374
+ // pause — but read from the LOCAL mirror (`_pausedMirror`), never the
375
+ // external governance contract's storage. A cross-contract staticcall
376
+ // here violates the ERC-7562 validation-scope rules and gets the op
377
+ // dropped by bundlers. The mirror is refreshed out-of-band via
378
+ // `syncPauseFromGovernance()` (a normal call) or `setPauseMirror()`.
379
+ if (_pausedMirror) revert SystemPaused();
243
380
 
244
381
  if (_dev) {
245
382
  // Dev mode: accept all. validationData = 0 → "valid sig,
@@ -251,6 +251,13 @@ contract DelegationManager is IDelegationManager, ReentrancyGuard {
251
251
  /// the chain is well-formed (signatures, authority chain, not
252
252
  /// revoked) WITHOUT executing it.
253
253
  ///
254
+ /// @dev ⚠️ DANGER — CHAIN-ONLY, NOT A CALL AUTHORIZATION (audit 2026-06-10). This validates ONLY
255
+ /// the chain (signatures / authority / revocation). It does NOT evaluate caveats, so a `true`
256
+ /// result is NOT permission to perform any specific (target, value, calldata). Anything
257
+ /// authorizing a concrete action MUST use {verifyAuthorizationForCall} (evaluates every caveat,
258
+ /// fail-closed) or live {redeemDelegation}. Treating this boolean as call authorization
259
+ /// re-opens the exact over-trust the caveats exist to prevent.
260
+ ///
254
261
  /// Spec 242 §6 (PD-9): the `attestations` package uses this to verify
255
262
  /// bilateral-consent delegations as signed authorization predicates,
256
263
  /// rather than redeeming them as cross-account execution. Returning
@@ -288,8 +295,12 @@ contract DelegationManager is IDelegationManager, ReentrancyGuard {
288
295
  /// stateful enforcer attempting a write under staticcall) yields `false` — so `true`
289
296
  /// is a genuine authorization guarantee for this exact call, and `false` means "use
290
297
  /// 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
298
+ /// evaluation mirrors `_runBeforeHooks` exactly (same args, same `sender`-as-redeemer).
299
+ /// SC-3 (audit 2026-06-09): a no-code enforcer is rejected here with `false`. A raw
300
+ /// `staticcall` to an empty address SUCCEEDS, which would otherwise make a delegation
301
+ /// whose caveat enforcer is a typo / undeployed pass as "constraint-checked" — fail-OPEN.
302
+ /// Live redemption fails CLOSED on the same case (solc's extcodesize check on the
303
+ /// high-level void-returning hook call reverts), so this restores symmetry. Pure
293
304
  /// addition — `redeemDelegation` is untouched.
294
305
  /// @param delegations Chain (leaf first; root authority last)
295
306
  /// @param sender The address that would be the redeemer
@@ -312,6 +323,10 @@ contract DelegationManager is IDelegationManager, ReentrancyGuard {
312
323
  Delegation calldata d = delegations[i];
313
324
  bytes32 dHash = hashDelegation(d);
314
325
  for (uint256 j = 0; j < d.caveats.length; j++) {
326
+ // SC-3: a no-code enforcer constrains NOTHING — a raw staticcall to an empty address
327
+ // returns success, which would fail-open. Reject explicitly so `true` is a genuine
328
+ // guarantee (live redemption fails closed here too via extcodesize on the high-level call).
329
+ if (d.caveats[j].enforcer.code.length == 0) return (false, "no-code-enforcer");
315
330
  // staticcall the same hook redemption would call; success == satisfied read-only.
316
331
  // Gas-bounded: a stateful enforcer (which would revert on its SSTORE under
317
332
  // staticcall) is capped so an on-chain caller (e.g. RW1-1) sees a cheap, predictable
@@ -72,6 +72,9 @@ interface IDelegationManager {
72
72
  /// Used by trust-substrate contracts (e.g. AttestationRegistry's
73
73
  /// bilateral-consent path per spec 242 PD-9) to validate a packed
74
74
  /// delegation as a signed authorization predicate.
75
+ /// @dev ⚠️ DANGER — chain-only; does NOT evaluate caveats. `true` is NOT permission for any
76
+ /// specific call. Use {verifyAuthorizationForCall} (fail-closed, evaluates caveats) or live
77
+ /// redemption to authorize a concrete (target, value, calldata).
75
78
  /// @param delegations Delegation chain, leaf first
76
79
  /// @param sender The address that would be the redeemer
77
80
  /// @return ok True if the chain verifies