@pvium/p2id-core 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -4,6 +4,8 @@ Derive deterministic EVM vault addresses from email addresses, social handles an
4
4
  identities. Addresses can receive funds before the recipient registers or the vault is deployed.
5
5
  Claims pay the wallet bound by an accepted identity proof.
6
6
 
7
+ Learn more about P2ID in the [P2ID protocol specification](https://github.com/pvium/zkid/blob/main/P2ID.md).
8
+
7
9
  The package includes identity hashing, address derivation and Solidity sources. For off-chain
8
10
  proof verification, use
9
11
  [`@pvium/p2id-verifier`](https://www.npmjs.com/package/@pvium/p2id-verifier).
@@ -7,75 +7,28 @@ import {IP2IDPolicy} from "./interfaces/IP2IDPolicy.sol";
7
7
  import {IP2IdVaultFactory} from "./interfaces/IP2IdVaultFactory.sol";
8
8
 
9
9
  /// @title P2IDVault
10
- /// @notice A namespace-bound vault that holds ERC-20 funds until the identity owner claims them.
11
- /// @dev Holds ERC-20 tokens and the chain's native coin (BNB, ETH), the latter identified by
12
- /// token == NATIVE (address(0)) everywhere: in deposits, buckets, sweeps, fees and views.
13
- ///
14
- /// **A bare transfer to this contract (ERC-20 transfer, or plain native send) is irrevocable:
15
- /// it records no funder, has no refund path, and is claimable only through the factory's
16
- /// default verifier.** Use fund() when refund rights, a funding constraint, or a specific
17
- /// verifier are required.
18
- ///
19
- /// This contract holds mechanics only; everything expected to evolve is decided by the
20
- /// factory's policy (IP2IDPolicy), consulted on every call, so it can change without
21
- /// changing this bytecode and therefore without moving any P2ID address. The limits a
22
- /// policy can never cross are enforced here:
23
- /// - fees are capped at MAX_FEE_BPS, fixed per deposit when it is made (a failing quote
24
- /// means no fee), and accrue in the vault per (verifier, token); a payout never calls
25
- /// the policy about fees, so fee handling can never block a claim. withdrawFees hands
26
- /// accrued fees to the current policy, which pulls exactly the approved amount and
27
- /// distributes it;
28
- /// - refunds never pay a fee and never consult the policy;
29
- /// - the policy can gate which verifiers are usable (freezing claims under a disallowed
30
- /// one), but cannot redirect a payout: every claim still pays the wallet the verifier
31
- /// resolved for this vault's identity. Claims through the factory's current default
32
- /// verifier can never be frozen: direct transfers have no refund path, so the only thing
33
- /// that may change who receives them is a default-verifier change with 14 days' notice.
34
- /// (The policy still gates *funding* under the default, so a revoked default takes no
35
- /// new deposits.)
36
- ///
37
- /// Verifiers. Every deposit names the IP2IDVerifier whose proofs can release it, chosen by
38
- /// the payer among those the policy allows (fund() takes the factory default). Every claim
39
- /// path hands the verifier this vault's `saltCommitment` plus a proof and gets back the
40
- /// wallet to pay; the verifier reverts unless the proof is for exactly that identity.
41
- /// A constrained deposit is refused under a verifier that cannot satisfy constraints.
42
- ///
43
- /// Accounting is per bucket, where a bucket is (verifier, constraint, token) and the
44
- /// default bucket of a verifier is constraint == bytes32(0). Each bucket keeps its own
45
- /// deposit-id list and cursor, so sweeps are bounded per bucket, can be paged
46
- /// (`depositCountLimit` = number of deposit records visited from the cursor, consumed or
47
- /// not; 0 = all) or aimed at specific ids, and spam in other tokens or buckets never blocks
48
- /// a sweep.
49
- ///
50
- /// Proof freshness ratchet, per verifier: every proof presented must be at least as fresh
51
- /// as the newest one this vault has seen under that verifier, and a newer proof becomes the
52
- /// owner proof, so presenting a proof for a new wallet retires every older proof. Funds
53
- /// already inside before that reset are not protected: reset immediately after a wallet
54
- /// compromise.
55
- ///
56
- /// Direct transfers follow the factory's default verifier, which governance can change after 14
57
- /// days' public notice. So that a change cannot bring back a wallet the owner has already moved away from,
58
- /// direct transfers have their own freshness floor, `untrackedProofIat`: the newest proof
59
- /// presented through whichever verifier was the default at the time. It carries across a
60
- /// default change, and direct transfers are paid only to a default-verifier owner whose proof
61
- /// is at least that fresh. Only the default verifier (chosen by governance, under the
62
- /// timelock) can raise it, so a verifier payers merely opted into cannot affect it.
63
- ///
64
- /// Invariant kept by every path: token balance >= trackedTotal[token] + feesOwedTotal[token].
10
+ /// @notice Vault implementation for ERC-20 and native-coin deposits associated with an identity.
11
+ /// Uses its immutable factory for policy and default-verifier settings.
12
+ /// @dev Deposits are indexed by (verifier, constraint, token). Cursor-based sweeps visit at most
13
+ /// MAX_SWEEP_PAGE records; explicit-id sweeps iterate the supplied list. Owner caches and
14
+ /// proof floors are per verifier, with an additional cross-default floor for untracked funds.
15
+ /// Direct transfers create no deposit record and have no refund entry point. Token accounting
16
+ /// relies on the token's reported balances and transfer behavior.
65
17
  contract P2IDVault is IP2IDVault {
66
- /// @notice Hard cap on the fee any policy can charge on a payout: 1%. Part of this bytecode.
18
+ /// @notice Maximum fee rate recorded or quoted by this implementation: 100 basis points (1%).
67
19
  uint16 public constant MAX_FEE_BPS = 100;
68
- /// @notice The token address that stands for the chain's native coin (BNB on BNB Chain).
20
+ /// @notice Token identifier used for the native coin.
69
21
  address public constant NATIVE = address(0);
70
22
  uint256 private constant BPS = 10_000;
71
- /// @dev Gas given to optional policy/verifier queries, so a misbehaving contract cannot burn
72
- /// the caller's gas; a query that fails or runs out is treated as "no fee" / "unsupported".
23
+ /// @dev Gas cap for _query calls. Failed fee/support queries return zero/false; failed revision
24
+ /// queries invalidate cached-owner reads and prevent proof updates.
73
25
  uint256 private constant QUERY_GAS = 50_000;
74
- /// @dev How far ahead of the block a proof's issue time may be (clock skew between the token
75
- /// issuer and the chain); proofs are made after their token is issued, so this is slack only.
26
+ /// @notice Record limit for cursor-based sweeps; a zero or larger requested limit uses this value.
27
+ uint256 public constant MAX_SWEEP_PAGE = 100;
28
+ /// @dev Maximum accepted issue time ahead of block.timestamp.
76
29
  uint64 private constant FUTURE_SLACK = 15 minutes;
77
30
 
78
- /// @notice Deployer (PviumP2IdVaultFactory): the policy's source, and the only caller of initialize() and fundFor().
31
+ /// @notice Factory supplied at construction; source of settings and authorized caller of initialize()/fundFor().
79
32
  address public immutable factory;
80
33
  bytes32 public nsHash;
81
34
  bytes32 public saltCommitment;
@@ -83,18 +36,17 @@ contract P2IDVault is IP2IDVault {
83
36
  uint64 public maxRefundWindow;
84
37
  bool private _initialized;
85
38
 
86
- /// @notice Wallet of the newest proof presented under a verifier; paid by that verifier's default-bucket sweeps.
39
+ /// @notice Cached payout wallet per verifier. Equal-time proofs do not replace an existing wallet.
87
40
  mapping(address verifier => address) public owner;
88
- /// @notice Issue time of that proof. Older proofs are refused under that verifier on every path.
41
+ /// @notice Stored issue-time floor checked by _apply for this verifier.
89
42
  mapping(address verifier => uint64) public latestProofIat;
90
- /// @notice The verifier's `revision()` when its owner was cached. A cache from an older revision
91
- /// is void: the verifier revoked something it trusted then, so the owner must re-prove.
43
+ /// @notice Revision stored with the cached owner; _ownerFresh requires equality with the current revision.
92
44
  mapping(address verifier => uint64) public ownerRevision;
93
45
  /// @notice Freshness floor for direct transfers: the newest proof presented through the verifier
94
46
  /// that was the default at the time. Carries across default changes.
95
47
  uint64 public untrackedProofIat;
96
48
 
97
- /// @notice All deposits ever made, by id.
49
+ /// @notice Deposit records, including consumed records, indexed by id.
98
50
  Deposit[] public deposits;
99
51
 
100
52
  /// @notice Unconsumed total per (verifier, constraint, token) bucket.
@@ -105,16 +57,16 @@ contract P2IDVault is IP2IDVault {
105
57
  mapping(address verifier => mapping(bytes32 constraint => mapping(address token => uint256[]))) private _bucketDeposits;
106
58
  /// @notice Index into the bucket's deposit list below which every deposit is consumed.
107
59
  mapping(address verifier => mapping(bytes32 constraint => mapping(address token => uint256))) public bucketCursor;
108
- /// @dev keccak256(constraint, funder) => depositId + 1. A funder can use a constraint once, so a
109
- /// signature over it releases at most one of their deposits; another funder reusing the
110
- /// same value only puts their own money in the bucket.
60
+ /// @dev keccak256(abi.encode(constraint, funder)) => depositId + 1 for nonzero constraints.
61
+ /// Entries persist after claims and refunds, preventing reuse by the same funder in this vault.
111
62
  mapping(bytes32 => uint256) private _constraintDeposit;
112
63
  /// @notice Fees accrued and not yet distributed, per verifier they were earned through and token.
113
64
  mapping(address verifier => mapping(address token => uint256)) public feesOwed;
114
65
  /// @notice Sum of feesOwed per token: held for distribution, never part of a payout.
115
66
  mapping(address token => uint256) public feesOwedTotal;
116
67
 
117
- uint256 private reentrancyLock = 1;
68
+ /// @dev 2 while a guarded call runs; any other value (including the 0 a fresh proxy starts with) is idle.
69
+ uint256 private reentrancyLock;
118
70
 
119
71
  error InvalidRefundWindow();
120
72
  error VerifierNotApproved(address verifier);
@@ -123,6 +75,9 @@ contract P2IDVault is IP2IDVault {
123
75
  error InvalidToken();
124
76
  error OwnerNotInitialized();
125
77
  error OwnerRevoked();
78
+ error RevisionUnavailable(address verifier);
79
+ error NotSelf();
80
+ error NotOwnerProof(address owner);
126
81
  error ProofFromFuture();
127
82
  error ProofTooOld();
128
83
  error InvalidWallet();
@@ -142,19 +97,18 @@ contract P2IDVault is IP2IDVault {
142
97
  error NativeValueMismatch();
143
98
  error NativeTransferFailed();
144
99
 
145
- /// @dev No constructor arguments, so the creation code is identical for every vault and
146
- /// `keccak256(creationCode)` is a constant anyone can use to derive an identity's vault
147
- /// address from the factory address. The deployer is recorded and must call initialize().
148
- constructor() {
149
- factory = msg.sender;
100
+ /// @dev This constructor runs only for the implementation; each proxy's storage is set up
101
+ /// separately by the factory calling initialize(). Address derivation uses proxy creation code.
102
+ /// @param _factory Address embedded in this implementation for internal factory references.
103
+ constructor(address _factory) {
104
+ factory = _factory;
150
105
  }
151
106
 
152
- /// @notice Plain native-coin sends (e.g. fee payouts from a launchpad) are accepted as direct
153
- /// transfers: untracked, claimable through the default verifier, never refundable.
154
- /// Deliberately empty, so it works with the 2300-gas stipend of `transfer`/`send`.
107
+ /// @notice Accept native coin without creating a deposit record.
108
+ /// @dev Calls to a vault proxy with empty calldata are handled by the proxy's own receive().
155
109
  receive() external payable {}
156
110
 
157
- /// @notice Set by the factory once, immediately after deployment.
111
+ /// @notice Initialize vault settings once; callable only by the configured factory.
158
112
  function initialize(
159
113
  bytes32 _nsHash,
160
114
  bytes32 _saltCommitment,
@@ -201,9 +155,8 @@ contract P2IDVault is IP2IDVault {
201
155
  return _fund(msg.sender, verifier, token, amount, constraint, refundWindow, ref);
202
156
  }
203
157
 
204
- /// @notice Factory-only: fund on behalf of `funder`, who keeps the refund right. Tokens are
205
- /// pulled from the factory, which has already collected them from `funder`; native
206
- /// coin arrives as msg.value.
158
+ /// @notice Factory-only: record a deposit for `funder`. ERC-20 tokens are pulled from the
159
+ /// factory; native coin is supplied as msg.value.
207
160
  /// @param ref Opaque application reference emitted in Funded; bytes32(0) for none.
208
161
  function fundFor(
209
162
  address funder,
@@ -218,8 +171,8 @@ contract P2IDVault is IP2IDVault {
218
171
  return _fund(funder, verifier, token, amount, constraint, refundWindow, ref);
219
172
  }
220
173
 
221
- /// @dev Take `amount` of `token` from msg.sender (native: exactly msg.value; ERC-20: pulled,
222
- /// crediting what actually arrived) and record a deposit owned by `funder`, with the fee
174
+ /// @dev Receive native msg.value or pull ERC-20 from msg.sender, crediting the reported balance
175
+ /// increase for ERC-20. Record a deposit owned by `funder`, with the fee
223
176
  /// rate the policy quotes now (capped) fixed for it.
224
177
  function _fund(
225
178
  address funder,
@@ -244,7 +197,7 @@ contract P2IDVault is IP2IDVault {
244
197
  if (msg.value != amount) revert NativeValueMismatch();
245
198
  credited = amount;
246
199
  } else {
247
- if (msg.value != 0) revert NativeValueMismatch(); // never strand native coin on an ERC-20 deposit
200
+ if (msg.value != 0) revert NativeValueMismatch(); // ERC-20 deposits do not accept msg.value
248
201
  if (token.code.length == 0) revert InvalidToken();
249
202
  uint256 beforeBalance = _balanceOf(token);
250
203
  _callToken(token, abi.encodeWithSignature("transferFrom(address,address,uint256)", msg.sender, address(this), amount));
@@ -291,17 +244,15 @@ contract P2IDVault is IP2IDVault {
291
244
 
292
245
  // ------------------------------------------------------------------ proofs
293
246
 
294
- /// @notice Present a proof under `verifier` without claiming: sets that verifier's owner
295
- /// wallet from it if it is newer than anything seen before, and retires every older
296
- /// proof. Call this immediately after moving to a new wallet.
247
+ /// @notice Verify a proof and update this verifier's owner cache and issue-time floor using _apply.
297
248
  function refreshProof(address verifier, bytes calldata proof) external nonReentrant {
298
249
  _present(verifier, proof, _noConstraint());
299
250
  }
300
251
 
301
- /// @notice Present a proof and sweep `verifier`'s default bucket for `token` in one call (first claim).
252
+ /// @notice Present a proof, then sweep `verifier`'s default bucket for `token`.
302
253
  /// @param depositCountLimit Number of deposit records to visit from the bucket cursor (not
303
- /// an amount, not an index); 0 = all. For the default verifier, untracked
304
- /// (bare-transfer) funds are always included.
254
+ /// an amount, not an index); zero or above MAX_SWEEP_PAGE uses MAX_SWEEP_PAGE.
255
+ /// @dev Includes untracked funds only when _paysUntracked(verifier) is true.
305
256
  function refreshProofAndSweep(
306
257
  address verifier,
307
258
  bytes calldata proof,
@@ -315,8 +266,9 @@ contract P2IDVault is IP2IDVault {
315
266
  // ------------------------------------------------------------------ claiming
316
267
 
317
268
  /// @notice Sweep `verifier`'s default bucket for `token` to that verifier's owner. Walks only
318
- /// this bucket's list from its cursor; `depositCountLimit` records per call (0 = all).
319
- /// When `verifier` is the factory default, untracked funds are included.
269
+ /// this bucket's list from its cursor; `depositCountLimit` records per call (0 = MAX_SWEEP_PAGE, also the cap).
270
+ /// Includes untracked funds when the verifier is the current default and its owner cache
271
+ /// meets the revision and untrackedProofIat checks.
320
272
  function sweep(address verifier, address token, uint256 depositCountLimit)
321
273
  external
322
274
  nonReentrant
@@ -325,7 +277,7 @@ contract P2IDVault is IP2IDVault {
325
277
  return _sweepDefault(verifier, token, depositCountLimit); // checks the owner is set and current
326
278
  }
327
279
 
328
- /// @notice Sweep only untracked funds (bare ERC-20 transfers backed by no deposit record) to
280
+ /// @notice Sweep only untracked ERC-20 or native funds to
329
281
  /// the default verifier's owner. The fee rate is quoted now.
330
282
  function sweepUntracked(address token) external nonReentrant returns (uint256 amount) {
331
283
  address verifier = defaultVerifier();
@@ -354,7 +306,7 @@ contract P2IDVault is IP2IDVault {
354
306
 
355
307
  /// @notice Sweep the bucket funded under `verifier` and `constraint.commitment`. The verifier
356
308
  /// must accept both the identity proof and the constraint evidence; funds go to the
357
- /// wallet the proof resolves to. Default buckets (zero commitment) are swept via sweep().
309
+ /// wallet the proof resolves to. This entry point rejects a zero commitment.
358
310
  function sweepBucket(
359
311
  address verifier,
360
312
  IP2IDVerifier.Constraint calldata constraint,
@@ -388,9 +340,9 @@ contract P2IDVault is IP2IDVault {
388
340
 
389
341
  // ------------------------------------------------------------------ fees
390
342
 
391
- /// @notice Hand the fees earned through `verifier` in `token` to the current policy, which
392
- /// pulls them and distributes them. Anyone may call it: the tokens can only go to the
393
- /// policy, and only as much as is owed. Returns the amount the policy took.
343
+ /// @notice Call the current policy to distribute accrued fees; callable by any address.
344
+ /// Native fees are sent as msg.value. For ERC-20, approve the accrued amount, reset the
345
+ /// allowance after the call, and deduct the observed balance decrease; revert if it exceeds fees owed.
394
346
  function withdrawFees(address verifier, address token) external nonReentrant returns (uint256 amount) {
395
347
  uint256 owed = feesOwed[verifier][token];
396
348
  if (owed == 0) return 0;
@@ -420,12 +372,12 @@ contract P2IDVault is IP2IDVault {
420
372
  return "p2id.vault.v1";
421
373
  }
422
374
 
423
- /// @notice The factory's current policy (replaceable there only through a timelock).
375
+ /// @notice Policy address returned by the configured factory.
424
376
  function policy() public view returns (address) {
425
377
  return IP2IdVaultFactory(factory).policy();
426
378
  }
427
379
 
428
- /// @notice The factory's current default verifier (timelocked there): used by fund() and for untracked funds.
380
+ /// @notice Default verifier returned by the configured factory, used by fund() and untracked claims.
429
381
  function defaultVerifier() public view returns (address) {
430
382
  return IP2IdVaultFactory(factory).defaultVerifier();
431
383
  }
@@ -434,40 +386,48 @@ contract P2IDVault is IP2IDVault {
434
386
  return deposits.length;
435
387
  }
436
388
 
437
- /// @notice Deposit ids in a bucket, in funding order (consumed ones included; see `deposits`).
438
- function bucketDepositIds(address verifier, bytes32 constraint, address token) external view returns (uint256[] memory) {
439
- return _bucketDeposits[verifier][constraint][token];
389
+ /// @notice A page of a bucket's deposit ids, in funding order (consumed ones included; see
390
+ /// `deposits`): up to `limit` ids from `offset`, or all from `offset` when `limit` is 0.
391
+ function bucketDepositIds(address verifier, bytes32 constraint, address token, uint256 offset, uint256 limit)
392
+ external
393
+ view
394
+ returns (uint256[] memory ids)
395
+ {
396
+ uint256[] storage all = _bucketDeposits[verifier][constraint][token];
397
+ if (offset >= all.length) return ids;
398
+ uint256 end = all.length;
399
+ if (limit != 0 && end - offset > limit) end = offset + limit;
400
+ ids = new uint256[](end - offset);
401
+ for (uint256 k = offset; k < end; k++) ids[k - offset] = all[k];
440
402
  }
441
403
 
442
404
  function bucketDepositCount(address verifier, bytes32 constraint, address token) external view returns (uint256) {
443
405
  return _bucketDeposits[verifier][constraint][token].length;
444
406
  }
445
407
 
446
- /// @notice The deposit `funder` made under `constraint`, if any. Each funder can use a constraint once.
408
+ /// @notice Recorded deposit for a nonzero constraint and funder; the record persists after consumption.
447
409
  function constraintDeposit(bytes32 constraint, address funder) external view returns (bool used, uint256 depositId) {
448
410
  uint256 stored = _constraintDeposit[keccak256(abi.encode(constraint, funder))];
449
411
  return stored == 0 ? (false, 0) : (true, stored - 1);
450
412
  }
451
413
 
452
- /// @notice Gross amount `sweep(verifier, token, 0)` would release now, before fees: every
453
- /// unconsumed default deposit under `verifier`, plus untracked funds when it is the default verifier.
414
+ /// @notice Total default-bucket amount plus untracked funds when _paysUntracked is true.
415
+ /// @dev Does not apply the sweep page limit or validate default-bucket claimability.
454
416
  function sweepable(address verifier, address token) external view returns (uint256) {
455
417
  uint256 amount = bucketTotal[verifier][bytes32(0)][token];
456
418
  if (_paysUntracked(verifier)) amount += _untrackedBalance(token);
457
419
  return amount;
458
420
  }
459
421
 
460
- /// @notice Untracked funds only: bare ERC-20 transfers not backed by any deposit record.
422
+ /// @notice Reported balance minus tracked deposits and accrued fees, floored at zero; supports native coin.
461
423
  function untrackedBalance(address token) external view returns (uint256) {
462
424
  return _untrackedBalance(token);
463
425
  }
464
426
 
465
427
  // ------------------------------------------------------------------ internals
466
428
 
467
- /// @dev Every proof enters through here. Requires `verifier` to be allowed, verifies the proof
468
- /// for this vault's identity (the verifier reverts for any other identity), refuses it if
469
- /// older than the newest proof seen under that verifier, and if it is newer makes its
470
- /// wallet that verifier's owner. Returns the wallet to pay.
429
+ /// @dev Check verifier claimability, request verification for saltCommitment, then apply the
430
+ /// returned wallet and issue time. Proxy-verified results enter through acceptOwnerProof.
471
431
  function _present(
472
432
  address verifier,
473
433
  bytes calldata proof,
@@ -475,15 +435,34 @@ contract P2IDVault is IP2IDVault {
475
435
  ) private onlyClaimable(verifier) returns (address wallet) {
476
436
  uint64 iat;
477
437
  (wallet, iat) = IP2IDVerifier(verifier).getIdentityWallet(saltCommitment, proof, constraint);
438
+ _apply(verifier, wallet, iat);
439
+ }
440
+
441
+ /// @notice Apply a proxy-verified wallet and issue time; require msg.sender == address(this).
442
+ /// Revert unless wallet is the cached owner afterwards, then return the hook selector.
443
+ /// @dev The self-call check relies on the proxy verifying the result before invoking this hook.
444
+ function acceptOwnerProof(address verifier, address wallet, uint64 iat)
445
+ external
446
+ nonReentrant
447
+ onlyClaimable(verifier)
448
+ returns (bytes4)
449
+ {
450
+ if (msg.sender != address(this)) revert NotSelf();
451
+ _apply(verifier, wallet, iat);
452
+ if (owner[verifier] != wallet) revert NotOwnerProof(owner[verifier]);
453
+ return IP2IDVault.acceptOwnerProof.selector; // compatibility acknowledgement after applying the result
454
+ }
455
+
456
+ /// @dev The freshness and revision rules for a verified (wallet, iat) under `verifier`: refuses
457
+ /// proofs older than the newest seen, and makes a newer wallet the owner.
458
+ function _apply(address verifier, address wallet, uint64 iat) private {
478
459
  if (wallet == address(0)) revert InvalidWallet();
479
- // A forged token could carry any issue time; refusing future ones (beyond clock skew) bounds
480
- // how far a forged proof can push the freshness ratchets, so a real owner recovers with a
481
- // token issued minutes later rather than days.
460
+ // Bound the issue time before storing it as a freshness floor.
482
461
  if (iat > block.timestamp + FUTURE_SLACK) revert ProofFromFuture();
483
- uint64 rev = _revision(verifier);
462
+ (bool revOk, uint64 rev) = _revision(verifier);
463
+ if (!revOk) revert RevisionUnavailable(verifier);
484
464
  bool fresh = ownerRevision[verifier] == rev;
485
- // The ratchet survives a revision change: a void cache pays nobody, but proofs it already
486
- // retired stay retired, so re-proving takes a proof at least as recent as the last one.
465
+ // Revision changes do not reset this verifier's stored issue-time floor.
487
466
  uint64 latest = latestProofIat[verifier];
488
467
  if (iat < latest) revert ProofTooOld();
489
468
  if (iat > latest || owner[verifier] == address(0)) {
@@ -502,7 +481,7 @@ contract P2IDVault is IP2IDVault {
502
481
  // wallet slot of the same token). The owner stays, and constrained paths pay `wallet`.
503
482
  }
504
483
 
505
- /// @dev Constrained-bucket entry: default buckets are only reachable through sweep().
484
+ /// @dev Verify a constrained claim; reject zero commitments at this entry point.
506
485
  function _verifyForThisVault(
507
486
  address verifier,
508
487
  bytes calldata proof,
@@ -512,14 +491,13 @@ contract P2IDVault is IP2IDVault {
512
491
  return _present(verifier, proof, constraint);
513
492
  }
514
493
 
515
- /// @dev Direct transfers go to `verifier`'s owner only if it is the current default and its
516
- /// owner proof is at least as fresh as the newest proof any default has seen.
494
+ /// @dev Whether verifier is the current default, its revision matches the cache, and its
495
+ /// stored proof time meets untrackedProofIat. Does not check for a nonzero owner.
517
496
  function _paysUntracked(address verifier) private view returns (bool) {
518
497
  return verifier == defaultVerifier() && _ownerFresh(verifier) && latestProofIat[verifier] >= untrackedProofIat;
519
498
  }
520
499
 
521
- /// @dev Owner wallet for proof-less default sweeps; the verifier must still be claimable and
522
- /// must not have revoked anything since the owner was proven.
500
+ /// @dev Require a claimable verifier, nonzero cached owner and matching readable revision.
523
501
  function _ownerOf(address verifier) private view onlyClaimable(verifier) returns (address to) {
524
502
  to = owner[verifier];
525
503
  if (to == address(0)) revert OwnerNotInitialized();
@@ -528,7 +506,8 @@ contract P2IDVault is IP2IDVault {
528
506
 
529
507
  /// @dev Whether the cached owner was proven under the verifier's current revision.
530
508
  function _ownerFresh(address verifier) private view returns (bool) {
531
- return ownerRevision[verifier] == _revision(verifier);
509
+ (bool ok, uint64 rev) = _revision(verifier);
510
+ return ok && ownerRevision[verifier] == rev;
532
511
  }
533
512
 
534
513
  function _noConstraint() private pure returns (IP2IDVerifier.Constraint memory c) {
@@ -536,9 +515,8 @@ contract P2IDVault is IP2IDVault {
536
515
  c.signature = "";
537
516
  }
538
517
 
539
- /// @dev Default-bucket sweep for one verifier: the default deposits consumed here at their
540
- /// fixed rates, plus the untracked surplus (at the rate quoted now) when `verifier` is the
541
- /// factory default.
518
+ /// @dev Consume a page of default-bucket deposits at their stored fee rates. Include untracked
519
+ /// surplus at the current quoted rate when _paysUntracked returns true.
542
520
  function _sweepDefault(address verifier, address token, uint256 depositCountLimit)
543
521
  private
544
522
  returns (uint256 amount, uint256 consumed)
@@ -569,9 +547,9 @@ contract P2IDVault is IP2IDVault {
569
547
  uint256[] storage ids = _bucketDeposits[verifier][constraint][token];
570
548
  uint256 i = bucketCursor[verifier][constraint][token];
571
549
  uint256 end = ids.length;
572
- // Bound by records visited, not records consumed: a run of already-consumed records
573
- // (refunded, or swept by id) is paged through at a fixed cost per call.
574
- if (depositCountLimit != 0 && end - i > depositCountLimit) end = i + depositCountLimit;
550
+ // Limit records visited, including already-consumed entries, to MAX_SWEEP_PAGE.
551
+ uint256 page = (depositCountLimit == 0 || depositCountLimit > MAX_SWEEP_PAGE) ? MAX_SWEEP_PAGE : depositCountLimit;
552
+ if (end - i > page) end = i + page;
575
553
  while (i < end) {
576
554
  uint256 id = ids[i];
577
555
  Deposit storage deposit = deposits[id];
@@ -656,10 +634,12 @@ contract P2IDVault is IP2IDVault {
656
634
  return v > MAX_FEE_BPS ? MAX_FEE_BPS : uint16(v);
657
635
  }
658
636
 
659
- /// @dev The verifier's revision; 0 if it does not report one (nothing it trusts is revocable).
660
- function _revision(address verifier) private view returns (uint64) {
661
- (bool ok, uint256 v) = _query(verifier, abi.encodeCall(IP2IDVerifier.revision, ()));
662
- return ok ? uint64(v) : 0;
637
+ /// @dev Query revision() with QUERY_GAS; ok is false on call failure, wrong length or uint64 overflow.
638
+ function _revision(address verifier) private view returns (bool ok, uint64 rev) {
639
+ uint256 v;
640
+ (ok, v) = _query(verifier, abi.encodeCall(IP2IDVerifier.revision, ()));
641
+ if (ok && v > type(uint64).max) ok = false;
642
+ rev = uint64(v);
663
643
  }
664
644
 
665
645
  /// @dev Whether `verifier` declares it can satisfy constraints; false if it does not say.
@@ -678,8 +658,8 @@ contract P2IDVault is IP2IDVault {
678
658
 
679
659
  // ------------------------------------------------------------------ tokens
680
660
 
681
- /// @dev Pay out `amount` of `token`; native coin goes with all remaining gas (every caller
682
- /// holds the reentrancy lock), so smart-contract wallets can receive it.
661
+ /// @dev Transfer tokens or call the native recipient without an explicit gas limit.
662
+ /// Entry points reaching this helper hold the reentrancy lock.
683
663
  function _transfer(address token, address to, uint256 amount) private {
684
664
  if (token == NATIVE) {
685
665
  (bool ok, ) = payable(to).call{value: amount}("");
@@ -709,8 +689,7 @@ contract P2IDVault is IP2IDVault {
709
689
  _;
710
690
  }
711
691
 
712
- /// @dev Claiming: through the factory's current default verifier always (see the contract
713
- /// notes), otherwise only while the policy allows the verifier.
692
+ /// @dev Skip policy approval for the current default verifier; check approval for other verifiers.
714
693
  modifier onlyClaimable(address verifier) {
715
694
  if (verifier != defaultVerifier() && !_policy().isVerifierAllowed(verifier)) revert VerifierNotApproved(verifier);
716
695
  _;
@@ -721,14 +700,14 @@ contract P2IDVault is IP2IDVault {
721
700
  _;
722
701
  }
723
702
 
724
- /// @dev Proof-less default-bucket payouts need an owner wallet under an allowed verifier.
703
+ /// @dev Require a cached owner that passes _ownerOf's claimability and revision checks.
725
704
  modifier onlyInitialized(address verifier) {
726
705
  _ownerOf(verifier);
727
706
  _;
728
707
  }
729
708
 
730
709
  modifier nonReentrant() {
731
- if (reentrancyLock != 1) revert Reentrancy();
710
+ if (reentrancyLock == 2) revert Reentrancy();
732
711
  reentrancyLock = 2;
733
712
  _;
734
713
  reentrancyLock = 1;