@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 +2 -0
- package/contracts/P2IDVault.sol +119 -140
- package/contracts/PviumIdentity.sol +23 -36
- package/contracts/PviumP2IDPolicy.sol +4 -7
- package/contracts/PviumP2IDVaultProxy.sol +169 -0
- package/contracts/PviumP2IdVaultFactory.sol +142 -28
- package/contracts/PviumVerifier.sol +13 -21
- package/contracts/interfaces/IP2IDPolicy.sol +5 -21
- package/contracts/interfaces/IP2IDVault.sol +24 -17
- package/contracts/interfaces/IP2IDVaultProxy.sol +38 -0
- package/contracts/interfaces/IP2IDVerifier.sol +9 -13
- package/contracts/interfaces/IP2IdVaultFactory.sol +34 -7
- package/contracts/interfaces/IPviumIdentity.sol +5 -7
- package/contracts/lib/P2IDHash.sol +5 -5
- package/dist/cjs/p2idConstants.js +2 -2
- package/dist/esm/p2idConstants.d.ts +2 -2
- package/dist/esm/p2idConstants.js +2 -2
- package/package.json +2 -2
|
@@ -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
|
|
9
|
-
///
|
|
10
|
-
///
|
|
11
|
-
///
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
///
|
|
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
|
|
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
|
|
70
|
-
///
|
|
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
|
|
100
|
-
/// @param _signerXs
|
|
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
|
|
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
|
|
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
|
|
160
|
-
///
|
|
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
|
|
214
|
-
/// and
|
|
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
|
|
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.
|
|
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
|
|
8
|
-
///
|
|
9
|
-
///
|
|
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
|
|
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
|
|
10
|
-
///
|
|
11
|
-
///
|
|
12
|
-
///
|
|
13
|
-
///
|
|
14
|
-
///
|
|
15
|
-
///
|
|
16
|
-
///
|
|
17
|
-
///
|
|
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
|
|
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
|
|
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
|
|
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
|
|
148
|
-
///
|
|
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
|
|
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(
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
//
|
|
278
|
-
//
|
|
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,
|