@agenticprimitives/contracts 0.1.0-alpha.2

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.
Files changed (84) hide show
  1. package/AUDIT.md +67 -0
  2. package/CLAUDE.md +40 -0
  3. package/LICENSE +21 -0
  4. package/README.md +45 -0
  5. package/deployments-anvil.json +1 -0
  6. package/deployments-base-sepolia.json +1 -0
  7. package/dist/abi/AgentNameAttributeResolver.json +798 -0
  8. package/dist/abi/AgentNamePredicates.json +1 -0
  9. package/dist/abi/AgentNameRegistry.json +826 -0
  10. package/dist/abi/AgentNameUniversalResolver.json +222 -0
  11. package/dist/abi/AgentProfilePredicates.json +1 -0
  12. package/dist/abi/AgentProfileResolver.json +1044 -0
  13. package/dist/abi/AgentRelationship.json +583 -0
  14. package/dist/abi/AgentRelationshipPredicates.json +1 -0
  15. package/dist/abi/AgenticGovernance.json +259 -0
  16. package/dist/abi/AllowedMethodsEnforcer.json +108 -0
  17. package/dist/abi/AllowedTargetsEnforcer.json +103 -0
  18. package/dist/abi/ApprovedHashRegistry.json +114 -0
  19. package/dist/abi/AttributeStorage.json +557 -0
  20. package/dist/abi/CaveatEnforcerBase.json +130 -0
  21. package/dist/abi/GovernanceManaged.json +43 -0
  22. package/dist/abi/IAttributeReader.json +98 -0
  23. package/dist/abi/ICaveatEnforcer.json +98 -0
  24. package/dist/abi/IDelegationManager.json +211 -0
  25. package/dist/abi/IERC7579Module.json +34 -0
  26. package/dist/abi/IERC7579ModuleLifecycle.json +60 -0
  27. package/dist/abi/IGovernanceView.json +34 -0
  28. package/dist/abi/MultiSendCallOnly.json +29 -0
  29. package/dist/abi/MultiSendCallOnlyHarness.json +42 -0
  30. package/dist/abi/OntologyTermRegistry.json +397 -0
  31. package/dist/abi/P256Verifier.json +1 -0
  32. package/dist/abi/PermissionlessSubregistry.json +207 -0
  33. package/dist/abi/RelationshipTypeRegistry.json +455 -0
  34. package/dist/abi/ShapeRegistry.json +627 -0
  35. package/dist/abi/SmartAgentModuleTypes.json +1 -0
  36. package/dist/abi/TimestampEnforcer.json +108 -0
  37. package/dist/abi/ValueEnforcer.json +103 -0
  38. package/dist/abi/WebAuthnLib.json +1 -0
  39. package/dist/abi/index.d.ts +35 -0
  40. package/dist/abi/index.js +35 -0
  41. package/package.json +48 -0
  42. package/spec.md +52 -0
  43. package/src/AgentAccount.sol +1374 -0
  44. package/src/AgentAccountFactory.sol +274 -0
  45. package/src/ApprovedHashRegistry.sol +57 -0
  46. package/src/IAgentAccount.sol +138 -0
  47. package/src/SmartAgentPaymaster.sol +281 -0
  48. package/src/UniversalSignatureValidator.sol +136 -0
  49. package/src/agency/DelegationManager.sol +374 -0
  50. package/src/agency/ICaveatEnforcer.sol +62 -0
  51. package/src/agency/IDelegationManager.sol +69 -0
  52. package/src/custody/CustodyPolicy.sol +892 -0
  53. package/src/custody/IERC7579Module.sol +60 -0
  54. package/src/enforcers/AllowedMethodsEnforcer.AUDIT.md +51 -0
  55. package/src/enforcers/AllowedMethodsEnforcer.sol +48 -0
  56. package/src/enforcers/AllowedTargetsEnforcer.AUDIT.md +49 -0
  57. package/src/enforcers/AllowedTargetsEnforcer.sol +44 -0
  58. package/src/enforcers/CaveatEnforcerBase.sol +19 -0
  59. package/src/enforcers/QuorumEnforcer.AUDIT.md +71 -0
  60. package/src/enforcers/QuorumEnforcer.sol +191 -0
  61. package/src/enforcers/TimestampEnforcer.AUDIT.md +50 -0
  62. package/src/enforcers/TimestampEnforcer.sol +43 -0
  63. package/src/enforcers/ValueEnforcer.AUDIT.md +51 -0
  64. package/src/enforcers/ValueEnforcer.sol +41 -0
  65. package/src/governance/AgenticGovernance.sol +140 -0
  66. package/src/governance/GovernanceManaged.sol +75 -0
  67. package/src/governance/IGovernance.sol +15 -0
  68. package/src/identity/AgentProfilePredicates.sol +40 -0
  69. package/src/identity/AgentProfileResolver.sol +194 -0
  70. package/src/libraries/MultiSendCallOnly.sol +95 -0
  71. package/src/libraries/P256Verifier.sol +47 -0
  72. package/src/libraries/SignatureSlotRecovery.sol +196 -0
  73. package/src/libraries/WebAuthnLib.sol +164 -0
  74. package/src/naming/AgentNameAttributeResolver.sol +95 -0
  75. package/src/naming/AgentNamePredicates.sol +74 -0
  76. package/src/naming/AgentNameRegistry.sol +362 -0
  77. package/src/naming/AgentNameUniversalResolver.sol +210 -0
  78. package/src/naming/PermissionlessSubregistry.sol +98 -0
  79. package/src/ontology/AttributeStorage.sol +289 -0
  80. package/src/ontology/OntologyTermRegistry.sol +146 -0
  81. package/src/ontology/ShapeRegistry.sol +240 -0
  82. package/src/relationships/AgentRelationship.sol +289 -0
  83. package/src/relationships/AgentRelationshipPredicates.sol +44 -0
  84. package/src/relationships/RelationshipTypeRegistry.sol +143 -0
