@pvium/p2id-core 0.1.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.
Files changed (41) hide show
  1. package/README.md +239 -0
  2. package/contracts/P2IDVault.sol +736 -0
  3. package/contracts/PviumIdentity.sol +268 -0
  4. package/contracts/PviumP2IDPolicy.sol +78 -0
  5. package/contracts/PviumP2IdVaultFactory.sol +327 -0
  6. package/contracts/PviumVerifier.sol +165 -0
  7. package/contracts/PviumZKVerifier.sol +2465 -0
  8. package/contracts/interfaces/IP2IDPolicy.sol +39 -0
  9. package/contracts/interfaces/IP2IDVault.sol +112 -0
  10. package/contracts/interfaces/IP2IDVerifier.sol +44 -0
  11. package/contracts/interfaces/IP2IdVaultFactory.sol +49 -0
  12. package/contracts/interfaces/IPviumIdentity.sol +28 -0
  13. package/contracts/lib/P2IDHash.sol +63 -0
  14. package/dist/cjs/identity.js +68 -0
  15. package/dist/cjs/identityNames.js +49 -0
  16. package/dist/cjs/index.js +24 -0
  17. package/dist/cjs/p2id.js +91 -0
  18. package/dist/cjs/p2idConstants.js +16 -0
  19. package/dist/cjs/package.json +3 -0
  20. package/dist/esm/identity.d.ts +38 -0
  21. package/dist/esm/identity.js +61 -0
  22. package/dist/esm/identityNames.d.ts +43 -0
  23. package/dist/esm/identityNames.js +44 -0
  24. package/dist/esm/index.d.ts +5 -0
  25. package/dist/esm/index.js +5 -0
  26. package/dist/esm/p2id.d.ts +54 -0
  27. package/dist/esm/p2id.js +82 -0
  28. package/dist/esm/p2idConstants.d.ts +30 -0
  29. package/dist/esm/p2idConstants.js +13 -0
  30. package/dist/esm/package.json +3 -0
  31. package/dist/identity.d.ts +38 -0
  32. package/dist/identity.js +61 -0
  33. package/dist/identityNames.d.ts +43 -0
  34. package/dist/identityNames.js +44 -0
  35. package/dist/index.d.ts +5 -0
  36. package/dist/index.js +5 -0
  37. package/dist/p2id.d.ts +54 -0
  38. package/dist/p2id.js +82 -0
  39. package/dist/p2idConstants.d.ts +38 -0
  40. package/dist/p2idConstants.js +21 -0
  41. package/package.json +44 -0
