@pvium/p2id-core 0.1.3 → 0.2.0

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.
@@ -124,6 +124,172 @@ contract P2IDVault is IP2IDVault {
124
124
  maxRefundWindow = _maxRefundWindow;
125
125
  }
126
126
 
127
+ // ------------------------------------------------------------------ alpha authorization
128
+
129
+ /// @notice Alpha authorization nonces already consumed by this vault (caller-chosen, single use).
130
+ /// @dev Preserve this mapping and append later storage after the cache bindings below.
131
+ mapping(uint256 => bool) public alphaNonceUsed;
132
+ mapping(address => bytes32) public ownerVkHash;
133
+ mapping(address => uint256) public ownerAlphaRevision;
134
+ error AlphaAuthorizationRequired();
135
+ error InvalidVkHash();
136
+ error InvalidAlphaCall();
137
+ error AlphaNonceAlreadyUsed(uint256 nonce);
138
+
139
+ function supportsAlphaGuard() external pure returns (bool) { return true; }
140
+
141
+ function executeWithAttestation(bytes calldata action, AlphaAttestation calldata attestation) external validAlphaCall(action) returns (bytes memory) {
142
+ return _dispatchAlphaCall(action, attestation);
143
+ }
144
+
145
+ function _dispatchAlphaCall(bytes calldata action, AlphaAttestation memory attestation) private returns (bytes memory) {
146
+ if (action.length < 4) revert InvalidAlphaCall();
147
+ bytes4 selector = bytes4(action[:4]);
148
+ if (selector == IP2IDVault.refreshProof.selector) {
149
+ (address verifier, bytes memory proof) = abi.decode(action[4:], (address, bytes));
150
+ refreshProofWithAttestation(verifier, proof, attestation);
151
+ return hex"";
152
+ }
153
+ if (selector == IP2IDVault.refreshProofAndSweep.selector) {
154
+ (address verifier, bytes memory proof, address token, uint256 depositCountLimit) = abi.decode(action[4:], (address, bytes, address, uint256));
155
+ (uint256 amount, uint256 consumed) = refreshProofAndSweepWithAttestation(verifier, proof, token, depositCountLimit, attestation);
156
+ return abi.encode(amount, consumed);
157
+ }
158
+
159
+ if (selector == IP2IDVault.sweepBucket.selector) {
160
+ (address verifier, IP2IDVerifier.Constraint memory constraint, address token, bytes memory proof, uint256 depositCountLimit) = abi.decode(action[4:], (address, IP2IDVerifier.Constraint, address, bytes, uint256));
161
+ (uint256 amount, uint256 consumed) = sweepBucketWithAttestation(verifier, constraint, token, proof, depositCountLimit, attestation);
162
+ return abi.encode(amount, consumed);
163
+ }
164
+ if (selector == IP2IDVault.sweepBucketDeposits.selector) {
165
+ (address verifier, IP2IDVerifier.Constraint memory constraint, address token, uint256[] memory depositIds, bytes memory proof) = abi.decode(action[4:], (address, IP2IDVerifier.Constraint, address, uint256[], bytes));
166
+ return abi.encode(sweepBucketDepositsWithAttestation(verifier, constraint, token, depositIds, proof, attestation));
167
+ }
168
+
169
+ revert InvalidAlphaCall();
170
+ }
171
+
172
+ function refreshProofWithAttestation(address verifier, bytes memory proof, AlphaAttestation memory attestation)
173
+ public nonReentrant
174
+ alphaAttestationRequired(verifier, attestation, abi.encodeCall(IP2IDVault.refreshProof, (verifier, proof)))
175
+ {
176
+ _refreshProof(verifier, proof);
177
+ }
178
+
179
+ function _refreshProof(address verifier, bytes memory proof) private {
180
+ _present(verifier, proof, _noConstraint());
181
+ }
182
+
183
+ function refreshProofAndSweepWithAttestation(address verifier, bytes memory proof, address token, uint256 depositCountLimit, AlphaAttestation memory attestation)
184
+ public nonReentrant
185
+ alphaAttestationRequired(verifier, attestation, abi.encodeCall(IP2IDVault.refreshProofAndSweep, (verifier, proof, token, depositCountLimit)))
186
+ returns (uint256 amount, uint256 consumed)
187
+ {
188
+ return _refreshProofAndSweep(verifier, proof, token, depositCountLimit);
189
+ }
190
+
191
+ function _refreshProofAndSweep(address verifier, bytes memory proof, address token, uint256 depositCountLimit) private returns (uint256 amount, uint256 consumed) {
192
+ _present(verifier, proof, _noConstraint());
193
+ return _sweepDefault(verifier, token, depositCountLimit);
194
+ }
195
+
196
+ function _sweep(address verifier, address token, uint256 depositCountLimit) private returns (uint256 amount, uint256 consumed) {
197
+ return _sweepDefault(verifier, token, depositCountLimit); // checks the owner is set and current
198
+ }
199
+
200
+ function _sweepUntracked(address token) private returns (uint256 amount) {
201
+ address verifier = defaultVerifier();
202
+ address to = _ownerOf(verifier);
203
+ if (latestProofIat[verifier] < untrackedProofIat) revert ProofTooOld();
204
+ uint256 gross = _untrackedBalance(token);
205
+ uint256 fee = (gross * _quoteFeeBps(verifier, token)) / BPS;
206
+ uint256 charged;
207
+ (amount, charged) = _payout(verifier, token, to, gross, fee);
208
+ emit Swept(verifier, token, amount, charged, to, 0);
209
+ }
210
+
211
+ function _sweepDeposits(address verifier, address token, uint256[] memory depositIds) private onlyInitialized(verifier) returns (uint256 amount) {
212
+ address to = owner[verifier];
213
+ (uint256 gross, uint256 fee) = _consumeIds(verifier, bytes32(0), token, depositIds, to);
214
+ uint256 charged;
215
+ (amount, charged) = _payout(verifier, token, to, gross, fee);
216
+ emit Swept(verifier, token, amount, charged, to, depositIds.length);
217
+ }
218
+
219
+ function sweepBucketWithAttestation(address verifier, IP2IDVerifier.Constraint memory constraint, address token, bytes memory proof, uint256 depositCountLimit, AlphaAttestation memory attestation)
220
+ public nonReentrant
221
+ alphaAttestationRequired(verifier, attestation, abi.encodeCall(IP2IDVault.sweepBucket, (verifier, constraint, token, proof, depositCountLimit)))
222
+ returns (uint256 amount, uint256 consumed)
223
+ {
224
+ return _sweepBucket(verifier, constraint, token, proof, depositCountLimit);
225
+ }
226
+
227
+ function _sweepBucket(address verifier, IP2IDVerifier.Constraint memory constraint, address token, bytes memory proof, uint256 depositCountLimit) private returns (uint256 amount, uint256 consumed) {
228
+ address to = _verifyForThisVault(verifier, proof, constraint);
229
+ uint256 gross;
230
+ uint256 fee;
231
+ (gross, fee, consumed) = _consumeFromCursor(verifier, constraint.commitment, token, depositCountLimit, to);
232
+ uint256 charged;
233
+ (amount, charged) = _payout(verifier, token, to, gross, fee);
234
+ emit SweptBucket(verifier, constraint.commitment, token, amount, charged, to, consumed);
235
+ }
236
+
237
+ function sweepBucketDepositsWithAttestation(address verifier, IP2IDVerifier.Constraint memory constraint, address token, uint256[] memory depositIds, bytes memory proof, AlphaAttestation memory attestation)
238
+ public nonReentrant
239
+ alphaAttestationRequired(verifier, attestation, abi.encodeCall(IP2IDVault.sweepBucketDeposits, (verifier, constraint, token, depositIds, proof)))
240
+ returns (uint256 amount)
241
+ {
242
+ return _sweepBucketDeposits(verifier, constraint, token, depositIds, proof);
243
+ }
244
+
245
+ function _sweepBucketDeposits(address verifier, IP2IDVerifier.Constraint memory constraint, address token, uint256[] memory depositIds, bytes memory proof) private returns (uint256 amount) {
246
+ address to = _verifyForThisVault(verifier, proof, constraint);
247
+ (uint256 gross, uint256 fee) = _consumeIds(verifier, constraint.commitment, token, depositIds, to);
248
+ uint256 charged;
249
+ (amount, charged) = _payout(verifier, token, to, gross, fee);
250
+ emit SweptBucket(verifier, constraint.commitment, token, amount, charged, to, depositIds.length);
251
+ }
252
+
253
+ function _withdrawFees(address verifier, address token) private returns (uint256 amount) {
254
+ uint256 owed = feesOwed[verifier][token];
255
+ if (owed == 0) return 0;
256
+ address pol = policy();
257
+ if (token == NATIVE) {
258
+ // Native coin cannot be pulled: it is sent along with the call, exactly the amount owed.
259
+ IP2IDPolicy(pol).distributeFee{value: owed}(verifier, token, owed);
260
+ amount = owed;
261
+ } else {
262
+ uint256 before = _balanceOf(token);
263
+ _callToken(token, abi.encodeWithSignature("approve(address,uint256)", pol, owed));
264
+ IP2IDPolicy(pol).distributeFee(verifier, token, owed);
265
+ _callToken(token, abi.encodeWithSignature("approve(address,uint256)", pol, 0));
266
+ uint256 afterBalance = _balanceOf(token);
267
+ amount = before > afterBalance ? before - afterBalance : 0;
268
+ if (amount > owed) revert FeeOverdrawn();
269
+ }
270
+ feesOwed[verifier][token] = owed - amount;
271
+ feesOwedTotal[token] -= amount;
272
+ emit FeesDistributed(verifier, token, amount, pol);
273
+ }
274
+
275
+ modifier validAlphaCall(bytes calldata action) {
276
+ if (action.length < 4) revert InvalidAlphaCall();
277
+ bytes4 selector = bytes4(action[:4]);
278
+ if (!(selector == IP2IDVault.refreshProof.selector || selector == IP2IDVault.refreshProofAndSweep.selector || selector == IP2IDVault.sweepBucket.selector || selector == IP2IDVault.sweepBucketDeposits.selector)) revert InvalidAlphaCall();
279
+ _;
280
+ }
281
+
282
+ /// @dev Empty signatures are allowed only after alpha. Bind approval to the entire ordinary call.
283
+ modifier alphaAttestationRequired(address verifier, AlphaAttestation memory attestation, bytes memory action) {
284
+ if (IP2IdVaultFactory(factory).isAlpha(IP2IDVerifier(verifier).vkHash())) {
285
+ if (attestation.signature.length == 0) revert AlphaAuthorizationRequired();
286
+ if (alphaNonceUsed[attestation.nonce]) revert AlphaNonceAlreadyUsed(attestation.nonce);
287
+ IP2IdVaultFactory(factory).verifyAlphaAuthorization(address(this), msg.sender, keccak256(action), attestation.nonce, attestation.deadline, attestation.signature);
288
+ alphaNonceUsed[attestation.nonce] = true;
289
+ }
290
+ _;
291
+ }
292
+
127
293
  // ------------------------------------------------------------------ funding