@@ -0,0 +1,281 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ import "account-abstraction/core/BasePaymaster.sol";
5
+ import "account-abstraction/interfaces/IEntryPoint.sol";
6
+ import "account-abstraction/interfaces/PackedUserOperation.sol";
7
+ import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
8
+ import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
9
+ import "./governance/IGovernance.sol";
10
+
11
+ /**
12
+ * @title SmartAgentPaymaster
13
+ * @notice ERC-4337 paymaster (v0.7+ interface, runs against our v0.9 EntryPoint)
14
+ * that sponsors gas at the EntryPoint level so users never need ETH.
15
+ *
16
+ * Validation modes (audit C2 — closed in pass 4):
17
+ * 1. **Dev mode** (`_dev=true`): accept every userOp. Local dev /
18
+ * Anvil only.
19
+ * 2. **Allowlist mode** (`_dev=false` AND `verifyingSigner == address(0)`):
20
+ * only senders in `_acceptList` are sponsored. Useful when the
21
+ * set of accounts is bounded + known up front.
22
+ * 3. **Verifying-paymaster mode** (`_dev=false` AND
23
+ * `verifyingSigner != address(0)`): a designated EOA signs over
24
+ * `(userOp_canonical, validUntil, validAfter)` off-chain;
25
+ * `_validatePaymasterUserOp` recovers the signature and accepts
26
+ * only if it recovers to `verifyingSigner`. Standard production
27
+ * pattern (Pimlico / Stackup / Alchemy reference); avoids the
28
+ * per-sender state of allowlist mode while keeping the paymaster
29
+ * from being drained by arbitrary callers.
30
+ *
31
+ * Wire format of `paymasterAndData` (verifying-paymaster mode):
32
+ * ```
33
+ * [20 bytes paymaster addr]
34
+ * [16 bytes paymasterVerificationGasLimit]
35
+ * [16 bytes paymasterPostOpGasLimit]
36
+ * [6 bytes validUntil (uint48 BE)]
37
+ * [6 bytes validAfter (uint48 BE)]
38
+ * [65 bytes ECDSA signature (r,s,v) over getHash(...)]
39
+ * ```
40
+ *
41
+ * Hash signed off-chain (matches the canonical-paymaster reference):
42
+ * ```
43
+ * keccak256(abi.encode(
44
+ * sender, nonce, keccak256(initCode), keccak256(callData),
45
+ * accountGasLimits, preVerificationGas, gasFees,
46
+ * chainId, address(this), validUntil, validAfter
47
+ * ))
48
+ * ```
49
+ *
50
+ * Note: the signature itself is NOT in the hash (otherwise recursive).
51
+ * The hash deliberately omits `paymasterAndData` from the userOp
52
+ * because the signature lives there.
53
+ *
54
+ * Production checklist:
55
+ * 1. Call `setDevMode(false)` (governance only) to leave dev mode.
56
+ * 2. Call `setVerifyingSigner(<KMS-backed signer addr>)` to enable
57
+ * verifying-paymaster mode (preferred). OR populate `_acceptList`
58
+ * via `setAccepted` if you want allowlist mode.
59
+ * 3. Monitor `getDeposit()` and alert below a runway threshold.
60
+ *
61
+ * @dev Inherits `addStake`, `unlockStake`, `withdrawStake`, `deposit`,
62
+ * and `withdrawTo` from `BasePaymaster`. Ownable owner is set in
63
+ * the constructor (Ownable2Step pattern).
64
+ */
65
+ contract SmartAgentPaymaster is BasePaymaster {
66
+ using ECDSA for bytes32;
67
+ using MessageHashUtils for bytes32;
68
+
69
+ /// @notice Whether the paymaster is in dev (accept-all) mode.
70
+ bool private _dev;
71
+
72
+ /// @notice Per-sender allow-list for production allowlist mode.
73
+ mapping(address => bool) private _acceptList;
74
+
75
+ /// @notice EOA that signs paymaster-validation envelopes off-chain.
76
+ /// `address(0)` disables verifying mode (allowlist becomes
77
+ /// the production check). Set via governance.
78
+ address public verifyingSigner;
79
+
80
+ /// @notice The Governance contract whose pause flag halts paymaster
81
+ /// validation. Stored immutable.
82
+ address public immutable governance;
83
+
84
+ error SenderNotAccepted(address sender);
85
+ error SystemPaused();
86
+ error ZeroGovernance();
87
+ error NotGovernance();
88
+ error PaymasterDataMalformed();
89
+ error PaymasterSignatureInvalid();
90
+
91
+ event DevModeSet(bool dev);
92
+ event SenderAcceptedSet(address indexed sender, bool accepted);
93
+ event VerifyingSignerSet(address indexed oldSigner, address indexed newSigner);
94
+
95
+ /// @dev Storage gap reserves slots for future state. Phase A.5 §3.1.
96
+ uint256[49] private __gap;
97
+
98
+ /// @dev Length of the paymaster-data tail when in verifying mode:
99
+ /// 6 (validUntil) + 6 (validAfter) + 65 (sig) = 77 bytes.
100
+ uint256 private constant VERIFYING_PAYMASTER_DATA_LEN = 77;
101
+ /// @dev Offset in `paymasterAndData` where the post-prefix payload
102
+ /// begins: 20 (paymaster addr) + 16 (verifGas) + 16 (postOpGas).
103
+ uint256 private constant PM_DATA_OFFSET = 52;
104
+
105
+ constructor(
106
+ IEntryPoint entryPointAddr,
107
+ address initialOwner,
108
+ address governance_
109
+ ) BasePaymaster(entryPointAddr, initialOwner) {
110
+ if (governance_ == address(0)) revert ZeroGovernance();
111
+ governance = governance_;
112
+ _dev = true;
113
+ emit DevModeSet(true);
114
+ }
115
+
116
+ // ─── Admin (governance-only) ────────────────────────────────────────
117
+
118
+ modifier onlyGovernance() {
119
+ if (msg.sender != governance) revert NotGovernance();
120
+ _;
121
+ }
122
+
123
+ function setDevMode(bool dev) external onlyGovernance {
124
+ _dev = dev;
125
+ emit DevModeSet(dev);
126
+ }
127
+
128
+ function setAccepted(address sender, bool accepted) external onlyGovernance {
129
+ _acceptList[sender] = accepted;
130
+ emit SenderAcceptedSet(sender, accepted);
131
+ }
132
+
133
+ function setAcceptedBatch(address[] calldata senders, bool accepted) external onlyGovernance {
134
+ for (uint256 i = 0; i < senders.length; i++) {
135
+ _acceptList[senders[i]] = accepted;
136
+ emit SenderAcceptedSet(senders[i], accepted);
137
+ }
138
+ }
139
+
140
+ /// @notice Set the EOA that signs paymaster-validation envelopes.
141
+ /// Pass `address(0)` to disable verifying mode + fall back
142
+ /// to allowlist (when `_dev=false`).
143
+ function setVerifyingSigner(address newSigner) external onlyGovernance {
144
+ address old = verifyingSigner;
145
+ verifyingSigner = newSigner;
146
+ emit VerifyingSignerSet(old, newSigner);
147
+ }
148
+
149
+ // ─── Views ──────────────────────────────────────────────────────────
150
+
151
+ function devMode() external view returns (bool) {
152
+ return _dev;
153
+ }
154
+
155
+ function isAccepted(address sender) external view returns (bool) {
156
+ return _acceptList[sender];
157
+ }
158
+
159
+ /// @notice The canonical hash a verifying signer must sign.
160
+ /// Off-chain callers compute the same hash + sign via
161
+ /// EIP-191 ("\x19Ethereum Signed Message:\n32" prefix);
162
+ /// on-chain validation recovers via that wrapper.
163
+ /// @dev Deliberately omits paymasterAndData (the signature lives
164
+ /// there) and the userOp.signature (the account signs
165
+ /// independently). Includes chainId + paymaster address for
166
+ /// replay protection across chains + deployments.
167
+ function getHash(
168
+ PackedUserOperation calldata userOp,
169
+ uint48 validUntil,
170
+ uint48 validAfter
171
+ ) public view returns (bytes32) {
172
+ // H7-C.7 / CON-PAYMASTER-004: bind `address(entryPoint)` into the
173
+ // signed material so a signed envelope cannot survive an EntryPoint
174
+ // redeploy. Pre-fix, the hash omitted the EntryPoint — a long-lived
175
+ // signed envelope (validUntil in the future) issued against the
176
+ // current EntryPoint would have been verifiable against a NEW
177
+ // EntryPoint deployed at a different address, allowing cross-deployment
178
+ // replay until validUntil elapses.
179
+ return keccak256(
180
+ abi.encode(
181
+ userOp.sender,
182
+ userOp.nonce,
183
+ keccak256(userOp.initCode),
184
+ keccak256(userOp.callData),
185
+ userOp.accountGasLimits,
186
+ userOp.preVerificationGas,
187
+ userOp.gasFees,
188
+ block.chainid,
189
+ address(this),
190
+ address(entryPoint()), // H7-C.7 binding
191
+ validUntil,
192
+ validAfter
193
+ )
194
+ );
195
+ }
196
+
197
+ // ─── Paymaster hook ────────────────────────────────────────────────
198
+
199
+ /// @inheritdoc BasePaymaster
200
+ function _validatePaymasterUserOp(
201
+ PackedUserOperation calldata userOp,
202
+ bytes32 /*userOpHash*/,
203
+ uint256 /*maxCost*/
204
+ ) internal view override returns (bytes memory context, uint256 validationData) {
205
+ // H7-C.10 / EXT3-010: gated behind system-wide governance pause.
206
+ // Skipped when `governance` is an EOA or non-conforming contract
207
+ // (legacy / test deploys); production deploys MUST pass an
208
+ // AgenticGovernance address, which the production-deploy preflight
209
+ // (`check:production-deploy`) enforces.
210
+ if (governance.code.length > 0) {
211
+ (bool ok, bytes memory data) = governance.staticcall(
212
+ abi.encodeWithSelector(IGovernanceView.isPaused.selector)
213
+ );
214
+ if (ok && data.length >= 32 && abi.decode(data, (bool))) revert SystemPaused();
215
+ }
216
+
217
+ if (_dev) {
218
+ // Dev mode: accept all. validationData = 0 → "valid sig,
219
+ // valid indefinitely".
220
+ return ("", 0);
221
+ }
222
+
223
+ // Production. Prefer verifying-paymaster when a signer is
224
+ // configured; fall back to the legacy allowlist otherwise.
225
+ if (verifyingSigner != address(0)) {
226
+ // Parse paymasterData tail.
227
+ if (userOp.paymasterAndData.length < PM_DATA_OFFSET + VERIFYING_PAYMASTER_DATA_LEN) {
228
+ revert PaymasterDataMalformed();
229
+ }
230
+ bytes calldata payData = userOp.paymasterAndData[PM_DATA_OFFSET:];
231
+ uint48 validUntil = uint48(bytes6(payData[0:6]));
232
+ uint48 validAfter = uint48(bytes6(payData[6:12]));
233
+ bytes calldata signature = payData[12:VERIFYING_PAYMASTER_DATA_LEN];
234
+
235
+ bytes32 hash = getHash(userOp, validUntil, validAfter);
236
+ // EIP-191 wrap matches what KMS-backed `signMessage({raw})`
237
+ // produces via the v0.7 reference paymaster convention.
238
+ bytes32 ethHash = hash.toEthSignedMessageHash();
239
+ (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(ethHash, signature);
240
+ if (err != ECDSA.RecoverError.NoError || recovered != verifyingSigner) {
241
+ // Returning sigFailed=true (per EntryPoint convention)
242
+ // is the canonical way to signal sig invalid; we revert
243
+ // explicitly for a clearer error in tests + tools.
244
+ revert PaymasterSignatureInvalid();
245
+ }
246
+ // Encode (sigFailed=false, validUntil, validAfter) into validationData.
247
+ return ("", _packValidationData(false, validUntil, validAfter));
248
+ }
249
+
250
+ // Fallback: allowlist mode.
251
+ if (!_acceptList[userOp.sender]) revert SenderNotAccepted(userOp.sender);
252
+ return ("", 0);
253
+ }
254
+
255
+ /// @dev v0.7 EntryPoint convention. Bits 0: sigFailed,
256
+ /// [1..49]: validUntil, [50..98]: validAfter (or similar).
257
+ /// We use the simple packing: aggregator addr (160 bits, 0),
258
+ /// validUntil (48 bits), validAfter (48 bits).
259
+ function _packValidationData(
260
+ bool sigFailed,
261
+ uint48 validUntil,
262
+ uint48 validAfter
263
+ ) internal pure returns (uint256) {
264
+ return
265
+ (sigFailed ? 1 : 0) |
266
+ (uint256(validUntil) << 160) |
267
+ (uint256(validAfter) << (160 + 48));
268
+ }
269
+
270
+ /// @inheritdoc BasePaymaster
271
+ /// @dev No per-call accounting. No-op so EntryPoint can safely
272
+ /// call us if it ever does (it won't — we return empty context).
273
+ function _postOp(
274
+ PostOpMode /*mode*/,
275
+ bytes calldata /*context*/,
276
+ uint256 /*actualGasCost*/,
277
+ uint256 /*actualUserOpFeePerGas*/
278
+ ) internal pure override {
279
+ // intentionally empty
280
+ }
281
+ }
@@ -0,0 +1,136 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ import "@openzeppelin/contracts/interfaces/IERC1271.sol";
5
+ import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
6
+ import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
7
+
8
+ /**
9
+ * @title UniversalSignatureValidator
10
+ * @notice Universal ERC-1271 / ERC-6492 / ECDSA signature verifier.
11
+ *
12
+ * Accepts three signature shapes — the caller never needs to know which:
13
+ * 1. Plain EOA signature (65 bytes) — ECDSA.recover, supports both raw
14
+ * and eth-signed-message hash forms.
15
+ * 2. ERC-1271 signature — `signer` already has code; the verifier
16
+ * invokes `IERC1271(signer).isValidSignature(hash, sig)` and checks
17
+ * for the magic value (0x1626ba7e).
18
+ * 3. ERC-6492 signature — `sig` ends with the 32-byte magic
19
+ * 0x6492…6492. The prefix is `abi.encode(factory, factoryCalldata,
20
+ * innerSig)`. If `signer` has no code, the verifier deploys it via
21
+ * `factory.call(factoryCalldata)` and then recurses into ERC-1271
22
+ * verification with `innerSig`.
23
+ *
24
+ * Matches the reference `UniversalSigValidator` in the ERC-6492 spec.
25
+ * Ported from smart-agent `packages/contracts/src/UniversalSignatureValidator.sol`
26
+ * (branch 003-intent-marketplace-proposal); kept byte-compatible.
27
+ *
28
+ * Two entry points:
29
+ * - `isValidSig` — state-changing; the 6492 path may deploy.
30
+ * - `isValidSigView` — view-only; safe to call from static contexts but
31
+ * cannot perform 6492 deploys (returns false in
32
+ * that case unless the account already has code).
33
+ *
34
+ * Doctrine: per spec 130 §7 and feedback memory
35
+ * "demo-a2a is signer-agnostic", the demo-a2a server verifies user
36
+ * signatures by calling THIS contract, never by parsing the signature
37
+ * bytes itself. Passkey vs EOA vs anything else is decided here on-chain.
38
+ */
39
+ contract UniversalSignatureValidator {
40
+ using ECDSA for bytes32;
41
+ using MessageHashUtils for bytes32;
42
+
43
+ bytes4 private constant ERC1271_MAGIC = 0x1626ba7e;
44
+ /// @dev 32-byte ERC-6492 magic suffix — `0x6492…6492` repeated.
45
+ bytes32 private constant ERC6492_MAGIC =
46
+ 0x6492649264926492649264926492649264926492649264926492649264926492;
47
+
48
+ error DeployFailed();
49
+
50
+ /// @notice State-changing verifier: counterfactually deploys the
51
+ /// signer's account if it isn't deployed yet, then validates
52
+ /// via ERC-1271. Use from a call that can mutate state
53
+ /// (relayer pre-flight, demo-a2a's siwe-verify endpoint).
54
+ function isValidSig(
55
+ address signer,
56
+ bytes32 hash,
57
+ bytes calldata sig
58
+ ) external returns (bool) {
59
+ if (_has6492Magic(sig)) {
60
+ (address factory, bytes memory factoryCalldata, bytes memory innerSig) =
61
+ _decode6492(sig[:sig.length - 32]);
62
+ if (signer.code.length == 0) {
63
+ (bool ok, ) = factory.call(factoryCalldata);
64
+ if (!ok || signer.code.length == 0) revert DeployFailed();
65
+ }
66
+ return _erc1271(signer, hash, innerSig);
67
+ }
68
+ if (signer.code.length > 0) {
69
+ return _erc1271(signer, hash, sig);
70
+ }
71
+ return _ecdsaRecover(signer, hash, sig);
72
+ }
73
+
74
+ /// @notice View-only verifier — skips 6492 deploy (cannot mutate
75
+ /// state). Returns false if a 6492-wrapped sig is presented
76
+ /// and the account isn't already deployed.
77
+ function isValidSigView(
78
+ address signer,
79
+ bytes32 hash,
80
+ bytes calldata sig
81
+ ) external view returns (bool) {
82
+ if (_has6492Magic(sig)) {
83
+ (, , bytes memory innerSig) = _decode6492(sig[:sig.length - 32]);
84
+ if (signer.code.length == 0) return false;
85
+ return _erc1271(signer, hash, innerSig);
86
+ }
87
+ if (signer.code.length > 0) {
88
+ return _erc1271(signer, hash, sig);
89
+ }
90
+ return _ecdsaRecover(signer, hash, sig);
91
+ }
92
+
93
+ // ─── Internals ───────────────────────────────────────────────────
94
+
95
+ function _has6492Magic(bytes calldata sig) private pure returns (bool) {
96
+ if (sig.length < 32) return false;
97
+ return bytes32(sig[sig.length - 32:]) == ERC6492_MAGIC;
98
+ }
99
+
100
+ function _decode6492(bytes calldata prefix)
101
+ private
102
+ pure
103
+ returns (address factory, bytes memory factoryCalldata, bytes memory innerSig)
104
+ {
105
+ (factory, factoryCalldata, innerSig) =
106
+ abi.decode(prefix, (address, bytes, bytes));
107
+ }
108
+
109
+ function _erc1271(
110
+ address signer,
111
+ bytes32 hash,
112
+ bytes memory sig
113
+ ) private view returns (bool) {
114
+ try IERC1271(signer).isValidSignature(hash, sig) returns (bytes4 mv) {
115
+ return mv == ERC1271_MAGIC;
116
+ } catch {
117
+ return false;
118
+ }
119
+ }
120
+
121
+ function _ecdsaRecover(
122
+ address signer,
123
+ bytes32 hash,
124
+ bytes memory sig
125
+ ) private pure returns (bool) {
126
+ if (sig.length != 65) return false;
127
+ // Try raw hash first.
128
+ (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, sig);
129
+ if (err == ECDSA.RecoverError.NoError && recovered == signer) return true;
130
+ // Then try the eth-signed prefix variant — many wallets sign this
131
+ // form even when the caller passes a raw digest.
132
+ bytes32 prefixed = hash.toEthSignedMessageHash();
133
+ (recovered, err, ) = ECDSA.tryRecover(prefixed, sig);
134
+ return err == ECDSA.RecoverError.NoError && recovered == signer;
135
+ }
136
+ }