@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.
- package/AUDIT.md +21 -1
- package/README.md +36 -12
- package/deployments-base-sepolia.json +32 -1
- package/deployments-registries-base-sepolia.json +1 -0
- package/dist/abi/AgentAccount.json +23 -0
- package/dist/abi/AgentAccountFactory.json +5 -0
- package/dist/abi/AgentNameRegistry.json +5 -0
- package/dist/abi/AgenticGovernance.json +32 -0
- package/dist/abi/AgreementRegistry.json +0 -5
- package/dist/abi/CustodyPolicy.json +11 -0
- package/dist/abi/QuorumEnforcer.json +16 -0
- package/dist/abi/SmartAgentPaymaster.json +209 -0
- package/dist/deployments/base-sepolia.js +28 -28
- package/dist/deployments/index.d.ts +2 -1
- package/dist/deployments/index.js +3 -0
- package/dist/deployments/registries-base-sepolia.d.ts +9 -0
- package/dist/deployments/registries-base-sepolia.js +7 -0
- package/package.json +3 -1
- package/src/AgentAccount.sol +61 -2
- package/src/AgentAccountFactory.sol +32 -7
- package/src/SmartAgentPaymaster.sol +149 -12
- package/src/agency/DelegationManager.sol +17 -2
- package/src/agency/IDelegationManager.sol +3 -0
- package/src/agreement/AgreementRegistry.sol +46 -21
- package/src/attestation/AttestationRegistry.sol +86 -22
- package/src/custody/CustodyPolicy.sol +59 -6
- package/src/enforcers/QuorumEnforcer.sol +12 -0
- package/src/governance/AgenticGovernance.sol +26 -4
- package/src/libraries/SignatureSlotRecovery.sol +8 -3
- package/src/libraries/WebAuthnLib.sol +13 -0
- package/src/naming/AgentNameRegistry.sol +27 -2
|
@@ -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": "
|
|
5
|
-
"agentAccountImplementation": "
|
|
6
|
-
"agentNameRegistry": "
|
|
7
|
-
"agentNameResolver": "
|
|
8
|
-
"agentNameUniversalResolver": "
|
|
9
|
-
"agentProfileResolver": "
|
|
10
|
-
"agentRelationship": "
|
|
11
|
-
"agreementRegistry": "
|
|
12
|
-
"allowedMethodsEnforcer": "
|
|
13
|
-
"allowedTargetsEnforcer": "
|
|
14
|
-
"approvedHashRegistry": "
|
|
15
|
-
"attestationRegistry": "
|
|
16
|
-
"callDataHashEnforcer": "
|
|
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": "
|
|
19
|
-
"delegationManager": "
|
|
18
|
+
"custodyPolicy": "0xDeD6542A55476745846370562d1c75D15DDf2572",
|
|
19
|
+
"delegationManager": "0x3a8E2cE74564f699b135db6f266ccDb563979C05",
|
|
20
20
|
"deployer": "0x31ed17fb99e82E02085Ab4B3cbdaB05489098b44",
|
|
21
|
-
"entryPoint": "
|
|
22
|
-
"geoFeatureRegistry": "
|
|
23
|
-
"ontologyTermRegistry": "
|
|
24
|
-
"permissionlessSubregistry": "
|
|
25
|
-
"permissionlessSubregistryDemoAgent": "
|
|
26
|
-
"quorumEnforcer": "
|
|
27
|
-
"relationshipTypeRegistry": "
|
|
28
|
-
"shapeRegistry": "
|
|
29
|
-
"skillDefinitionRegistry": "
|
|
30
|
-
"smartAgentPaymaster": "
|
|
31
|
-
"timestampEnforcer": "
|
|
32
|
-
"universalSignatureValidator": "
|
|
33
|
-
"valueEnforcer": "
|
|
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.
|
|
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
|
}
|
package/src/AgentAccount.sol
CHANGED
|
@@ -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
|
-
/// `
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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,
|
|
242
|
+
return _create2Address(initData, deploySalt);
|
|
218
243
|
}
|
|
219
244
|
|
|
220
|
-
function _create2Address(bytes memory initData,
|
|
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),
|
|
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
|
-
|
|
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
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
///
|
|
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
|