@pvium/p2id-core 0.1.2 → 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.
@@ -5,17 +5,10 @@ import {IVerifier} from "./PviumZKVerifier.sol";
5
5
  import {IPviumIdentity} from "./interfaces/IPviumIdentity.sol";
6
6
 
7
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.
8
+ /// @notice Verifies attestations using an immutable proof verifier and circuit version.
9
+ /// The constructor supplies the initial P-256 signer keys. The owner can propose additions
10
+ /// with a 7-day activation delay or remove accepted keys immediately.
11
+ /// @dev Proposal URLs and key ids are metadata; this contract does not validate them against JWKS.
19
12
  /// @dev Public input layout emitted by circuit/src/main.nr:
20
13
  /// [0] identity_type [1] wallet (EVM address checked in-circuit against the token; 0 if none)
21
14
  /// [2] signer_x_hi [3] signer_x_lo [4] signer_y_hi [5] signer_y_lo
@@ -27,19 +20,17 @@ contract PviumIdentity is IPviumIdentity {
27
20
  uint256 public constant PUBLIC_INPUT_COUNT = 11;
28
21
 
29
22
  IVerifier public immutable verifier;
30
- /// @notice Circuit version this deployment verifies (see circuit/version.json). One deployment per version.
23
+ /// @notice Circuit version identifier supplied at deployment; not checked against the verifier's bytecode.
31
24
  uint16 public immutable circuitVersion;
32
25
  /// @notice Notice a key proposal gives before it can be activated.
33
26
  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.
27
+ /// @notice Number of accepted signer keys. When zero, the signer check rejects attestations.
36
28
  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.
29
+ /// @notice Incremented by removeSignerKey; exposed to consumers for cache invalidation.
39
30
  uint64 public keySetRevision;
40
31
  /// @dev Accepted signer keys, keyed by signerKeyHash(x, y).
41
32
  mapping(bytes32 keyHash => bool) private _signerKeys;
42
- /// @notice Manages the signer key set (two-step transfer); address(0) = the set is permanent.
33
+ /// @notice Owner authorized to propose/remove keys and transfer ownership; zero disables owner-only calls.
43
34
  address public owner;
44
35
  address public pendingOwner;
45
36
 
@@ -48,7 +39,7 @@ contract PviumIdentity is IPviumIdentity {
48
39
  uint256 y;
49
40
  /// Earliest activation time; zero when there is no proposal for this key.
50
41
  uint64 eta;
51
- /// Where the key is published: the JWKS URL and the key id in it, for anyone to check.
42
+ /// Caller-supplied JWKS URL and key id; stored without publication checks.
52
43
  string url;
53
44
  string kid;
54
45
  }
@@ -62,13 +53,11 @@ contract PviumIdentity is IPviumIdentity {
62
53
  event OwnershipTransferStarted(address indexed from, address indexed to);
63
54
  event OwnershipTransferred(address indexed from, address indexed to);
64
55
 
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.
56
+ /// @notice Decoded public inputs returned by verifyAttestation after verification.
67
57
  struct Attestation {
68
58
  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).
59
+ /// EVM wallet public input. The circuit checks the selected wallet against the signed token;
60
+ /// zero represents an absent or non-EVM wallet.
72
61
  address wallet;
73
62
  /// When Privy issued the token (unix seconds). Freshness policy is the caller's.
74
63
  uint64 iat;
@@ -96,8 +85,9 @@ contract PviumIdentity is IPviumIdentity {
96
85
  error TimelockNotElapsed(uint64 eta);
97
86
  error UnknownKey(uint256 x, uint256 y);
98
87
 
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.
88
+ /// @param _owner Key-set administrator; address(0) disables owner-only calls.
89
+ /// @param _signerXs X coordinates of the initial accepted P-256 keys.
90
+ /// @param _signerYs Corresponding Y coordinates.
101
91
  constructor(
102
92
  IVerifier _verifier,
103
93
  uint16 _circuitVersion,
@@ -124,7 +114,7 @@ contract PviumIdentity is IPviumIdentity {
124
114
 
125
115
  // ---- signer key management ------------------------------------------------------------
126
116
 
127
- /// @notice Propose accepting a key, published at `url` under `kid`, after SIGNER_KEY_DELAY.
117
+ /// @notice Propose a key with caller-supplied `url` and `kid`, eligible after SIGNER_KEY_DELAY.
128
118
  /// Re-proposing a key restarts its delay.
129
119
  function proposeSignerKey(uint256 x, uint256 y, string calldata url, string calldata kid) external onlyOwner {
130
120
  if (!_isOnCurve(x, y)) revert InvalidPublicKey();
@@ -142,8 +132,7 @@ contract PviumIdentity is IPviumIdentity {
142
132
  emit SignerKeyProposalCancelled(x, y);
143
133
  }
144
134
 
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.
135
+ /// @notice Accept an uncancelled proposal once its delay has passed. Callable by any address.
147
136
  function activateSignerKey(uint256 x, uint256 y) external {
148
137
  bytes32 h = signerKeyHash(x, y);
149
138
  uint64 eta = signerKeyProposals[h].eta;
@@ -156,10 +145,8 @@ contract PviumIdentity is IPviumIdentity {
156
145
  emit SignerKeyAdded(x, y);
157
146
  }
158
147
 
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.
148
+ /// @notice Remove an accepted key and increment keySetRevision. The last key may be removed.
149
+ /// Attestations naming this key fail the signer check until it is added again.
163
150
  function removeSignerKey(uint256 x, uint256 y) external onlyOwner {
164
151
  bytes32 h = signerKeyHash(x, y);
165
152
  if (!_signerKeys[h]) revert UnknownKey(x, y);
@@ -210,8 +197,8 @@ contract PviumIdentity is IPviumIdentity {
210
197
 
211
198
  // ---- lower level ----------------------------------------------------------------------
212
199
 
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.
200
+ /// @notice Check the public-input count and accepted signer, require verifier.verify to return
201
+ /// true, and return the decoded attestation.
215
202
  function verifyAttestation(bytes calldata proof, bytes32[] calldata publicInputs)
216
203
  public
217
204
  view
@@ -239,7 +226,7 @@ contract PviumIdentity is IPviumIdentity {
239
226
  return a.iat;
240
227
  }
241
228
 
242
- /// @dev Cheap checks first (layout, signer), so bad requests fail before the 4M-gas verify.
229
+ /// @dev Check input count and signer membership before calling the proof verifier.
243
230
  function _decode(bytes32[] calldata publicInputs) internal view returns (Attestation memory a) {
244
231
  if (publicInputs.length != PUBLIC_INPUT_COUNT) revert WrongPublicInputCount(publicInputs.length);
245
232
  bytes32 x = _join(publicInputs[2], publicInputs[3]);
@@ -256,7 +243,7 @@ contract PviumIdentity is IPviumIdentity {
256
243
  return bytes32((uint256(hi) << 128) | uint256(lo));
257
244
  }
258
245
 
259
- /// @dev y^2 == x^3 - 3x + b (mod p) on NIST P-256. Guards against registering a typo.
246
+ /// @dev Check coordinate bounds and y^2 == x^3 - 3x + b (mod p) on NIST P-256.
260
247
  function _isOnCurve(uint256 x, uint256 y) internal pure returns (bool) {
261
248
  uint256 p = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF;
262
249
  uint256 b = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B;
@@ -4,12 +4,9 @@ pragma solidity ^0.8.27;
4
4
  import {IP2IDPolicy} from "./interfaces/IP2IDPolicy.sol";
5
5
 
6
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.
7
+ /// @notice Owner-managed verifier allowlist with a zero fee quote.
8
+ /// @dev Approval checks code presence, not verifier behavior. P2IDVault uses the allowlist for
9
+ /// funding and non-default-verifier claims; its current default verifier bypasses the claim check.
13
10
  contract PviumP2IDPolicy is IP2IDPolicy {
14
11
  address public owner;
15
12
  address public pendingOwner;
@@ -48,7 +45,7 @@ contract PviumP2IDPolicy is IP2IDPolicy {
48
45
  }
49
46
 
50
47
  /// @inheritdoc IP2IDPolicy
51
- /// @dev The launch policy charges no fee, so a vault never has fees for it to distribute.
48
+ /// @dev Distribution is unsupported, including for fees accrued under an earlier policy.
52
49
  function distributeFee(address, address, uint256) external payable {
53
50
  revert NoFees();
54
51
  }
@@ -0,0 +1,169 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.27;
3
+
4
+ import {IP2IDVaultProxy} from "./interfaces/IP2IDVaultProxy.sol";
5
+ import {IP2IdVaultFactory} from "./interfaces/IP2IdVaultFactory.sol";
6
+ import {IP2IDVault} from "./interfaces/IP2IDVault.sol";
7
+ import {IP2IDVerifier} from "./interfaces/IP2IDVerifier.sol";
8
+
9
+ /// @title PviumP2IDVaultProxy
10
+ /// @notice CREATE2 vault proxy initialized from its deploying factory's baseImplementation.
11
+ /// upgradeTo requires a registered target and an identity proof resolving to the caller.
12
+ /// @dev Stores the implementation in the ERC-1967 slot and upgrade history in UPGRADE_SLOT.
13
+ /// Delegated code can write both slots. Proxy-defined selectors take precedence over the
14
+ /// implementation, and empty-calldata native transfers are handled here without delegation.
15
+ contract PviumP2IDVaultProxy is IP2IDVaultProxy {
16
+ /// @dev bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)
17
+ bytes32 private constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
18
+ /// @dev bytes32(uint256(keccak256("pvium.vault.proxy.upgrading")) - 1): 1 while upgradeTo runs (a plain slot: the Paris target has no transient storage).
19
+ bytes32 private constant UPGRADING_SLOT = 0x1355c758876c0a085036d5960a223942d04ee15c92162da025899b2513e77bae;
20
+ /// @dev bytes32(uint256(keccak256("pvium.vault.proxy.lastUpgrade")) - 1): wallet (160 bits) | iat << 160.
21
+ bytes32 private constant UPGRADE_SLOT = 0x01d942a94cdff9c1162dd0a9d8cabadc4e9bf5ab8b614fc78cedc4d44e694edf;
22
+ /// @dev Maximum accepted issue time ahead of block.timestamp, matching P2IDVault.
23
+ uint64 private constant FUTURE_SLACK = 15 minutes;
24
+ /// @dev Gas for reading the vault's own freshness floors through the implementation.
25
+ uint256 private constant READ_GAS = 100_000;
26
+
27
+ /// @inheritdoc IP2IDVaultProxy
28
+ address public immutable factory;
29
+
30
+ error NotRegisteredImplementation(address implementation);
31
+ error NotVaultOf(bytes32 identityHash);
32
+ error NotOwner(address proven, address caller);
33
+ error ProofTooOld();
34
+ error ProofFromFuture();
35
+ error SameImplementation();
36
+ error NotAContract(address implementation);
37
+ error HookNotAcknowledged(address implementation);
38
+ error UpgradeReentered();
39
+ error ImplementationChangedDuringHook(address found);
40
+ error HookDidNotApply(address owner, uint64 latest);
41
+
42
+ constructor() {
43
+ factory = msg.sender;
44
+ _setImplementation(IP2IdVaultFactory(msg.sender).baseImplementation());
45
+ }
46
+
47
+ /// @inheritdoc IP2IDVaultProxy
48
+ function implementation() public view returns (address impl) {
49
+ assembly {
50
+ impl := sload(IMPLEMENTATION_SLOT)
51
+ }
52
+ }
53
+
54
+ /// @inheritdoc IP2IDVaultProxy
55
+ function lastUpgrade() public view returns (address wallet, uint64 iat) {
56
+ uint256 packed;
57
+ assembly {
58
+ packed := sload(UPGRADE_SLOT)
59
+ }
60
+ wallet = address(uint160(packed));
61
+ iat = uint64(packed >> 160);
62
+ }
63
+
64
+ /// @inheritdoc IP2IDVaultProxy
65
+ function upgradeTo(bytes32 identityHash, address newImplementation, bytes calldata proof) external {
66
+ _enterUpgrade();
67
+ IP2IdVaultFactory f = IP2IdVaultFactory(factory);
68
+ if (f.vaultFor(identityHash) != address(this)) revert NotVaultOf(identityHash);
69
+ if (!f.isRegisteredImplementation(newImplementation)) revert NotRegisteredImplementation(newImplementation);
70
+ if (newImplementation == implementation()) revert SameImplementation();
71
+ address verifier = f.defaultVerifier();
72
+ (address wallet, uint64 iat) =
73
+ IP2IDVerifier(verifier).getIdentityWallet(identityHash, proof, IP2IDVerifier.Constraint(bytes32(0), ""));
74
+ if (wallet != msg.sender) revert NotOwner(wallet, msg.sender);
75
+ if (iat > block.timestamp + FUTURE_SLACK) revert ProofFromFuture();
76
+ // Combine stored upgrade history with the readable issue-time floors of the current implementation.
77
+ (address lastWallet, uint64 lastIat) = lastUpgrade();
78
+ // Ignore stored history beyond this entry point's accepted future-time bound.
79
+ if (lastIat > block.timestamp + FUTURE_SLACK) (lastWallet, lastIat) = (address(0), 0);
80
+ if (iat < lastIat || (iat == lastIat && wallet != lastWallet)) revert ProofTooOld();
81
+ if (iat < _vaultFloor(verifier)) revert ProofTooOld();
82
+ uint256 packed = uint256(uint160(wallet)) | (uint256(iat) << 160);
83
+ assembly {
84
+ sstore(UPGRADE_SLOT, packed)
85
+ }
86
+ _setImplementation(newImplementation);
87
+ // Apply the verified result through the new implementation. Revert the upgrade on hook
88
+ // failure, invalid acknowledgement or failed post-condition checks below.
89
+ (bool ok, bytes memory ret) = address(this).call(abi.encodeCall(IP2IDVault.acceptOwnerProof, (verifier, wallet, iat)));
90
+ if (!ok) {
91
+ assembly {
92
+ revert(add(ret, 32), mload(ret)) // the vault's own reason (ProofTooOld, NotOwnerProof, …)
93
+ }
94
+ }
95
+ // Require a canonical ABI-encoded selector. A fallback returning the same data can satisfy this.
96
+ if (ret.length != 32 || abi.decode(ret, (bytes4)) != IP2IDVault.acceptOwnerProof.selector) {
97
+ revert HookNotAcknowledged(newImplementation);
98
+ }
99
+ // Post-conditions, read through the new implementation: it is still what was installed,
100
+ // and it now records this wallet as the payout owner with a floor at least this fresh.
101
+ // These detect some incompatible hooks. Registered code can fake getters or alter other storage;
102
+ // the registry does not audit implementation behavior.
103
+ if (implementation() != newImplementation) revert ImplementationChangedDuringHook(implementation());
104
+ address ownerNow = address(uint160(_read(abi.encodeCall(IP2IDVault.owner, (verifier)))));
105
+ uint256 latestNow = _read(abi.encodeCall(IP2IDVault.latestProofIat, (verifier)));
106
+ if (ownerNow != wallet || latestNow < iat) revert HookDidNotApply(ownerNow, uint64(latestNow));
107
+ _exitUpgrade();
108
+ }
109
+
110
+ /// @dev upgradeTo is not reentrant: the hook runs with the new implementation installed, and
111
+ /// must not be able to start another upgrade from inside this one.
112
+ function _enterUpgrade() private {
113
+ uint256 flag;
114
+ assembly {
115
+ flag := sload(UPGRADING_SLOT)
116
+ }
117
+ if (flag != 0) revert UpgradeReentered();
118
+ assembly {
119
+ sstore(UPGRADING_SLOT, 1)
120
+ }
121
+ }
122
+
123
+ function _exitUpgrade() private {
124
+ assembly {
125
+ sstore(UPGRADING_SLOT, 0)
126
+ }
127
+ }
128
+
129
+ /// @dev Maximum of the current implementation's per-verifier and cross-default floors.
130
+ /// Failed, malformed and excessively future-dated reads contribute zero.
131
+ function _vaultFloor(address verifier) private view returns (uint64 floor) {
132
+ uint256 a = _read(abi.encodeCall(IP2IDVault.latestProofIat, (verifier)));
133
+ uint256 b = _read(abi.encodeCall(IP2IDVault.untrackedProofIat, ()));
134
+ // Discard values beyond the same future-time bound used for proofs.
135
+ uint256 limit = block.timestamp + FUTURE_SLACK;
136
+ if (a > limit) a = 0;
137
+ if (b > limit) b = 0;
138
+ floor = uint64(a > b ? a : b);
139
+ }
140
+
141
+ function _read(bytes memory data) private view returns (uint256 value) {
142
+ (bool ok, bytes memory ret) = address(this).staticcall{gas: READ_GAS}(data);
143
+ if (!ok || ret.length != 32) return 0;
144
+ value = abi.decode(ret, (uint256));
145
+ }
146
+
147
+ /// @dev Accepted here, not delegated: keeps plain sends within the 2300-gas stipend.
148
+ receive() external payable {}
149
+
150
+ fallback() external payable {
151
+ address impl = implementation();
152
+ assembly {
153
+ calldatacopy(0, 0, calldatasize())
154
+ let ok := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
155
+ returndatacopy(0, 0, returndatasize())
156
+ switch ok
157
+ case 0 { revert(0, returndatasize()) }
158
+ default { return(0, returndatasize()) }
159
+ }
160
+ }
161
+
162
+ function _setImplementation(address impl) private {
163
+ if (impl.code.length == 0) revert NotAContract(impl);
164
+ assembly {
165
+ sstore(IMPLEMENTATION_SLOT, impl)
166
+ }
167
+ emit Upgraded(impl);
168
+ }
169
+ }
@@ -4,33 +4,40 @@ pragma solidity ^0.8.27;
4
4
  import {IP2IdVaultFactory} from "./interfaces/IP2IdVaultFactory.sol";
5
5
  import {IP2IDPolicy} from "./interfaces/IP2IDPolicy.sol";
6
6
  import {P2IDVault} from "./P2IDVault.sol";
7
+ import {PviumP2IDVaultProxy} from "./PviumP2IDVaultProxy.sol";
8
+ import {IP2IDVault} from "./interfaces/IP2IDVault.sol";
7
9
 
8
10
  /// @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).
11
+ /// @notice Deploys one PviumP2IDVaultProxy per identity with CREATE2 (salt = identityHash) and
12
+ /// supplies policy and default-verifier settings. The proxy takes no constructor arguments;
13
+ /// its address is the low 160 bits of keccak256(0xff || factory || identityHash || initCodeHash()).
14
+ /// @dev The policy and the default verifier change only through a timelock (propose, wait,
15
+ /// activate): the policy after `policyChangeDelay`, the default verifier after the fixed
16
+ /// DEFAULT_VERIFIER_DELAY. Implementation registration also has a timelock; revocation is
17
+ /// immediate and does not change existing proxies. Vault upgrades preserve their addresses.
18
+ /// Manual registration checks code presence; deterministic deployment also checks factory binding.
19
+ /// Neither path verifies storage compatibility or implementation behavior.
24
20
  contract PviumP2IdVaultFactory is IP2IdVaultFactory {
25
21
  bytes32 public immutable nsHash;
22
+ /// @notice Starting implementation for new proxies, changed only by a delayed default proposal.
23
+ address public baseImplementation;
24
+ /// @notice Original implementation, kept registered as a fallback.
25
+ address public immutable initialImplementation;
26
+ bytes32 public constant IMPLEMENTATION_SALT = keccak256("pvium.vault.implementation.v1");
27
+ bool public proposedImplementationMakeDefault;
28
+ /// @notice Target allowlist checked by PviumP2IDVaultProxy.upgradeTo.
29
+ mapping(address implementation => bool) public isRegisteredImplementation;
30
+ address public proposedImplementation;
31
+ /// @notice Earliest time the proposed implementation can be registered; 0 when nothing is proposed.
32
+ uint64 public proposedImplementationEta;
26
33
  uint64 public immutable minRefundWindow;
27
34
  uint64 public immutable maxRefundWindow;
28
35
 
29
- /// @notice Proposes and activates policy / default-verifier changes (a Pvium multisig). Two-step transfer.
36
+ /// @notice Administrative owner for settings and implementation registration; transferred in two steps.
30
37
  address public owner;
31
38
  address public pendingOwner;
32
39
 
33
- /// @notice The policy every vault consults. Changes only via the timelock below.
40
+ /// @notice Policy address returned to vaults; changed through proposePolicy/activatePolicy.
34
41
  address public policy;
35
42
  /// @notice Verifier used when a payer does not choose one. Changes only via the timelock below.
36
43
  address public defaultVerifier;
@@ -67,6 +74,8 @@ contract PviumP2IdVaultFactory is IP2IdVaultFactory {
67
74
  error TokenBalanceQueryFailed();
68
75
  error NothingReceived();
69
76
  error UnexpectedValue();
77
+ error InvalidImplementation();
78
+ error ImplementationProposalPending();
70
79
 
71
80
  constructor(
72
81
  address _owner,
@@ -91,6 +100,113 @@ contract PviumP2IdVaultFactory is IP2IdVaultFactory {
91
100
  emit PolicyActivated(_policy);
92
101
  defaultVerifier = _defaultVerifier;
93
102
  emit DefaultVerifierActivated(_defaultVerifier);
103
+ address base = address(new P2IDVault(address(this)));
104
+ baseImplementation = base;
105
+ initialImplementation = base;
106
+ isRegisteredImplementation[base] = true;
107
+ emit ImplementationRegistered(base);
108
+ }
109
+
110
+ // ------------------------------------------------------------------ vault implementations (timelocked)
111
+
112
+ /// @notice Predict the CREATE2 address from full creation code, including constructor arguments.
113
+ function implementationFor(bytes32 creationCodeHash) public view returns (address) {
114
+ return address(uint160(uint256(keccak256(abi.encodePacked(
115
+ bytes1(0xff), address(this), IMPLEMENTATION_SALT, creationCodeHash
116
+ )))));
117
+ }
118
+
119
+ /// @notice Deploy full creation code with CREATE2 and propose registration in the same transaction.
120
+ /// @dev Reuses existing code. An identical pending proposal keeps its ETA; a different proposal
121
+ /// must first be cancelled. makeDefault affects new proxies only, after registration.
122
+ function deployVaultImplementation(bytes memory creationCode, bool makeDefault)
123
+ external onlyOwner returns (address implementation)
124
+ {
125
+ if (creationCode.length == 0) revert InvalidImplementation();
126
+ implementation = implementationFor(keccak256(creationCode));
127
+ if (implementation.code.length == 0) {
128
+ bytes32 salt = IMPLEMENTATION_SALT;
129
+ address deployed;
130
+ assembly {
131
+ deployed := create2(0, add(creationCode, 32), mload(creationCode), salt)
132
+ }
133
+ if (deployed != implementation || deployed.code.length == 0) revert InvalidImplementation();
134
+ emit ImplementationDeployed(implementation, keccak256(creationCode));
135
+ }
136
+ _checkFactoryBinding(implementation);
137
+ if (proposedImplementationEta != 0) {
138
+ if (proposedImplementation != implementation || proposedImplementationMakeDefault != makeDefault) {
139
+ revert ImplementationProposalPending();
140
+ }
141
+ return implementation;
142
+ }
143
+ if (isRegisteredImplementation[implementation] && (!makeDefault || baseImplementation == implementation)) {
144
+ return implementation;
145
+ }
146
+ _proposeImplementation(implementation, makeDefault);
147
+ }
148
+
149
+ /// @notice Propose an opt-in upgrade target. Replaces a pending proposal and resets its delay.
150
+ function proposeImplementation(address implementation) external onlyOwner {
151
+ if (implementation.code.length == 0 || isRegisteredImplementation[implementation]) revert InvalidImplementation();
152
+ _proposeImplementation(implementation, false);
153
+ }
154
+
155
+ /// @notice Propose a registered target as the starting implementation for future proxies.
156
+ function proposeDefaultImplementation(address implementation) external onlyOwner {
157
+ if (!isRegisteredImplementation[implementation] || implementation == baseImplementation) revert InvalidImplementation();
158
+ _checkFactoryBinding(implementation);
159
+ _proposeImplementation(implementation, true);
160
+ }
161
+
162
+ function _proposeImplementation(address implementation, bool makeDefault) private {
163
+ proposedImplementation = implementation;
164
+ proposedImplementationMakeDefault = makeDefault;
165
+ proposedImplementationEta = uint64(block.timestamp) + DEFAULT_VERIFIER_DELAY;
166
+ emit ImplementationProposed(implementation, proposedImplementationEta);
167
+ emit ImplementationDefaultProposed(implementation, makeDefault, proposedImplementationEta);
168
+ }
169
+
170
+ function _checkFactoryBinding(address implementation) private view {
171
+ (bool ok, bytes memory result) = implementation.staticcall(abi.encodeWithSignature("factory()"));
172
+ if (!ok || result.length != 32 || abi.decode(result, (bytes32)) != bytes32(uint256(uint160(address(this))))) {
173
+ revert InvalidImplementation();
174
+ }
175
+ }
176
+
177
+ function cancelImplementationProposal() external onlyOwner {
178
+ if (proposedImplementationEta == 0) revert NothingProposed();
179
+ emit ImplementationProposalCancelled(proposedImplementation);
180
+ delete proposedImplementation;
181
+ delete proposedImplementationEta;
182
+ delete proposedImplementationMakeDefault;
183
+ }
184
+
185
+ function registerImplementation() external onlyOwner {
186
+ uint64 eta = proposedImplementationEta;
187
+ if (eta == 0) revert NothingProposed();
188
+ if (block.timestamp < eta) revert TimelockNotElapsed(eta);
189
+ address implementation = proposedImplementation;
190
+ bool makeDefault = proposedImplementationMakeDefault;
191
+ if (implementation.code.length == 0) revert InvalidImplementation();
192
+ if (makeDefault) _checkFactoryBinding(implementation);
193
+ delete proposedImplementation;
194
+ delete proposedImplementationEta;
195
+ delete proposedImplementationMakeDefault;
196
+ isRegisteredImplementation[implementation] = true;
197
+ emit ImplementationRegistered(implementation);
198
+ if (makeDefault) {
199
+ baseImplementation = implementation;
200
+ emit DefaultImplementationActivated(implementation);
201
+ }
202
+ }
203
+
204
+ /// @notice Remove a registered target immediately, except the current default and original implementation.
205
+ /// This changes the registry, not the implementation stored in existing proxies.
206
+ function revokeImplementation(address implementation) external onlyOwner {
207
+ if (implementation == baseImplementation || implementation == initialImplementation || !isRegisteredImplementation[implementation]) revert InvalidImplementation();
208
+ isRegisteredImplementation[implementation] = false;
209
+ emit ImplementationRevoked(implementation);
94
210
  }
95
211
 
96
212
  // ------------------------------------------------------------------ policy (timelocked)
@@ -111,8 +227,7 @@ contract PviumP2IdVaultFactory is IP2IdVaultFactory {
111
227
  delete proposedPolicyEta;
112
228
  }
113
229
 
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.
230
+ /// @notice Set policy after the proposal delay, provided it approves the current default verifier.
116
231
  function activatePolicy() external onlyOwner {
117
232
  uint64 eta = proposedPolicyEta;
118
233
  if (eta == 0) revert NothingProposed();
@@ -144,8 +259,8 @@ contract PviumP2IdVaultFactory is IP2IdVaultFactory {
144
259
  delete proposedDefaultEta;
145
260
  }
146
261
 
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).
262
+ /// @notice Set the proposed default after its delay, provided the current policy approves it.
263
+ /// Failed approval leaves the proposal pending.
149
264
  function activateDefaultVerifier() external onlyOwner {
150
265
  uint64 eta = proposedDefaultEta;
151
266
  if (eta == 0) revert NothingProposed();
@@ -201,18 +316,17 @@ contract PviumP2IdVaultFactory is IP2IdVaultFactory {
201
316
  return vaultFor(identityHash).code.length != 0;
202
317
  }
203
318
 
204
- /// @notice keccak256 of the P2IDVault creation code (no constructor arguments), for offline derivation.
319
+ /// @notice keccak256 of the vault proxy's creation code (no constructor arguments), for offline derivation.
205
320
  function initCodeHash() public pure returns (bytes32) {
206
- return keccak256(type(P2IDVault).creationCode);
321
+ return keccak256(type(PviumP2IDVaultProxy).creationCode);
207
322
  }
208
323
 
209
324
  /// @inheritdoc IP2IdVaultFactory
210
325
  function deploy(bytes32 identityHash) public returns (address vault) {
211
326
  vault = vaultFor(identityHash);
212
327
  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);
328
+ vault = address(new PviumP2IDVaultProxy{salt: identityHash}());
329
+ IP2IDVault(vault).initialize(nsHash, identityHash, minRefundWindow, maxRefundWindow);
216
330
  emit VaultDeployed(identityHash, vault);
217
331
  }
218
332
 
@@ -274,8 +388,8 @@ contract PviumP2IdVaultFactory is IP2IdVaultFactory {
274
388
  return (vault, P2IDVault(payable(vault)).fundFor{value: msg.value}(msg.sender, verifier, token, amount, constraint, refundWindow, ref));
275
389
  }
276
390
  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.
391
+ // Measure the factory's balance increase, approve that amount to the vault, and record
392
+ // msg.sender as funder. The vault measures its own balance increase during the second transfer.
279
393
  uint256 before = _balanceOf(token);
280
394
  _callToken(
281
395
  token,