@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.
- package/AUDIT.md +67 -0
- package/CLAUDE.md +40 -0
- package/LICENSE +21 -0
- package/README.md +45 -0
- package/deployments-anvil.json +1 -0
- package/deployments-base-sepolia.json +1 -0
- package/dist/abi/AgentNameAttributeResolver.json +798 -0
- package/dist/abi/AgentNamePredicates.json +1 -0
- package/dist/abi/AgentNameRegistry.json +826 -0
- package/dist/abi/AgentNameUniversalResolver.json +222 -0
- package/dist/abi/AgentProfilePredicates.json +1 -0
- package/dist/abi/AgentProfileResolver.json +1044 -0
- package/dist/abi/AgentRelationship.json +583 -0
- package/dist/abi/AgentRelationshipPredicates.json +1 -0
- package/dist/abi/AgenticGovernance.json +259 -0
- package/dist/abi/AllowedMethodsEnforcer.json +108 -0
- package/dist/abi/AllowedTargetsEnforcer.json +103 -0
- package/dist/abi/ApprovedHashRegistry.json +114 -0
- package/dist/abi/AttributeStorage.json +557 -0
- package/dist/abi/CaveatEnforcerBase.json +130 -0
- package/dist/abi/GovernanceManaged.json +43 -0
- package/dist/abi/IAttributeReader.json +98 -0
- package/dist/abi/ICaveatEnforcer.json +98 -0
- package/dist/abi/IDelegationManager.json +211 -0
- package/dist/abi/IERC7579Module.json +34 -0
- package/dist/abi/IERC7579ModuleLifecycle.json +60 -0
- package/dist/abi/IGovernanceView.json +34 -0
- package/dist/abi/MultiSendCallOnly.json +29 -0
- package/dist/abi/MultiSendCallOnlyHarness.json +42 -0
- package/dist/abi/OntologyTermRegistry.json +397 -0
- package/dist/abi/P256Verifier.json +1 -0
- package/dist/abi/PermissionlessSubregistry.json +207 -0
- package/dist/abi/RelationshipTypeRegistry.json +455 -0
- package/dist/abi/ShapeRegistry.json +627 -0
- package/dist/abi/SmartAgentModuleTypes.json +1 -0
- package/dist/abi/TimestampEnforcer.json +108 -0
- package/dist/abi/ValueEnforcer.json +103 -0
- package/dist/abi/WebAuthnLib.json +1 -0
- package/dist/abi/index.d.ts +35 -0
- package/dist/abi/index.js +35 -0
- package/package.json +48 -0
- package/spec.md +52 -0
- package/src/AgentAccount.sol +1374 -0
- package/src/AgentAccountFactory.sol +274 -0
- package/src/ApprovedHashRegistry.sol +57 -0
- package/src/IAgentAccount.sol +138 -0
- package/src/SmartAgentPaymaster.sol +281 -0
- package/src/UniversalSignatureValidator.sol +136 -0
- package/src/agency/DelegationManager.sol +374 -0
- package/src/agency/ICaveatEnforcer.sol +62 -0
- package/src/agency/IDelegationManager.sol +69 -0
- package/src/custody/CustodyPolicy.sol +892 -0
- package/src/custody/IERC7579Module.sol +60 -0
- package/src/enforcers/AllowedMethodsEnforcer.AUDIT.md +51 -0
- package/src/enforcers/AllowedMethodsEnforcer.sol +48 -0
- package/src/enforcers/AllowedTargetsEnforcer.AUDIT.md +49 -0
- package/src/enforcers/AllowedTargetsEnforcer.sol +44 -0
- package/src/enforcers/CaveatEnforcerBase.sol +19 -0
- package/src/enforcers/QuorumEnforcer.AUDIT.md +71 -0
- package/src/enforcers/QuorumEnforcer.sol +191 -0
- package/src/enforcers/TimestampEnforcer.AUDIT.md +50 -0
- package/src/enforcers/TimestampEnforcer.sol +43 -0
- package/src/enforcers/ValueEnforcer.AUDIT.md +51 -0
- package/src/enforcers/ValueEnforcer.sol +41 -0
- package/src/governance/AgenticGovernance.sol +140 -0
- package/src/governance/GovernanceManaged.sol +75 -0
- package/src/governance/IGovernance.sol +15 -0
- package/src/identity/AgentProfilePredicates.sol +40 -0
- package/src/identity/AgentProfileResolver.sol +194 -0
- package/src/libraries/MultiSendCallOnly.sol +95 -0
- package/src/libraries/P256Verifier.sol +47 -0
- package/src/libraries/SignatureSlotRecovery.sol +196 -0
- package/src/libraries/WebAuthnLib.sol +164 -0
- package/src/naming/AgentNameAttributeResolver.sol +95 -0
- package/src/naming/AgentNamePredicates.sol +74 -0
- package/src/naming/AgentNameRegistry.sol +362 -0
- package/src/naming/AgentNameUniversalResolver.sol +210 -0
- package/src/naming/PermissionlessSubregistry.sol +98 -0
- package/src/ontology/AttributeStorage.sol +289 -0
- package/src/ontology/OntologyTermRegistry.sol +146 -0
- package/src/ontology/ShapeRegistry.sol +240 -0
- package/src/relationships/AgentRelationship.sol +289 -0
- package/src/relationships/AgentRelationshipPredicates.sol +44 -0
- 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
|
+
}
|