@@ -0,0 +1,268 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.27;
3
+
4
+ import {IVerifier} from "./PviumZKVerifier.sol";
5
+ import {IPviumIdentity} from "./interfaces/IPviumIdentity.sol";
6
+
7
+ /// @title PviumIdentity
8
+ /// @notice On-chain verifier for Pvium attestations: one deployment per circuit version. The
9
+ /// accepted signer keys start as the Privy app's JWKS at deployment (Privy publishes more
10
+ /// than one key per app and may sign with any of them). The owner manages the set so a
11
+ /// Privy key rotation does not strand deposits: adding a key is proposed with the JWKS URL
12
+ /// and key id it can be checked at, and takes effect after SIGNER_KEY_DELAY (7 days);
13
+ /// removing one is immediate, because a leaked key must stop being trusted at once. A key
14
+ /// proposal is the strongest power here (a forged key forges proofs), which is why it is
15
+ /// public for 7 days: anyone can verify the key at the URL, and payees and funders can
16
+ /// claim or refund before it activates. An owner of address(0) makes the set permanent.
17
+ /// A new circuit build is still a new deployment (plus a new PviumVerifier), which the
18
+ /// P2ID vault factory registers alongside the old one. Developers call it through IPviumIdentity.
19
+ /// @dev Public input layout emitted by circuit/src/main.nr:
20
+ /// [0] identity_type [1] wallet (EVM address checked in-circuit against the token; 0 if none)
21
+ /// [2] signer_x_hi [3] signer_x_lo [4] signer_y_hi [5] signer_y_lo
22
+ /// [6] iat [7] identity_hash_hi [8] identity_hash_lo
23
+ /// [9] wallet_hash_hi [10] wallet_hash_lo (zero when the proof carries no wallet)
24
+ /// Hashes and coordinates are split into two 128-bit halves because a 256-bit value does
25
+ /// not fit in one BN254 field element.
26
+ contract PviumIdentity is IPviumIdentity {
27
+ uint256 public constant PUBLIC_INPUT_COUNT = 11;
28
+
29
+ IVerifier public immutable verifier;
30
+ /// @notice Circuit version this deployment verifies (see circuit/version.json). One deployment per version.
31
+ uint16 public immutable circuitVersion;
32
+ /// @notice Notice a key proposal gives before it can be activated.
33
+ uint64 public constant SIGNER_KEY_DELAY = 7 days;
34
+ /// @notice Number of accepted signer keys. Zero (every key revoked) refuses every proof until a
35
+ /// proposal matures: a freeze, never a payout.
36
+ uint256 public signerKeyCount;
37
+ /// @notice Incremented whenever a key is revoked, i.e. whenever proofs that verified before may
38
+ /// no longer be trusted. Vaults discard wallets cached under an older revision.
39
+ uint64 public keySetRevision;
40
+ /// @dev Accepted signer keys, keyed by signerKeyHash(x, y).
41
+ mapping(bytes32 keyHash => bool) private _signerKeys;
42
+ /// @notice Manages the signer key set (two-step transfer); address(0) = the set is permanent.
43
+ address public owner;
44
+ address public pendingOwner;
45
+
46
+ struct SignerKeyProposal {
47
+ uint256 x;
48
+ uint256 y;
49
+ /// Earliest activation time; zero when there is no proposal for this key.
50
+ uint64 eta;
51
+ /// Where the key is published: the JWKS URL and the key id in it, for anyone to check.
52
+ string url;
53
+ string kid;
54
+ }
55
+ /// @notice Pending key proposals, keyed by signerKeyHash(x, y).
56
+ mapping(bytes32 keyHash => SignerKeyProposal) public signerKeyProposals;
57
+
58
+ event SignerKeyProposed(uint256 indexed x, uint256 indexed y, uint64 eta, string url, string kid);
59
+ event SignerKeyProposalCancelled(uint256 indexed x, uint256 indexed y);
60
+ event SignerKeyAdded(uint256 indexed x, uint256 indexed y);
61
+ event SignerKeyRemoved(uint256 indexed x, uint256 indexed y);
62
+ event OwnershipTransferStarted(address indexed from, address indexed to);
63
+ event OwnershipTransferred(address indexed from, address indexed to);
64
+
65
+ /// @notice Everything an attestation asserts, for Pvium's own contracts (e.g. vault claims
66
+ /// that pay `wallet`). Developers should use the IPviumIdentity functions.
67
+ struct Attestation {
68
+ uint8 identityType;
69
+ /// EVM address of the wallet linked in the same token. The circuit decodes it from the
70
+ /// signed token and asserts equality, so it is never prover-chosen. Zero when the proof
71
+ /// carries no wallet or the wallet is not an EVM address (then only walletHash is set).
72
+ address wallet;
73
+ /// When Privy issued the token (unix seconds). Freshness policy is the caller's.
74
+ uint64 iat;
75
+ bytes32 identityHash;
76
+ /// P2IDHash.walletHash of a wallet linked in the same token, or 0.
77
+ bytes32 walletHash;
78
+ }
79
+
80
+ error InvalidVerifier();
81
+ error InvalidProof();
82
+ error UnknownSigner(bytes32 x, bytes32 y);
83
+ error WrongPublicInputCount(uint256 got);
84
+ error InvalidPublicKey();
85
+ error NoSignerKeys();
86
+ error DuplicateSignerKey(uint256 x, uint256 y);
87
+ error InvalidCircuitVersion();
88
+ error IdentityTypeMismatch(uint8 expected, uint8 got);
89
+ error IdentityMismatch();
90
+ error NoWallet();
91
+ error WalletMismatch();
92
+ error NotOwner();
93
+ error NotPendingOwner();
94
+ error KeyAlreadyAccepted(uint256 x, uint256 y);
95
+ error NothingProposed(uint256 x, uint256 y);
96
+ error TimelockNotElapsed(uint64 eta);
97
+ error UnknownKey(uint256 x, uint256 y);
98
+
99
+ /// @param _owner Manages the key set; address(0) fixes the set given here forever.
100
+ /// @param _signerXs/_signerYs Raw P-256 coordinates of every key the Privy app signs with.
101
+ constructor(
102
+ IVerifier _verifier,
103
+ uint16 _circuitVersion,
104
+ address _owner,
105
+ uint256[] memory _signerXs,
106
+ uint256[] memory _signerYs
107
+ ) {
108
+ if (address(_verifier).code.length == 0) revert InvalidVerifier();
109
+ if (_circuitVersion == 0) revert InvalidCircuitVersion();
110
+ if (_signerXs.length == 0 || _signerXs.length != _signerYs.length) revert NoSignerKeys();
111
+ for (uint256 i = 0; i < _signerXs.length; i++) {
112
+ (uint256 x, uint256 y) = (_signerXs[i], _signerYs[i]);
113
+ if (!_isOnCurve(x, y)) revert InvalidPublicKey();
114
+ bytes32 h = signerKeyHash(x, y);
115
+ if (_signerKeys[h]) revert DuplicateSignerKey(x, y);
116
+ _signerKeys[h] = true;
117
+ }
118
+ verifier = _verifier;
119
+ circuitVersion = _circuitVersion;
120
+ signerKeyCount = _signerXs.length;
121
+ owner = _owner;
122
+ emit OwnershipTransferred(address(0), _owner);
123
+ }
124
+
125
+ // ---- signer key management ------------------------------------------------------------
126
+
127
+ /// @notice Propose accepting a key, published at `url` under `kid`, after SIGNER_KEY_DELAY.
128
+ /// Re-proposing a key restarts its delay.
129
+ function proposeSignerKey(uint256 x, uint256 y, string calldata url, string calldata kid) external onlyOwner {
130
+ if (!_isOnCurve(x, y)) revert InvalidPublicKey();
131
+ bytes32 h = signerKeyHash(x, y);
132
+ if (_signerKeys[h]) revert KeyAlreadyAccepted(x, y);
133
+ uint64 eta = uint64(block.timestamp) + SIGNER_KEY_DELAY;
134
+ signerKeyProposals[h] = SignerKeyProposal({x: x, y: y, eta: eta, url: url, kid: kid});
135
+ emit SignerKeyProposed(x, y, eta, url, kid);
136
+ }
137
+
138
+ function cancelSignerKeyProposal(uint256 x, uint256 y) external onlyOwner {
139
+ bytes32 h = signerKeyHash(x, y);
140
+ if (signerKeyProposals[h].eta == 0) revert NothingProposed(x, y);
141
+ delete signerKeyProposals[h];
142
+ emit SignerKeyProposalCancelled(x, y);
143
+ }
144
+
145
+ /// @notice Accept a proposed key once its delay has passed. Anyone may call it: the owner
146
+ /// already decided by proposing, and can still cancel until this runs.
147
+ function activateSignerKey(uint256 x, uint256 y) external {
148
+ bytes32 h = signerKeyHash(x, y);
149
+ uint64 eta = signerKeyProposals[h].eta;
150
+ if (eta == 0) revert NothingProposed(x, y);
151
+ if (block.timestamp < eta) revert TimelockNotElapsed(eta);
152
+ delete signerKeyProposals[h];
153
+ if (_signerKeys[h]) revert KeyAlreadyAccepted(x, y);
154
+ _signerKeys[h] = true;
155
+ signerKeyCount++;
156
+ emit SignerKeyAdded(x, y);
157
+ }
158
+
159
+ /// @notice Stop accepting a key at once (e.g. Privy retired it, or it leaked). Proofs made from
160
+ /// tokens it signed fail from now on, and wallets that vaults cached from any proof are
161
+ /// discarded (keySetRevision). Removing the last key is allowed: it freezes claims,
162
+ /// which is the safe state while a compromised key is replaced.
163
+ function removeSignerKey(uint256 x, uint256 y) external onlyOwner {
164
+ bytes32 h = signerKeyHash(x, y);
165
+ if (!_signerKeys[h]) revert UnknownKey(x, y);
166
+ _signerKeys[h] = false;
167
+ signerKeyCount--;
168
+ keySetRevision++;
169
+ emit SignerKeyRemoved(x, y);
170
+ }
171
+
172
+ function transferOwnership(address to) external onlyOwner {
173
+ pendingOwner = to;
174
+ emit OwnershipTransferStarted(owner, to);
175
+ }
176
+
177
+ function acceptOwnership() external {
178
+ if (msg.sender != pendingOwner) revert NotPendingOwner();
179
+ emit OwnershipTransferred(owner, msg.sender);
180
+ owner = msg.sender;
181
+ pendingOwner = address(0);
182
+ }
183
+
184
+ modifier onlyOwner() {
185
+ if (msg.sender != owner || owner == address(0)) revert NotOwner();
186
+ _;
187
+ }
188
+
189
+ /// @inheritdoc IPviumIdentity
190
+ function isSignerKey(uint256 x, uint256 y) public view returns (bool) {
191
+ return _signerKeys[signerKeyHash(x, y)];
192
+ }
193
+
194
+ function signerKeyHash(uint256 x, uint256 y) public pure returns (bytes32) {
195
+ return keccak256(abi.encode(x, y));
196
+ }
197
+
198
+ // ---- developer-facing ---------------------------------------------------------------
199
+
200
+ /// @inheritdoc IPviumIdentity
201
+ function verifyIdentity(
202
+ bytes calldata proof,
203
+ bytes32[] calldata publicInputs,
204
+ uint8 identityType,
205
+ bytes32 identityHash,
206
+ bytes32 walletHash
207
+ ) external view returns (uint64 issuedAt) {
208
+ return _verifyIdentity(proof, publicInputs, identityType, identityHash, walletHash);
209
+ }
210
+
211
+ // ---- lower level ----------------------------------------------------------------------
212
+
213
+ /// @notice Verify a proof and return everything it asserts. Reverts unless the proof is valid
214
+ /// and was produced from a token signed by one of the accepted keys.
215
+ function verifyAttestation(bytes calldata proof, bytes32[] calldata publicInputs)
216
+ public
217
+ view
218
+ returns (Attestation memory a)
219
+ {
220
+ a = _decode(publicInputs);
221
+ if (!verifier.verify(proof, publicInputs)) revert InvalidProof();
222
+ }
223
+
224
+ // ---- internals -----------------------------------------------------------------------
225
+
226
+ function _verifyIdentity(
227
+ bytes calldata proof,
228
+ bytes32[] calldata publicInputs,
229
+ uint8 identityType,
230
+ bytes32 identityHash,
231
+ bytes32 walletHash
232
+ ) internal view returns (uint64) {
233
+ Attestation memory a = _decode(publicInputs);
234
+ if (a.identityType != identityType) revert IdentityTypeMismatch(identityType, a.identityType);
235
+ if (a.identityHash != identityHash) revert IdentityMismatch();
236
+ if (a.walletHash == bytes32(0)) revert NoWallet();
237
+ if (a.walletHash != walletHash) revert WalletMismatch();
238
+ if (!verifier.verify(proof, publicInputs)) revert InvalidProof();
239
+ return a.iat;
240
+ }
241
+
242
+ /// @dev Cheap checks first (layout, signer), so bad requests fail before the 4M-gas verify.
243
+ function _decode(bytes32[] calldata publicInputs) internal view returns (Attestation memory a) {
244
+ if (publicInputs.length != PUBLIC_INPUT_COUNT) revert WrongPublicInputCount(publicInputs.length);
245
+ bytes32 x = _join(publicInputs[2], publicInputs[3]);
246
+ bytes32 y = _join(publicInputs[4], publicInputs[5]);
247
+ if (!isSignerKey(uint256(x), uint256(y))) revert UnknownSigner(x, y);
248
+ a.identityType = uint8(uint256(publicInputs[0]));
249
+ a.wallet = address(uint160(uint256(publicInputs[1])));
250
+ a.iat = uint64(uint256(publicInputs[6]));
251
+ a.identityHash = _join(publicInputs[7], publicInputs[8]);
252
+ a.walletHash = _join(publicInputs[9], publicInputs[10]);
253
+ }
254
+
255
+ function _join(bytes32 hi, bytes32 lo) internal pure returns (bytes32) {
256
+ return bytes32((uint256(hi) << 128) | uint256(lo));
257
+ }
258
+
259
+ /// @dev y^2 == x^3 - 3x + b (mod p) on NIST P-256. Guards against registering a typo.
260
+ function _isOnCurve(uint256 x, uint256 y) internal pure returns (bool) {
261
+ uint256 p = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF;
262
+ uint256 b = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B;
263
+ if (x >= p || y >= p) return false;
264
+ uint256 lhs = mulmod(y, y, p);
265
+ uint256 rhs = addmod(addmod(mulmod(mulmod(x, x, p), x, p), p - mulmod(3, x, p), p), b, p);
266
+ return lhs == rhs;
267
+ }
268
+ }
@@ -0,0 +1,78 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.27;
3
+
4
+ import {IP2IDPolicy} from "./interfaces/IP2IDPolicy.sol";
5
+
6
+ /// @title PviumP2IDPolicy
7
+ /// @notice The launch policy for P2ID vaults: an owner-managed allowlist of verifiers, and no fee.
8
+ /// Later capabilities (permissionless verifier registration with a stake, protocol fees)
9
+ /// ship as a new policy that the vault factory switches to through its timelock; the
10
+ /// vaults and their addresses do not change.
11
+ /// @dev `owner` should be a Pvium multisig. Approving a verifier only lets payers opt into it;
12
+ /// revoking one freezes claims under it (refunds still work) and never moves funds.
13
+ contract PviumP2IDPolicy is IP2IDPolicy {
14
+ address public owner;
15
+ address public pendingOwner;
16
+ mapping(address verifier => bool) public approvedVerifiers;
17
+
18
+ event VerifierApprovalSet(address indexed verifier, bool approved);
19
+ event OwnershipTransferStarted(address indexed from, address indexed to);
20
+ event OwnershipTransferred(address indexed from, address indexed to);
21
+
22
+ error NotOwner();
23
+ error NotPendingOwner();
24
+ error InvalidOwner();
25
+ error InvalidVerifier();
26
+ error NoFees();
27
+
28
+ constructor(address _owner, address[] memory _verifiers) {
29
+ if (_owner == address(0)) revert InvalidOwner();
30
+ owner = _owner;
31
+ emit OwnershipTransferred(address(0), _owner);
32
+ for (uint256 i = 0; i < _verifiers.length; i++) _setApproval(_verifiers[i], true);
33
+ }
34
+
35
+ /// @notice Approve (or revoke) a verifier.
36
+ function approveVerifier(address verifier, bool approved) external onlyOwner {
37
+ _setApproval(verifier, approved);
38
+ }
39
+
40
+ /// @inheritdoc IP2IDPolicy
41
+ function isVerifierAllowed(address verifier) external view returns (bool) {
42
+ return approvedVerifiers[verifier];
43
+ }
44
+
45
+ /// @inheritdoc IP2IDPolicy
46
+ function feeBps(address, address) external pure returns (uint16) {
47
+ return 0;
48
+ }
49
+
50
+ /// @inheritdoc IP2IDPolicy
51
+ /// @dev The launch policy charges no fee, so a vault never has fees for it to distribute.
52
+ function distributeFee(address, address, uint256) external payable {
53
+ revert NoFees();
54
+ }
55
+
56
+ function transferOwnership(address to) external onlyOwner {
57
+ pendingOwner = to;
58
+ emit OwnershipTransferStarted(owner, to);
59
+ }
60
+
61
+ function acceptOwnership() external {
62
+ if (msg.sender != pendingOwner) revert NotPendingOwner();
63
+ emit OwnershipTransferred(owner, msg.sender);
64
+ owner = msg.sender;
65
+ pendingOwner = address(0);
66
+ }
67
+
68
+ function _setApproval(address verifier, bool approved) private {
69
+ if (approved && verifier.code.length == 0) revert InvalidVerifier();
70
+ approvedVerifiers[verifier] = approved;
71
+ emit VerifierApprovalSet(verifier, approved);
72
+ }
73
+
74
+ modifier onlyOwner() {
75
+ if (msg.sender != owner) revert NotOwner();
76
+ _;
77
+ }
78
+ }
@@ -0,0 +1,327 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.27;
3
+
4
+ import {IP2IdVaultFactory} from "./interfaces/IP2IdVaultFactory.sol";
5
+ import {IP2IDPolicy} from "./interfaces/IP2IDPolicy.sol";
6
+ import {P2IDVault} from "./P2IDVault.sol";
7
+
8
+ /// @title PviumP2IdVaultFactory
9
+ /// @notice Deploys P2IDVaults with CREATE2, salted by identity hash, and points them at the
10
+ /// current policy (which verifiers are allowed, what capped fee applies) and default verifier. A vault takes no constructor arguments
11
+ /// (the factory calls `initialize` right after `new`), so its creation code is a constant
12
+ /// and an identity's vault address is
13
+ /// `keccak256(0xff ‖ factory ‖ identityHash ‖ keccak256(creationCode))`, computable
14
+ /// anywhere from two constants. Funds sent to that address before deployment are swept
15
+ /// by the owner after deployment (see P2IDVault: bare transfers are untracked and irrevocable).
16
+ /// @dev Everything expected to evolve is in the policy (IP2IDPolicy), so this factory and the vault
17
+ /// code, which fix every P2ID address, never have to change. Both move only through a
18
+ /// timelock (propose, wait, activate): the policy after `policyChangeDelay`, the default
19
+ /// verifier after DEFAULT_VERIFIER_DELAY, a fixed 14 days. Direct transfers follow the default
20
+ /// verifier and have no refund path, so a change to it gets the longer notice: anyone who
21
+ /// objects can sweep first, and the vault guarantees nothing can freeze their claim meanwhile.
22
+ /// What a policy can do is bounded by the vault (fee cap, fees fixed at funding, no fee on
23
+ /// refunds, never redirecting a payout, never blocking claims through the default verifier).
24
+ contract PviumP2IdVaultFactory is IP2IdVaultFactory {
25
+ bytes32 public immutable nsHash;
26
+ uint64 public immutable minRefundWindow;
27
+ uint64 public immutable maxRefundWindow;
28
+
29
+ /// @notice Proposes and activates policy / default-verifier changes (a Pvium multisig). Two-step transfer.
30
+ address public owner;
31
+ address public pendingOwner;
32
+
33
+ /// @notice The policy every vault consults. Changes only via the timelock below.
34
+ address public policy;
35
+ /// @notice Verifier used when a payer does not choose one. Changes only via the timelock below.
36
+ address public defaultVerifier;
37
+
38
+ /// @notice Notice before a proposed default verifier can be activated. Fixed in this bytecode.
39
+ uint64 public constant DEFAULT_VERIFIER_DELAY = 14 days;
40
+ /// @notice Notice before a proposed policy can be activated.
41
+ uint64 public immutable policyChangeDelay;
42
+ address public proposedDefaultVerifier;
43
+ /// @notice Earliest time the proposed default can be activated; 0 when nothing is proposed.
44
+ uint64 public proposedDefaultEta;
45
+ address public proposedPolicy;
46
+ /// @notice Earliest time the proposed policy can be activated; 0 when nothing is proposed.
47
+ uint64 public proposedPolicyEta;
48
+
49
+ event OwnershipTransferStarted(address indexed from, address indexed to);
50
+ event OwnershipTransferred(address indexed from, address indexed to);
51
+ event DefaultVerifierProposed(address indexed verifier, uint64 eta);
52
+ event DefaultVerifierProposalCancelled(address indexed verifier);
53
+ event DefaultVerifierActivated(address indexed verifier);
54
+ event PolicyProposed(address indexed policy, uint64 eta);
55
+ event PolicyProposalCancelled(address indexed policy);
56
+ event PolicyActivated(address indexed policy);
57
+
58
+ error NotOwner();
59
+ error NotPendingOwner();
60
+ error InvalidOwner();
61
+ error InvalidPolicy();
62
+ error VerifierNotApproved(address verifier);
63
+ error NothingProposed();
64
+ error TimelockNotElapsed(uint64 eta);
65
+ error InvalidRefundWindow();
66
+ error TokenCallFailed();
67
+ error TokenBalanceQueryFailed();
68
+ error NothingReceived();
69
+ error UnexpectedValue();
70
+
71
+ constructor(
72
+ address _owner,
73
+ bytes32 _nsHash,
74
+ address _policy,
75
+ address _defaultVerifier,
76
+ uint64 _policyChangeDelay,
77
+ uint64 _minRefundWindow,
78
+ uint64 _maxRefundWindow
79
+ ) {
80
+ if (_owner == address(0)) revert InvalidOwner();
81
+ if (_minRefundWindow > _maxRefundWindow) revert InvalidRefundWindow();
82
+ if (_policy.code.length == 0) revert InvalidPolicy();
83
+ if (!IP2IDPolicy(_policy).isVerifierAllowed(_defaultVerifier)) revert VerifierNotApproved(_defaultVerifier);
84
+ owner = _owner;
85
+ emit OwnershipTransferred(address(0), _owner);
86
+ nsHash = _nsHash;
87
+ policyChangeDelay = _policyChangeDelay;
88
+ minRefundWindow = _minRefundWindow;
89
+ maxRefundWindow = _maxRefundWindow;
90
+ policy = _policy;
91
+ emit PolicyActivated(_policy);
92
+ defaultVerifier = _defaultVerifier;
93
+ emit DefaultVerifierActivated(_defaultVerifier);
94
+ }
95
+
96
+ // ------------------------------------------------------------------ policy (timelocked)
97
+
98
+ /// @notice Announce a new policy. Takes effect only after `policyChangeDelay`, via
99
+ /// activatePolicy(). Replaces any pending policy proposal.
100
+ function proposePolicy(address newPolicy) external onlyOwner {
101
+ if (newPolicy.code.length == 0) revert InvalidPolicy();
102
+ proposedPolicy = newPolicy;
103
+ proposedPolicyEta = uint64(block.timestamp) + policyChangeDelay;
104
+ emit PolicyProposed(newPolicy, proposedPolicyEta);
105
+ }
106
+
107
+ function cancelPolicyProposal() external onlyOwner {
108
+ if (proposedPolicyEta == 0) revert NothingProposed();
109
+ emit PolicyProposalCancelled(proposedPolicy);
110
+ delete proposedPolicy;
111
+ delete proposedPolicyEta;
112
+ }
113
+
114
+ /// @notice Switch every vault to the proposed policy once the delay has elapsed. The new policy
115
+ /// must allow the current default verifier, so bare transfers stay claimable.
116
+ function activatePolicy() external onlyOwner {
117
+ uint64 eta = proposedPolicyEta;
118
+ if (eta == 0) revert NothingProposed();
119
+ if (block.timestamp < eta) revert TimelockNotElapsed(eta);
120
+ address newPolicy = proposedPolicy;
121
+ if (!IP2IDPolicy(newPolicy).isVerifierAllowed(defaultVerifier)) revert VerifierNotApproved(defaultVerifier);
122
+ delete proposedPolicy;
123
+ delete proposedPolicyEta;
124
+ policy = newPolicy;
125
+ emit PolicyActivated(newPolicy);
126
+ }
127
+
128
+ // ------------------------------------------------------------------ default verifier (timelocked)
129
+
130
+ /// @notice Announce a new default verifier (must be allowed by the policy). Takes effect only
131
+ /// after DEFAULT_VERIFIER_DELAY (14 days), via activateDefaultVerifier(). Replaces any
132
+ /// pending proposal.
133
+ function proposeDefaultVerifier(address verifier) external onlyOwner {
134
+ if (!IP2IDPolicy(policy).isVerifierAllowed(verifier)) revert VerifierNotApproved(verifier);
135
+ proposedDefaultVerifier = verifier;
136
+ proposedDefaultEta = uint64(block.timestamp) + DEFAULT_VERIFIER_DELAY;
137
+ emit DefaultVerifierProposed(verifier, proposedDefaultEta);
138
+ }
139
+
140
+ function cancelDefaultVerifierProposal() external onlyOwner {
141
+ if (proposedDefaultEta == 0) revert NothingProposed();
142
+ emit DefaultVerifierProposalCancelled(proposedDefaultVerifier);
143
+ delete proposedDefaultVerifier;
144
+ delete proposedDefaultEta;
145
+ }
146
+
147
+ /// @notice Make the proposed verifier the default once the delay has elapsed. The policy must
148
+ /// still allow it (a revocation in the meantime cancels it in effect).
149
+ function activateDefaultVerifier() external onlyOwner {
150
+ uint64 eta = proposedDefaultEta;
151
+ if (eta == 0) revert NothingProposed();
152
+ if (block.timestamp < eta) revert TimelockNotElapsed(eta);
153
+ address verifier = proposedDefaultVerifier;
154
+ if (!IP2IDPolicy(policy).isVerifierAllowed(verifier)) revert VerifierNotApproved(verifier);
155
+ delete proposedDefaultVerifier;
156
+ delete proposedDefaultEta;
157
+ defaultVerifier = verifier;
158
+ emit DefaultVerifierActivated(verifier);
159
+ }
160
+
161
+ function transferOwnership(address to) external onlyOwner {
162
+ pendingOwner = to;
163
+ emit OwnershipTransferStarted(owner, to);
164
+ }
165
+
166
+ function acceptOwnership() external {
167
+ if (msg.sender != pendingOwner) revert NotPendingOwner();
168
+ emit OwnershipTransferred(owner, msg.sender);
169
+ owner = msg.sender;
170
+ pendingOwner = address(0);
171
+ }
172
+
173
+ // ------------------------------------------------------------------ vaults
174
+
175
+ /// @inheritdoc IP2IdVaultFactory
176
+ function p2idVersion() external pure returns (string memory) {
177
+ return "p2id.factory.v1";
178
+ }
179
+
180
+ /// @inheritdoc IP2IdVaultFactory
181
+ function vaultFor(bytes32 identityHash) public view returns (address) {
182
+ return
183
+ address(
184
+ uint160(
185
+ uint256(
186
+ keccak256(
187
+ abi.encodePacked(
188
+ bytes1(0xff),
189
+ address(this),
190
+ identityHash,
191
+ initCodeHash()
192
+ )
193
+ )
194
+ )
195
+ )
196
+ );
197
+ }
198
+
199
+ /// @inheritdoc IP2IdVaultFactory
200
+ function isDeployed(bytes32 identityHash) public view returns (bool) {
201
+ return vaultFor(identityHash).code.length != 0;
202
+ }
203
+
204
+ /// @notice keccak256 of the P2IDVault creation code (no constructor arguments), for offline derivation.
205
+ function initCodeHash() public pure returns (bytes32) {
206
+ return keccak256(type(P2IDVault).creationCode);
207
+ }
208
+
209
+ /// @inheritdoc IP2IdVaultFactory
210
+ function deploy(bytes32 identityHash) public returns (address vault) {
211
+ vault = vaultFor(identityHash);
212
+ if (vault.code.length != 0) return vault;
213
+ P2IDVault v = new P2IDVault{salt: identityHash}();
214
+ v.initialize(nsHash, identityHash, minRefundWindow, maxRefundWindow);
215
+ vault = address(v);
216
+ emit VaultDeployed(identityHash, vault);
217
+ }
218
+
219
+ /// @inheritdoc IP2IdVaultFactory
220
+ function fund(
221
+ bytes32 identityHash,
222
+ address token,
223
+ uint256 amount,
224
+ bytes32 constraint,
225
+ uint64 refundWindow,
226
+ bytes32 ref
227
+ ) external payable returns (address vault, uint256 depositId) {
228
+ return
229
+ _fund(
230
+ identityHash,
231
+ defaultVerifier,
232
+ token,
233
+ amount,
234
+ constraint,
235
+ refundWindow,
236
+ ref
237
+ );
238
+ }
239
+
240
+ /// @inheritdoc IP2IdVaultFactory
241
+ function fundWith(
242
+ bytes32 identityHash,
243
+ address verifier,
244
+ address token,
245
+ uint256 amount,
246
+ bytes32 constraint,
247
+ uint64 refundWindow,
248
+ bytes32 ref
249
+ ) external payable returns (address vault, uint256 depositId) {
250
+ return
251
+ _fund(
252
+ identityHash,
253
+ verifier,
254
+ token,
255
+ amount,
256
+ constraint,
257
+ refundWindow,
258
+ ref
259
+ );
260
+ }
261
+
262
+ function _fund(
263
+ bytes32 identityHash,
264
+ address verifier,
265
+ address token,
266
+ uint256 amount,
267
+ bytes32 constraint,
268
+ uint64 refundWindow,
269
+ bytes32 ref
270
+ ) private returns (address vault, uint256 depositId) {
271
+ vault = deploy(identityHash);
272
+ if (token == address(0)) {
273
+ // Native coin: forward exactly the value sent; the vault checks it equals `amount`.
274
+ return (vault, P2IDVault(payable(vault)).fundFor{value: msg.value}(msg.sender, verifier, token, amount, constraint, refundWindow, ref));
275
+ }
276
+ if (msg.value != 0) revert UnexpectedValue();
277
+ // Collect from the payer (credit what actually arrived, for fee-on-transfer tokens),
278
+ // then let the vault pull exactly that and record the payer as funder.
279
+ uint256 before = _balanceOf(token);
280
+ _callToken(
281
+ token,
282
+ abi.encodeWithSignature(
283
+ "transferFrom(address,address,uint256)",
284
+ msg.sender,
285
+ address(this),
286
+ amount
287
+ )
288
+ );
289
+ uint256 received = _balanceOf(token) - before;
290
+ if (received == 0) revert NothingReceived();
291
+ _callToken(
292
+ token,
293
+ abi.encodeWithSignature("approve(address,uint256)", vault, received)
294
+ );
295
+ depositId = P2IDVault(payable(vault)).fundFor(
296
+ msg.sender,
297
+ verifier,
298
+ token,
299
+ received,
300
+ constraint,
301
+ refundWindow,
302
+ ref
303
+ );
304
+ }
305
+
306
+ function _callToken(address token, bytes memory input) private {
307
+ (bool ok, bytes memory data) = token.call(input);
308
+ if (
309
+ !ok ||
310
+ (data.length != 0 &&
311
+ (data.length != 32 || !abi.decode(data, (bool))))
312
+ ) revert TokenCallFailed();
313
+ }
314
+
315
+ function _balanceOf(address token) private view returns (uint256 balance) {
316
+ (bool ok, bytes memory data) = token.staticcall(
317
+ abi.encodeWithSignature("balanceOf(address)", address(this))
318
+ );
319
+ if (!ok || data.length != 32) revert TokenBalanceQueryFailed();
320
+ balance = abi.decode(data, (uint256));
321
+ }
322
+
323
+ modifier onlyOwner() {
324
+ if (msg.sender != owner) revert NotOwner();
325
+ _;
326
+ }
327
+ }