@agenticprimitives/contracts 0.1.0-alpha.2 → 1.0.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUDIT.md +324 -47
- package/README.md +45 -10
- package/deployments-base-sepolia.json +36 -1
- package/deployments-registries-base-sepolia.json +1 -0
- package/dist/abi/AgentAccount.json +1670 -0
- package/dist/abi/AgentAccountFactory.json +477 -0
- package/dist/abi/AgentNameRegistry.json +51 -0
- package/dist/abi/AgenticGovernance.json +32 -0
- package/dist/abi/AgreementRegistry.json +427 -0
- package/dist/abi/AttestationRegistry.json +436 -0
- package/dist/abi/CallDataHashEnforcer.json +119 -0
- package/dist/abi/CustodyPolicy.json +1122 -0
- package/dist/abi/DelegationManager.json +645 -0
- package/dist/abi/GeoFeatureRegistry.json +625 -0
- package/dist/abi/IAgentAccount.json +262 -0
- package/dist/abi/IAgentAccountFactoryAccessor.json +15 -0
- package/dist/abi/IAgentAccountFactoryView.json +41 -0
- package/dist/abi/IAgentAccountPauseView.json +15 -0
- package/dist/abi/IAgenticPrimitivesAgentAccount.json +15 -0
- package/dist/abi/IApprovedHashRegistryView.json +26 -0
- package/dist/abi/ICustodyPolicyFactoryView.json +15 -0
- package/dist/abi/ICustodyPolicyPauseView.json +15 -0
- package/dist/abi/IDelegationManager.json +171 -0
- package/dist/abi/IERC7579HookLike.json +44 -0
- package/dist/abi/IERC7579ModuleLike.json +28 -0
- package/dist/abi/IPaymentReceiptRegistry.json +50 -0
- package/dist/abi/MockUSDC.json +621 -0
- package/dist/abi/PaymentEnforcer.json +407 -0
- package/dist/abi/PaymentEscrow.json +299 -0
- package/dist/abi/PaymentReceiptRegistry.json +182 -0
- package/dist/abi/PermissionlessSubregistry.json +5 -0
- package/dist/abi/QuorumEnforcer.json +311 -0
- package/dist/abi/SignatureSlotRecovery.json +88 -0
- package/dist/abi/SkillDefinitionRegistry.json +491 -0
- package/dist/abi/SmartAgentPaymaster.json +873 -0
- package/dist/abi/UniversalSignatureValidator.json +65 -0
- package/dist/abi/index.d.ts +28 -0
- package/dist/abi/index.js +28 -0
- package/dist/deployments/base-sepolia.d.ts +41 -0
- package/dist/deployments/base-sepolia.js +39 -0
- package/dist/deployments/index.d.ts +9 -0
- package/dist/deployments/index.js +16 -0
- package/dist/deployments/registries-base-sepolia.d.ts +9 -0
- package/dist/deployments/registries-base-sepolia.js +7 -0
- package/package.json +28 -7
- package/src/AgentAccount.sol +191 -11
- package/src/AgentAccountFactory.sol +66 -12
- package/src/SmartAgentPaymaster.sol +186 -20
- package/src/UniversalSignatureValidator.sol +3 -0
- package/src/agency/DelegationManager.sol +170 -0
- package/src/agency/IDelegationManager.sol +38 -0
- package/src/agreement/AgreementRegistry.sol +331 -0
- package/src/attestation/AttestationRegistry.sol +384 -0
- package/src/custody/CustodyPolicy.sol +138 -8
- package/src/enforcers/AllowedMethodsEnforcer.sol +6 -0
- package/src/enforcers/AllowedTargetsEnforcer.sol +6 -0
- package/src/enforcers/CallDataHashEnforcer.sol +61 -0
- package/src/enforcers/PaymentEnforcer.sol +194 -0
- package/src/enforcers/QuorumEnforcer.sol +20 -0
- package/src/enforcers/TimestampEnforcer.sol +7 -0
- package/src/enforcers/ValueEnforcer.sol +6 -0
- package/src/geo/GeoFeatureRegistry.sol +197 -0
- package/src/governance/AgenticGovernance.sol +26 -4
- package/src/libraries/SignatureSlotRecovery.sol +8 -3
- package/src/libraries/WebAuthnLib.sol +13 -0
- package/src/mocks/MockUSDC.sol +152 -0
- package/src/naming/AgentNameRegistry.sol +52 -15
- package/src/naming/AgentNameUniversalResolver.sol +1 -1
- package/src/naming/PermissionlessSubregistry.sol +19 -2
- package/src/payments/PaymentEscrow.sol +137 -0
- package/src/payments/PaymentReceiptRegistry.sol +67 -0
- package/src/relationships/AgentRelationship.sol +8 -0
- package/src/skills/SkillDefinitionRegistry.sol +187 -0
- package/deployments-anvil.json +0 -1
|
@@ -51,12 +51,40 @@ import "./governance/IGovernance.sol";
|
|
|
51
51
|
* The hash deliberately omits `paymasterAndData` from the userOp
|
|
52
52
|
* because the signature lives there.
|
|
53
53
|
*
|
|
54
|
+
* Construction modes (R5.7 / PKG-PAYMASTER-002 closure — external audit P0-2):
|
|
55
|
+
* The constructor takes `bool devMode_` + `address verifyingSigner_`
|
|
56
|
+
* EXPLICITLY. There is no implicit fail-open default; pre-R5.7 the
|
|
57
|
+
* constructor forcibly set `_dev = true` and production deploys had to
|
|
58
|
+
* remember to call `setDevMode(false) + setVerifyingSigner(...)` AFTER
|
|
59
|
+
* the broadcast, which left a window where the paymaster would sponsor
|
|
60
|
+
* any userOp on the freshly-deployed network. Now: testnet deploys pass
|
|
61
|
+
* `devMode_=true`; production deploys pass `devMode_=false` with a
|
|
62
|
+
* verifying signer (or allowlist seed). See `Deploy.s.sol`.
|
|
63
|
+
*
|
|
54
64
|
* Production checklist:
|
|
55
|
-
* 1.
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
65
|
+
* 1. Construct with `devMode_=false` AND either:
|
|
66
|
+
* - a non-zero `verifyingSigner_` for verifying-paymaster mode
|
|
67
|
+
* (preferred — Pimlico/Stackup/Alchemy pattern), OR
|
|
68
|
+
* - `verifyingSigner_=address(0)` to start in allowlist mode (no
|
|
69
|
+
* sender is sponsored until `setAccepted` runs). The fall-back
|
|
70
|
+
* to allowlist is fail-closed: every userOp reverts with
|
|
71
|
+
* `SenderNotAccepted` until governance explicitly opts a sender
|
|
72
|
+
* in. That is the documented safe state.
|
|
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.
|
|
60
88
|
*
|
|
61
89
|
* @dev Inherits `addStake`, `unlockStake`, `withdrawStake`, `deposit`,
|
|
62
90
|
* and `withdrawTo` from `BasePaymaster`. Ownable owner is set in
|
|
@@ -81,19 +109,62 @@ contract SmartAgentPaymaster is BasePaymaster {
|
|
|
81
109
|
/// validation. Stored immutable.
|
|
82
110
|
address public immutable governance;
|
|
83
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
|
+
|
|
84
142
|
error SenderNotAccepted(address sender);
|
|
85
143
|
error SystemPaused();
|
|
86
144
|
error ZeroGovernance();
|
|
87
145
|
error NotGovernance();
|
|
88
146
|
error PaymasterDataMalformed();
|
|
89
147
|
error PaymasterSignatureInvalid();
|
|
148
|
+
/// @dev PM-2 withdrawal-lifecycle errors.
|
|
149
|
+
error NoPendingWithdrawal();
|
|
150
|
+
error WithdrawalNotReady(uint64 eta);
|
|
151
|
+
error WithdrawalAmountZero();
|
|
152
|
+
error WithdrawalToZero();
|
|
90
153
|
|
|
91
154
|
event DevModeSet(bool dev);
|
|
92
155
|
event SenderAcceptedSet(address indexed sender, bool accepted);
|
|
93
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);
|
|
94
163
|
|
|
95
164
|
/// @dev Storage gap reserves slots for future state. Phase A.5 §3.1.
|
|
96
|
-
|
|
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;
|
|
97
168
|
|
|
98
169
|
/// @dev Length of the paymaster-data tail when in verifying mode:
|
|
99
170
|
/// 6 (validUntil) + 6 (validAfter) + 65 (sig) = 77 bytes.
|
|
@@ -102,15 +173,28 @@ contract SmartAgentPaymaster is BasePaymaster {
|
|
|
102
173
|
/// begins: 20 (paymaster addr) + 16 (verifGas) + 16 (postOpGas).
|
|
103
174
|
uint256 private constant PM_DATA_OFFSET = 52;
|
|
104
175
|
|
|
176
|
+
/// @param devMode_ true → accept-all (dev/anvil); false → require
|
|
177
|
+
/// verifying-signer or allowlist. R5.7 removed
|
|
178
|
+
/// the implicit fail-open default.
|
|
179
|
+
/// @param verifyingSigner_ EOA that signs paymaster envelopes when
|
|
180
|
+
/// `devMode_=false`. Pass `address(0)` to start
|
|
181
|
+
/// in allowlist mode (fail-closed until
|
|
182
|
+
/// `setAccepted` runs).
|
|
105
183
|
constructor(
|
|
106
184
|
IEntryPoint entryPointAddr,
|
|
107
185
|
address initialOwner,
|
|
108
|
-
address governance_
|
|
186
|
+
address governance_,
|
|
187
|
+
bool devMode_,
|
|
188
|
+
address verifyingSigner_
|
|
109
189
|
) BasePaymaster(entryPointAddr, initialOwner) {
|
|
110
190
|
if (governance_ == address(0)) revert ZeroGovernance();
|
|
111
191
|
governance = governance_;
|
|
112
|
-
_dev =
|
|
113
|
-
emit DevModeSet(
|
|
192
|
+
_dev = devMode_;
|
|
193
|
+
emit DevModeSet(devMode_);
|
|
194
|
+
if (verifyingSigner_ != address(0)) {
|
|
195
|
+
verifyingSigner = verifyingSigner_;
|
|
196
|
+
emit VerifyingSignerSet(address(0), verifyingSigner_);
|
|
197
|
+
}
|
|
114
198
|
}
|
|
115
199
|
|
|
116
200
|
// ─── Admin (governance-only) ────────────────────────────────────────
|
|
@@ -146,6 +230,90 @@ contract SmartAgentPaymaster is BasePaymaster {
|
|
|
146
230
|
emit VerifyingSignerSet(old, newSigner);
|
|
147
231
|
}
|
|
148
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
|
+
|
|
149
317
|
// ─── Views ──────────────────────────────────────────────────────────
|
|
150
318
|
|
|
151
319
|
function devMode() external view returns (bool) {
|
|
@@ -202,17 +370,13 @@ contract SmartAgentPaymaster is BasePaymaster {
|
|
|
202
370
|
bytes32 /*userOpHash*/,
|
|
203
371
|
uint256 /*maxCost*/
|
|
204
372
|
) internal view override returns (bytes memory context, uint256 validationData) {
|
|
205
|
-
// H7-C.10 / EXT3-010: gated behind system-wide governance
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
abi.encodeWithSelector(IGovernanceView.isPaused.selector)
|
|
213
|
-
);
|
|
214
|
-
if (ok && data.length >= 32 && abi.decode(data, (bool))) revert SystemPaused();
|
|
215
|
-
}
|
|
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();
|
|
216
380
|
|
|
217
381
|
if (_dev) {
|
|
218
382
|
// Dev mode: accept all. validationData = 0 → "valid sig,
|
|
@@ -236,6 +400,8 @@ contract SmartAgentPaymaster is BasePaymaster {
|
|
|
236
400
|
// EIP-191 wrap matches what KMS-backed `signMessage({raw})`
|
|
237
401
|
// produces via the v0.7 reference paymaster convention.
|
|
238
402
|
bytes32 ethHash = hash.toEthSignedMessageHash();
|
|
403
|
+
// R6.3: third return (sigVersion) intentionally discarded; the explicit `err` + `recovered != verifyingSigner` revert IS the auth.
|
|
404
|
+
// slither-disable-next-line unused-return
|
|
239
405
|
(address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(ethHash, signature);
|
|
240
406
|
if (err != ECDSA.RecoverError.NoError || recovered != verifyingSigner) {
|
|
241
407
|
// Returning sigFailed=true (per EntryPoint convention)
|
|
@@ -125,11 +125,14 @@ contract UniversalSignatureValidator {
|
|
|
125
125
|
) private pure returns (bool) {
|
|
126
126
|
if (sig.length != 65) return false;
|
|
127
127
|
// Try raw hash first.
|
|
128
|
+
// R6.3: third return (sigVersion) intentionally discarded; the `err` + `recovered == signer` is the auth.
|
|
129
|
+
// slither-disable-next-line unused-return
|
|
128
130
|
(address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, sig);
|
|
129
131
|
if (err == ECDSA.RecoverError.NoError && recovered == signer) return true;
|
|
130
132
|
// Then try the eth-signed prefix variant — many wallets sign this
|
|
131
133
|
// form even when the caller passes a raw digest.
|
|
132
134
|
bytes32 prefixed = hash.toEthSignedMessageHash();
|
|
135
|
+
// slither-disable-next-line unused-return
|
|
133
136
|
(recovered, err, ) = ECDSA.tryRecover(prefixed, sig);
|
|
134
137
|
return err == ECDSA.RecoverError.NoError && recovered == signer;
|
|
135
138
|
}
|
|
@@ -86,6 +86,11 @@ contract DelegationManager is IDelegationManager, ReentrancyGuard {
|
|
|
86
86
|
"Caveat(address enforcer,bytes terms)"
|
|
87
87
|
);
|
|
88
88
|
|
|
89
|
+
/// @dev Gas cap for the read-only caveat evaluation in `verifyAuthorizationForCall`.
|
|
90
|
+
/// Generous for any pure/view enforcer; bounds the gas a stateful enforcer would
|
|
91
|
+
/// burn before reverting under staticcall, so on-chain callers get a cheap `false`.
|
|
92
|
+
uint256 private constant CAVEAT_VIEW_GAS = 1_000_000;
|
|
93
|
+
|
|
89
94
|
/// @dev Revoked delegation hashes
|
|
90
95
|
mapping(bytes32 => bool) private _revoked;
|
|
91
96
|
|
|
@@ -242,6 +247,171 @@ contract DelegationManager is IDelegationManager, ReentrancyGuard {
|
|
|
242
247
|
return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash));
|
|
243
248
|
}
|
|
244
249
|
|
|
250
|
+
/// @notice View-only verification of a delegation chain. Returns whether
|
|
251
|
+
/// the chain is well-formed (signatures, authority chain, not
|
|
252
|
+
/// revoked) WITHOUT executing it.
|
|
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
|
+
///
|
|
261
|
+
/// Spec 242 §6 (PD-9): the `attestations` package uses this to verify
|
|
262
|
+
/// bilateral-consent delegations as signed authorization predicates,
|
|
263
|
+
/// rather than redeeming them as cross-account execution. Returning
|
|
264
|
+
/// `(true, "")` means the delegation chain authorizes the named call from
|
|
265
|
+
/// `sender`. Caveat predicates (CalldataHashEnforcer, AllowedTargets,
|
|
266
|
+
/// AllowedMethods, Timestamp) are NOT executed here — callers are expected
|
|
267
|
+
/// to pin those into the delegation's caveats at issuance time, where
|
|
268
|
+
/// they're hash-bound to the delegation signature.
|
|
269
|
+
///
|
|
270
|
+
/// @param delegations Chain (leaf first; root authority last)
|
|
271
|
+
/// @param sender The address that would be the redeemer
|
|
272
|
+
/// @return ok True if every link verifies
|
|
273
|
+
/// @return reason Empty if ok; otherwise a short rejection reason
|
|
274
|
+
function verifyAuthorization(
|
|
275
|
+
Delegation[] calldata delegations,
|
|
276
|
+
address sender
|
|
277
|
+
) external view returns (bool ok, string memory reason) {
|
|
278
|
+
if (delegations.length == 0) return (false, "empty-chain");
|
|
279
|
+
|
|
280
|
+
for (uint256 i = 0; i < delegations.length; i++) {
|
|
281
|
+
(bool valid, string memory r) = _validateDelegationView(delegations, i, sender);
|
|
282
|
+
if (!valid) return (false, r);
|
|
283
|
+
}
|
|
284
|
+
return (true, "");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/// @notice View verifier for a SPECIFIC call (ADR-0027 / spec 249, RW1-4). Validates the
|
|
288
|
+
/// delegation chain AND evaluates every caveat's `beforeHook` against the exact
|
|
289
|
+
/// `(target, value, data)` — read-only, FAIL-CLOSED.
|
|
290
|
+
/// @dev `verifyAuthorization` checks chain/signature/authority/revocation but NOT the
|
|
291
|
+
/// caveats — too weak to authorize a specific call. This evaluates each caveat via
|
|
292
|
+
/// `staticcall` (the hook is `external`/non-view because stateful enforcers like
|
|
293
|
+
/// Quorum exist). Returns `true` ONLY if the chain validates and EVERY caveat passes
|
|
294
|
+
/// read-only. A caveat that reverts (denied) OR cannot be evaluated read-only (a
|
|
295
|
+
/// stateful enforcer attempting a write under staticcall) yields `false` — so `true`
|
|
296
|
+
/// is a genuine authorization guarantee for this exact call, and `false` means "use
|
|
297
|
+
/// live redemption" (which evaluates stateful caveats properly). The caveat
|
|
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
|
|
304
|
+
/// addition — `redeemDelegation` is untouched.
|
|
305
|
+
/// @param delegations Chain (leaf first; root authority last)
|
|
306
|
+
/// @param sender The address that would be the redeemer
|
|
307
|
+
/// @param target The exact target the delegated call would hit
|
|
308
|
+
/// @param value The exact ETH value
|
|
309
|
+
/// @param data The exact calldata
|
|
310
|
+
function verifyAuthorizationForCall(
|
|
311
|
+
Delegation[] calldata delegations,
|
|
312
|
+
address sender,
|
|
313
|
+
address target,
|
|
314
|
+
uint256 value,
|
|
315
|
+
bytes calldata data
|
|
316
|
+
) external view returns (bool ok, string memory reason) {
|
|
317
|
+
if (delegations.length == 0) return (false, "empty-chain");
|
|
318
|
+
|
|
319
|
+
for (uint256 i = 0; i < delegations.length; i++) {
|
|
320
|
+
(bool valid, string memory r) = _validateDelegationView(delegations, i, sender);
|
|
321
|
+
if (!valid) return (false, r);
|
|
322
|
+
|
|
323
|
+
Delegation calldata d = delegations[i];
|
|
324
|
+
bytes32 dHash = hashDelegation(d);
|
|
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");
|
|
330
|
+
// staticcall the same hook redemption would call; success == satisfied read-only.
|
|
331
|
+
// Gas-bounded: a stateful enforcer (which would revert on its SSTORE under
|
|
332
|
+
// staticcall) is capped so an on-chain caller (e.g. RW1-1) sees a cheap, predictable
|
|
333
|
+
// `false`, not an out-of-gas of the whole call. CAVEAT_VIEW_GAS is generous for any
|
|
334
|
+
// pure/view enforcer (timestamp/value/targets/methods are all far below it).
|
|
335
|
+
(bool passed, ) = d.caveats[j].enforcer.staticcall{gas: CAVEAT_VIEW_GAS}(
|
|
336
|
+
abi.encodeWithSelector(
|
|
337
|
+
ICaveatEnforcer.beforeHook.selector,
|
|
338
|
+
d.caveats[j].terms,
|
|
339
|
+
d.caveats[j].args,
|
|
340
|
+
dHash,
|
|
341
|
+
d.delegator,
|
|
342
|
+
sender,
|
|
343
|
+
target,
|
|
344
|
+
value,
|
|
345
|
+
data
|
|
346
|
+
)
|
|
347
|
+
);
|
|
348
|
+
if (!passed) return (false, "caveat-failed");
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return (true, "");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/// @dev View-only sibling of `_validateDelegation`. Same checks, no state
|
|
355
|
+
/// writes, no events. Used by `verifyAuthorization`.
|
|
356
|
+
function _validateDelegationView(
|
|
357
|
+
Delegation[] calldata delegations,
|
|
358
|
+
uint256 i,
|
|
359
|
+
address sender
|
|
360
|
+
) internal view returns (bool ok, string memory reason) {
|
|
361
|
+
Delegation calldata d = delegations[i];
|
|
362
|
+
bytes32 dHash = hashDelegation(d);
|
|
363
|
+
|
|
364
|
+
if (_revoked[dHash]) return (false, "revoked");
|
|
365
|
+
|
|
366
|
+
// Delegate chain
|
|
367
|
+
if (i == 0) {
|
|
368
|
+
if (d.delegate != OPEN_DELEGATION && d.delegate != sender) {
|
|
369
|
+
return (false, "invalid-delegate");
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
if (d.delegate != OPEN_DELEGATION && d.delegate != delegations[i - 1].delegator) {
|
|
373
|
+
return (false, "invalid-delegate-chain");
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Authority chain
|
|
378
|
+
if (d.authority != ROOT_AUTHORITY) {
|
|
379
|
+
if (i + 1 >= delegations.length) return (false, "invalid-authority");
|
|
380
|
+
bytes32 parentHash = hashDelegation(delegations[i + 1]);
|
|
381
|
+
if (d.authority != parentHash) return (false, "invalid-authority-chain");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Signature
|
|
385
|
+
if (!_isValidSignatureBool(d.delegator, dHash, d.signature)) {
|
|
386
|
+
return (false, "invalid-signature");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return (true, "");
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/// @dev Bool-returning sibling of `_validateSignature`. Returns false
|
|
393
|
+
/// instead of reverting, so view-only callers can produce a typed
|
|
394
|
+
/// `(ok, reason)` instead of bubbling a revert.
|
|
395
|
+
function _isValidSignatureBool(
|
|
396
|
+
address signer,
|
|
397
|
+
bytes32 digest,
|
|
398
|
+
bytes calldata signature
|
|
399
|
+
) internal view returns (bool) {
|
|
400
|
+
// ERC-1271 path for smart accounts
|
|
401
|
+
if (signer.code.length > 0) {
|
|
402
|
+
try IERC1271(signer).isValidSignature(digest, signature) returns (bytes4 result) {
|
|
403
|
+
return result == IERC1271.isValidSignature.selector;
|
|
404
|
+
} catch {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// EOA — tryRecover the eth-signed message hash
|
|
409
|
+
bytes32 ethHash = digest.toEthSignedMessageHash();
|
|
410
|
+
(address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(ethHash, signature);
|
|
411
|
+
if (err != ECDSA.RecoverError.NoError) return false;
|
|
412
|
+
return recovered == signer;
|
|
413
|
+
}
|
|
414
|
+
|
|
245
415
|
// ─── Internal: Validation ──────────────────────────────────────────
|
|
246
416
|
|
|
247
417
|
function _validateDelegation(
|
|
@@ -66,4 +66,42 @@ interface IDelegationManager {
|
|
|
66
66
|
|
|
67
67
|
/// @notice Check if a delegation has been revoked.
|
|
68
68
|
function isRevoked(bytes32 delegationHash) external view returns (bool);
|
|
69
|
+
|
|
70
|
+
/// @notice View-only verification of a delegation chain. Returns whether
|
|
71
|
+
/// the chain authorizes `sender` to redeem WITHOUT executing it.
|
|
72
|
+
/// Used by trust-substrate contracts (e.g. AttestationRegistry's
|
|
73
|
+
/// bilateral-consent path per spec 242 PD-9) to validate a packed
|
|
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).
|
|
78
|
+
/// @param delegations Delegation chain, leaf first
|
|
79
|
+
/// @param sender The address that would be the redeemer
|
|
80
|
+
/// @return ok True if the chain verifies
|
|
81
|
+
/// @return reason Empty if ok; otherwise short rejection reason
|
|
82
|
+
function verifyAuthorization(
|
|
83
|
+
Delegation[] calldata delegations,
|
|
84
|
+
address sender
|
|
85
|
+
) external view returns (bool ok, string memory reason);
|
|
86
|
+
|
|
87
|
+
/// @notice View-only authorization of a SPECIFIC call (ADR-0027 / spec 249 RW1-4):
|
|
88
|
+
/// validates the chain AND evaluates every caveat's `beforeHook` against the exact
|
|
89
|
+
/// `(target, value, data)` read-only, fail-closed. `true` ONLY if the chain validates
|
|
90
|
+
/// and every caveat passes under `staticcall`; a denied OR non-view-evaluable caveat
|
|
91
|
+
/// yields `false` ("use live redemption"). Lets a registry bind consent to the exact
|
|
92
|
+
/// call inside its own transaction. Caveat evaluation mirrors `redeemDelegation`.
|
|
93
|
+
/// @param delegations Delegation chain, leaf first
|
|
94
|
+
/// @param sender The address that would be the redeemer
|
|
95
|
+
/// @param target The exact target of the delegated call
|
|
96
|
+
/// @param value The exact ETH value
|
|
97
|
+
/// @param data The exact calldata
|
|
98
|
+
/// @return ok True if the chain + every caveat authorize this exact call
|
|
99
|
+
/// @return reason Empty if ok; otherwise short rejection reason
|
|
100
|
+
function verifyAuthorizationForCall(
|
|
101
|
+
Delegation[] calldata delegations,
|
|
102
|
+
address sender,
|
|
103
|
+
address target,
|
|
104
|
+
uint256 value,
|
|
105
|
+
bytes calldata data
|
|
106
|
+
) external view returns (bool ok, string memory reason);
|
|
69
107
|
}
|