128
294
 
129
295
  /// @notice Fund under the factory's default verifier.
@@ -245,8 +411,8 @@ contract P2IDVault is IP2IDVault {
245
411
  // ------------------------------------------------------------------ proofs
246
412
 
247
413
  /// @notice Verify a proof and update this verifier's owner cache and issue-time floor using _apply.
248
- function refreshProof(address verifier, bytes calldata proof) external nonReentrant {
249
- _present(verifier, proof, _noConstraint());
414
+ function refreshProof(address verifier, bytes memory proof) external {
415
+ refreshProofWithAttestation(verifier, proof, AlphaAttestation(0, 0, hex""));
250
416
  }
251
417
 
252
418
  /// @notice Present a proof, then sweep `verifier`'s default bucket for `token`.
@@ -255,12 +421,11 @@ contract P2IDVault is IP2IDVault {
255
421
  /// @dev Includes untracked funds only when _paysUntracked(verifier) is true.
256
422
  function refreshProofAndSweep(
257
423
  address verifier,
258
- bytes calldata proof,
424
+ bytes memory proof,
259
425
  address token,
260
426
  uint256 depositCountLimit
261
- ) external nonReentrant returns (uint256 amount, uint256 consumed) {
262
- _present(verifier, proof, _noConstraint());
263
- return _sweepDefault(verifier, token, depositCountLimit);
427
+ ) external returns (uint256 amount, uint256 consumed) {
428
+ return refreshProofAndSweepWithAttestation(verifier, proof, token, depositCountLimit, AlphaAttestation(0, 0, hex""));
264
429
  }
265
430
 
266
431
  // ------------------------------------------------------------------ claiming
@@ -269,39 +434,19 @@ contract P2IDVault is IP2IDVault {
269
434
  /// this bucket's list from its cursor; `depositCountLimit` records per call (0 = MAX_SWEEP_PAGE, also the cap).
270
435
  /// Includes untracked funds when the verifier is the current default and its owner cache
271
436
  /// meets the revision and untrackedProofIat checks.
272
- function sweep(address verifier, address token, uint256 depositCountLimit)
273
- external
274
- nonReentrant
275
- returns (uint256 amount, uint256 consumed)
276
- {
277
- return _sweepDefault(verifier, token, depositCountLimit); // checks the owner is set and current
437
+ function sweep(address verifier, address token, uint256 depositCountLimit) external nonReentrant returns (uint256 amount, uint256 consumed) {
438
+ return _sweep(verifier, token, depositCountLimit);
278
439
  }
279
440
 
280
441
  /// @notice Sweep only untracked ERC-20 or native funds to
281
442
  /// the default verifier's owner. The fee rate is quoted now.
282
443
  function sweepUntracked(address token) external nonReentrant returns (uint256 amount) {
283
- address verifier = defaultVerifier();
284
- address to = _ownerOf(verifier);
285
- if (latestProofIat[verifier] < untrackedProofIat) revert ProofTooOld();
286
- uint256 gross = _untrackedBalance(token);
287
- uint256 fee = (gross * _quoteFeeBps(verifier, token)) / BPS;
288
- uint256 charged;
289
- (amount, charged) = _payout(verifier, token, to, gross, fee);
290
- emit Swept(verifier, token, amount, charged, to, 0);
444
+ return _sweepUntracked(token);
291
445
  }
292
446
 
293
447
  /// @notice Sweep specific default-bucket deposits of `verifier` by id (e.g. to skip spam in the same bucket).
294
- function sweepDeposits(address verifier, address token, uint256[] calldata depositIds)
295
- external
296
- nonReentrant
297
- onlyInitialized(verifier)
298
- returns (uint256 amount)
299
- {
300
- address to = owner[verifier];
301
- (uint256 gross, uint256 fee) = _consumeIds(verifier, bytes32(0), token, depositIds, to);
302
- uint256 charged;
303
- (amount, charged) = _payout(verifier, token, to, gross, fee);
304
- emit Swept(verifier, token, amount, charged, to, depositIds.length);
448
+ function sweepDeposits(address verifier, address token, uint256[] memory depositIds) external nonReentrant returns (uint256 amount) {
449
+ return _sweepDeposits(verifier, token, depositIds);
305
450
  }
306
451
 
307
452
  /// @notice Sweep the bucket funded under `verifier` and `constraint.commitment`. The verifier
@@ -309,33 +454,23 @@ contract P2IDVault is IP2IDVault {
309
454
  /// wallet the proof resolves to. This entry point rejects a zero commitment.
310
455
  function sweepBucket(
311
456
  address verifier,
312
- IP2IDVerifier.Constraint calldata constraint,
457
+ IP2IDVerifier.Constraint memory constraint,
313
458
  address token,
314
- bytes calldata proof,
459
+ bytes memory proof,
315
460
  uint256 depositCountLimit
316
- ) external nonReentrant returns (uint256 amount, uint256 consumed) {
317
- address to = _verifyForThisVault(verifier, proof, constraint);
318
- uint256 gross;
319
- uint256 fee;
320
- (gross, fee, consumed) = _consumeFromCursor(verifier, constraint.commitment, token, depositCountLimit, to);
321
- uint256 charged;
322
- (amount, charged) = _payout(verifier, token, to, gross, fee);
323
- emit SweptBucket(verifier, constraint.commitment, token, amount, charged, to, consumed);
461
+ ) external returns (uint256 amount, uint256 consumed) {
462
+ return sweepBucketWithAttestation(verifier, constraint, token, proof, depositCountLimit, AlphaAttestation(0, 0, hex""));
324
463
  }
325
464
 
326
465
  /// @notice Sweep specific deposits of a constrained bucket by id.
327
466
  function sweepBucketDeposits(
328
467
  address verifier,
329
- IP2IDVerifier.Constraint calldata constraint,
468
+ IP2IDVerifier.Constraint memory constraint,
330
469
  address token,
331
- uint256[] calldata depositIds,
332
- bytes calldata proof
333
- ) external nonReentrant returns (uint256 amount) {
334
- address to = _verifyForThisVault(verifier, proof, constraint);
335
- (uint256 gross, uint256 fee) = _consumeIds(verifier, constraint.commitment, token, depositIds, to);
336
- uint256 charged;
337
- (amount, charged) = _payout(verifier, token, to, gross, fee);
338
- emit SweptBucket(verifier, constraint.commitment, token, amount, charged, to, depositIds.length);
470
+ uint256[] memory depositIds,
471
+ bytes memory proof
472
+ ) external returns (uint256 amount) {
473
+ return sweepBucketDepositsWithAttestation(verifier, constraint, token, depositIds, proof, AlphaAttestation(0, 0, hex""));
339
474
  }
340
475
 
341
476
  // ------------------------------------------------------------------ fees
@@ -344,25 +479,7 @@ contract P2IDVault is IP2IDVault {
344
479
  /// Native fees are sent as msg.value. For ERC-20, approve the accrued amount, reset the
345
480
  /// allowance after the call, and deduct the observed balance decrease; revert if it exceeds fees owed.
346
481
  function withdrawFees(address verifier, address token) external nonReentrant returns (uint256 amount) {
347
- uint256 owed = feesOwed[verifier][token];
348
- if (owed == 0) return 0;
349
- address pol = policy();
350
- if (token == NATIVE) {
351
- // Native coin cannot be pulled: it is sent along with the call, exactly the amount owed.
352
- IP2IDPolicy(pol).distributeFee{value: owed}(verifier, token, owed);
353
- amount = owed;
354
- } else {
355
- uint256 before = _balanceOf(token);
356
- _callToken(token, abi.encodeWithSignature("approve(address,uint256)", pol, owed));
357
- IP2IDPolicy(pol).distributeFee(verifier, token, owed);
358
- _callToken(token, abi.encodeWithSignature("approve(address,uint256)", pol, 0));
359
- uint256 afterBalance = _balanceOf(token);
360
- amount = before > afterBalance ? before - afterBalance : 0;
361
- if (amount > owed) revert FeeOverdrawn();
362
- }
363
- feesOwed[verifier][token] = owed - amount;
364
- feesOwedTotal[token] -= amount;
365
- emit FeesDistributed(verifier, token, amount, pol);
482
+ return _withdrawFees(verifier, token);
366
483
  }
367
484
 
368
485
  // ------------------------------------------------------------------ views
@@ -430,7 +547,7 @@ contract P2IDVault is IP2IDVault {
430
547
  /// returned wallet and issue time. Proxy-verified results enter through acceptOwnerProof.
431
548
  function _present(
432
549
  address verifier,
433
- bytes calldata proof,
550
+ bytes memory proof,
434
551
  IP2IDVerifier.Constraint memory constraint
435
552
  ) private onlyClaimable(verifier) returns (address wallet) {
436
553
  uint64 iat;
@@ -461,7 +578,7 @@ contract P2IDVault is IP2IDVault {
461
578
  if (iat > block.timestamp + FUTURE_SLACK) revert ProofFromFuture();
462
579
  (bool revOk, uint64 rev) = _revision(verifier);
463
580
  if (!revOk) revert RevisionUnavailable(verifier);
464
- bool fresh = ownerRevision[verifier] == rev;
581
+ bool fresh = _ownerFresh(verifier);
465
582
  // Revision changes do not reset this verifier's stored issue-time floor.
466
583
  uint64 latest = latestProofIat[verifier];
467
584
  if (iat < latest) revert ProofTooOld();
@@ -476,6 +593,12 @@ contract P2IDVault is IP2IDVault {
476
593
  ownerRevision[verifier] = rev;
477
594
  emit OwnerRefreshed(verifier, wallet, iat);
478
595
  }
596
+ if (wallet == owner[verifier] && iat == latestProofIat[verifier]) {
597
+ bytes32 key = IP2IDVerifier(verifier).vkHash();
598
+ if (key == bytes32(0)) revert InvalidVkHash();
599
+ ownerVkHash[verifier] = key;
600
+ ownerAlphaRevision[verifier] = IP2IdVaultFactory(factory).alphaRevision(key);
601
+ }
479
602
  if (iat > untrackedProofIat && verifier == defaultVerifier()) untrackedProofIat = iat;
480
603
  // Otherwise iat == latest with the owner set: a same-age proof (a replayed copy, or another
481
604
  // wallet slot of the same token). The owner stays, and constrained paths pay `wallet`.
@@ -484,8 +607,8 @@ contract P2IDVault is IP2IDVault {
484
607
  /// @dev Verify a constrained claim; reject zero commitments at this entry point.
485
608
  function _verifyForThisVault(
486
609
  address verifier,
487
- bytes calldata proof,
488
- IP2IDVerifier.Constraint calldata constraint
610
+ bytes memory proof,
611
+ IP2IDVerifier.Constraint memory constraint
489
612
  ) private returns (address wallet) {
490
613
  if (constraint.commitment == bytes32(0)) revert ConstraintRequired();
491
614
  return _present(verifier, proof, constraint);
@@ -507,7 +630,11 @@ contract P2IDVault is IP2IDVault {
507
630
  /// @dev Whether the cached owner was proven under the verifier's current revision.
508
631
  function _ownerFresh(address verifier) private view returns (bool) {
509
632
  (bool ok, uint64 rev) = _revision(verifier);
510
- return ok && ownerRevision[verifier] == rev;
633
+ if (!ok || ownerRevision[verifier] != rev) return false;
634
+ try IP2IDVerifier(verifier).vkHash() returns (bytes32 key) {
635
+ return key != bytes32(0) && ownerVkHash[verifier] == key
636
+ && ownerAlphaRevision[verifier] == IP2IdVaultFactory(factory).alphaRevision(key);
637
+ } catch { return false; }
511
638
  }
512
639
 
513
640
  function _noConstraint() private pure returns (IP2IDVerifier.Constraint memory c) {
@@ -568,7 +695,7 @@ contract P2IDVault is IP2IDVault {
568
695
  address verifier,
569
696
  bytes32 constraint,
570
697
  address token,
571
- uint256[] calldata depositIds,
698
+ uint256[] memory depositIds,
572
699
  address to
573
700
  ) private returns (uint256 amount, uint256 fee) {
574
701
  for (uint256 k = 0; k < depositIds.length; k++) {
@@ -22,6 +22,8 @@ contract PviumIdentity is IPviumIdentity {
22
22
  IVerifier public immutable verifier;
23
23
  /// @notice Circuit version identifier supplied at deployment; not checked against the verifier's bytecode.
24
24
  uint16 public immutable circuitVersion;
25
+ /// @notice SHA-256 of the verification-key artifact pinned by this deployment.
26
+ bytes32 public immutable vkHash;
25
27
  /// @notice Notice a key proposal gives before it can be activated.
26
28
  uint64 public constant SIGNER_KEY_DELAY = 7 days;
27
29
  /// @notice Number of accepted signer keys. When zero, the signer check rejects attestations.
@@ -85,17 +87,20 @@ contract PviumIdentity is IPviumIdentity {
85
87
  error TimelockNotElapsed(uint64 eta);
86
88
  error UnknownKey(uint256 x, uint256 y);
87
89
 
90
+ /// @param _vkHash Trusted SHA-256 of the pinned verification-key artifact; not derived on-chain.
88
91
  /// @param _owner Key-set administrator; address(0) disables owner-only calls.
89
92
  /// @param _signerXs X coordinates of the initial accepted P-256 keys.
90
93
  /// @param _signerYs Corresponding Y coordinates.
91
94
  constructor(
92
95
  IVerifier _verifier,
93
96
  uint16 _circuitVersion,
97
+ bytes32 _vkHash,
94
98
  address _owner,
95
99
  uint256[] memory _signerXs,
96
100
  uint256[] memory _signerYs
97
101
  ) {
98
102
  if (address(_verifier).code.length == 0) revert InvalidVerifier();
103
+ if (_vkHash == bytes32(0)) revert InvalidVerifier();
99
104
  if (_circuitVersion == 0) revert InvalidCircuitVersion();
100
105
  if (_signerXs.length == 0 || _signerXs.length != _signerYs.length) revert NoSignerKeys();
101
106
  for (uint256 i = 0; i < _signerXs.length; i++) {
@@ -107,6 +112,7 @@ contract PviumIdentity is IPviumIdentity {
107
112
  }
108
113
  verifier = _verifier;
109
114
  circuitVersion = _circuitVersion;
115
+ vkHash = _vkHash;
110
116
  signerKeyCount = _signerXs.length;
111
117
  owner = _owner;
112
118
  emit OwnershipTransferred(address(0), _owner);
@@ -13,6 +13,21 @@ import {IP2IDVerifier} from "./interfaces/IP2IDVerifier.sol";
13
13
  /// Delegated code can write both slots. Proxy-defined selectors take precedence over the
14
14
  /// implementation, and empty-calldata native transfers are handled here without delegation.
15
15
  contract PviumP2IDVaultProxy is IP2IDVaultProxy {
16
+ error AlphaAuthorizationRequired();
17
+ error AlphaImplementationRequired();
18
+ bytes32 private constant ALPHA_UPGRADE_SLOT = keccak256("pvium.vault.proxy.alpha.upgrade.nonce.v1");
19
+
20
+ function alphaUpgradeNonceUsed(uint256 nonce) public view returns (bool used) {
21
+ bytes32 slot = keccak256(abi.encode(nonce, ALPHA_UPGRADE_SLOT));
22
+ assembly { used := sload(slot) }
23
+ }
24
+
25
+ error AlphaNonceAlreadyUsed(uint256 nonce);
26
+
27
+ function upgradeToWithAttestation(bytes32 identityHash, address newImplementation, bytes calldata proof, IP2IDVault.AlphaAttestation calldata attestation) external {
28
+ _upgradeTo(identityHash, newImplementation, proof, attestation);
29
+ }
30
+
16
31
  /// @dev bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)
17
32
  bytes32 private constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
18
33
  /// @dev bytes32(uint256(keccak256("pvium.vault.proxy.upgrading")) - 1): 1 while upgradeTo runs (a plain slot: the Paris target has no transient storage).
@@ -63,12 +78,27 @@ contract PviumP2IDVaultProxy is IP2IDVaultProxy {
63
78
 
64
79
  /// @inheritdoc IP2IDVaultProxy
65
80
  function upgradeTo(bytes32 identityHash, address newImplementation, bytes calldata proof) external {
81
+ _upgradeTo(identityHash, newImplementation, proof, IP2IDVault.AlphaAttestation(0, 0, hex""));
82
+ }
83
+
84
+ function _upgradeTo(bytes32 identityHash, address newImplementation, bytes calldata proof, IP2IDVault.AlphaAttestation memory attestation) private {
66
85
  _enterUpgrade();
67
86
  IP2IdVaultFactory f = IP2IdVaultFactory(factory);
68
87
  if (f.vaultFor(identityHash) != address(this)) revert NotVaultOf(identityHash);
69
88
  if (!f.isRegisteredImplementation(newImplementation)) revert NotRegisteredImplementation(newImplementation);
89
+ try IP2IDVault(newImplementation).supportsAlphaGuard() returns (bool supported) {
90
+ if (!supported) revert AlphaImplementationRequired();
91
+ } catch { revert AlphaImplementationRequired(); }
70
92
  if (newImplementation == implementation()) revert SameImplementation();
71
93
  address verifier = f.defaultVerifier();
94
+ if (f.isAlpha(IP2IDVerifier(verifier).vkHash())) {
95
+ if (attestation.signature.length == 0) revert AlphaAuthorizationRequired();
96
+ if (alphaUpgradeNonceUsed(attestation.nonce)) revert AlphaNonceAlreadyUsed(attestation.nonce);
97
+ f.verifyAlphaAuthorization(address(this), msg.sender,
98
+ keccak256(abi.encodeCall(IP2IDVaultProxy.upgradeTo, (identityHash, newImplementation, proof))), attestation.nonce, attestation.deadline, attestation.signature);
99
+ bytes32 slot = keccak256(abi.encode(attestation.nonce, ALPHA_UPGRADE_SLOT));
100
+ assembly { sstore(slot, 1) }
101
+ }
72
102
  (address wallet, uint64 iat) =
73
103
  IP2IDVerifier(verifier).getIdentityWallet(identityHash, proof, IP2IDVerifier.Constraint(bytes32(0), ""));
74
104
  if (wallet != msg.sender) revert NotOwner(wallet, msg.sender);