-
Notifications
You must be signed in to change notification settings - Fork 535
/
HydraS1AccountboundAttester.sol
348 lines (298 loc) · 14.3 KB
/
HydraS1AccountboundAttester.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.14;
pragma experimental ABIEncoderV2;
import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol';
import {IHydraS1AccountboundAttester} from './interfaces/IHydraS1AccountboundAttester.sol';
// Core protocol Protocol imports
import {Request, Attestation, Claim} from '../../core/libs/Structs.sol';
// Imports related to Hydra-S1
import {HydraS1SimpleAttester, IAttester, HydraS1Lib, HydraS1ProofData, HydraS1Claim} from './HydraS1SimpleAttester.sol';
/**
* @title Hydra-S1 Accountbound Attester
* @author Sismo
* @notice This attester is part of the family of the Hydra-S1 Attesters.
* Hydra-S1 attesters enable users to prove they have an account in a group in a privacy preserving way.
* The Hydra-S1 Simple Attester contract is inherited and holds the complex Hydra S1 verification logic.
* Request verification alongside proof verification is already implemented in the inherited HydraS1SimpleAttester, along with the buildAttestations logic.
* However, we override the buildAttestations function to encode the nullifier and its burn count in the user attestation.
* The _beforeRecordAttestations is also overriden to fit the Accountbound logic.
* We invite readers to refer to:
* - https://hydra-s1.docs.sismo.io for a full guide through the Hydra-S1 ZK Attestations
* - https://hydra-s1-circuits.docs.sismo.io for circuits, prover and verifiers of Hydra-S1
* This specific attester has the following characteristics:
* - Zero Knowledge
* One cannot deduct from an attestation what source account was used to generate the underlying proof
* - Non Strict (scores)
* If a user can generate an attestation of max value 100, they can also generate any attestation with value < 100.
* This attester generate attestations of scores
* - Nullified
* Each source account gets one nullifier per claim (i.e only one attestation per source account per claim)
* While semaphore/ tornado cash are using the following notations: nullifierHash = hash(IdNullifier, externalNullifier)
* We prefered to use the naming 'nullifier' instead of 'nullifierHash' in our contracts and documentation.
* We also renamed 'IdNullifier' in 'sourceSecret' (the secret tied to a source account) and we kept the 'externalNullifier' notation.
* Finally, here is our notations at Sismo: nullifier = hash(sourceSecret, externalNullifier)
* - Accountbound (opt-in, with cooldown period)
* The owner of this attester can set a cooldown duration for a specific group, activating the accountbound feature for this group.
* Users can update their attestation's destination by providing a new Hydra-S1 ZK proof
* It means the attestation is bound to the source account, stored on an updatable destination account.
* When deleting/ sending to a new destination, the nullifier will enter a cooldown period, so it remains occasional.
* A group that has its cooldown duration set to 0 means it has been configured to not feature accountbound attestations, attestations can not be transferred
* One can however know that the former and the new destinations were created using the same nullifier, thus creating a link between those two destinations.
* - Renewable
* A nullifier can actually be reused as long as the destination of the attestation remains the same
* It enables users to renew or update their attestations
**/
contract HydraS1AccountboundAttester is
IHydraS1AccountboundAttester,
HydraS1SimpleAttester,
Ownable
{
using HydraS1Lib for HydraS1ProofData;
using HydraS1Lib for bytes;
using HydraS1Lib for Request;
uint8 public constant IMPLEMENTATION_VERSION = 5;
/*******************************************************
Storage layout:
20 slots between HydraS1SimpleAttester and HydraS1AccountboundAttester
1 currently used by Ownable
19 place holders
2O for config
1 currently used
19 place holders
20 for logic
2 currently used
18 place holders
*******************************************************/
// keeping some space for future config logics
uint256[19] private _placeHolderBeforeHydraS1Accountbound;
// cooldown durations for each groupIndex
mapping(uint256 => uint32) internal _cooldownDurations;
// keeping some space for future config logics
uint256[19] private _placeHoldersHydraS1AccountboundConfig;
mapping(uint256 => uint32) internal _nullifiersCooldownStart;
mapping(uint256 => uint16) internal _nullifiersBurnCount;
// keeping some space for future config logics
uint256[18] private _placeHoldersHydraS1AccountboundLogic;
/*******************************************************
INITIALIZATION FUNCTIONS
*******************************************************/
/**
* @dev Constructor. Initializes the contract
* @param attestationsRegistryAddress Attestations Registry contract on which the attester will write attestations
* @param hydraS1VerifierAddress ZK Snark Hydra-S1 Verifier contract
* @param availableRootsRegistryAddress Registry storing the available groups for this attester (e.g roots of registry merkle trees)
* @param commitmentMapperAddress commitment mapper's public key registry
* @param collectionIdFirst Id of the first attestation collection in which the attester is supposed to record
* @param collectionIdLast Id of the last attestation collection in which the attester is supposed to record
* @param owner Address of attester's owner
*/
constructor(
address attestationsRegistryAddress,
address hydraS1VerifierAddress,
address availableRootsRegistryAddress,
address commitmentMapperAddress,
uint256 collectionIdFirst,
uint256 collectionIdLast,
address owner
)
HydraS1SimpleAttester(
attestationsRegistryAddress,
hydraS1VerifierAddress,
availableRootsRegistryAddress,
commitmentMapperAddress,
collectionIdFirst,
collectionIdLast
)
{
initialize(owner);
}
/**
* @dev Initialize function, to be called by the proxy delegating calls to this implementation
* @param ownerAddress Owner of the contract, has the right to authorize/unauthorize attestations issuers
* @notice The reinitializer modifier is needed to configure modules that are added through upgrades and that require initialization.
*/
function initialize(address ownerAddress) public reinitializer(IMPLEMENTATION_VERSION) {
// if proxy did not setup owner yet or if called by constructor (for implem setup)
if (owner() == address(0) || address(this).code.length == 0) {
_transferOwnership(ownerAddress);
}
}
/*******************************************************
MANDATORY FUNCTIONS TO OVERRIDE FROM ATTESTER.SOL
*******************************************************/
/**
* @dev Returns the actual attestations constructed from the user request
* @param request users request. Claim of having an account part of a group of accounts
* @param proofData snark public input as well as snark proof
*/
function buildAttestations(
Request calldata request,
bytes calldata proofData
) public view virtual override(IAttester, HydraS1SimpleAttester) returns (Attestation[] memory) {
Attestation[] memory attestations = super.buildAttestations(request, proofData);
uint256 nullifier = proofData._getNullifier();
attestations[0].extraData = abi.encode(
attestations[0].extraData, // nullifier, from HydraS1 Simple
_getNextBurnCount(nullifier, attestations[0].owner) // BurnCount
);
return (attestations);
}
/*******************************************************
OPTIONAL HOOK VIRTUAL FUNCTIONS FROM ATTESTER.SOL
*******************************************************/
/**
* @dev Hook run before recording the attestation.
* Throws if nullifier already used, not a renewal, and nullifier on cooldown.
* @param request users request. Claim of having an account part of a group of accounts
* @param proofData provided to back the request. snark input and snark proof
*/
function _beforeRecordAttestations(
Request calldata request,
bytes calldata proofData
) internal virtual override {
uint256 nullifier = proofData._getNullifier();
address previousNullifierDestination = _getDestinationOfNullifier(nullifier);
HydraS1Claim memory claim = request._claim();
// check if the nullifier has already been used previously, if so it may be on cooldown
if (
previousNullifierDestination != address(0) &&
previousNullifierDestination != claim.destination
) {
uint32 cooldownDuration = _getCooldownDurationForGroupIndex(claim.groupProperties.groupIndex);
if (cooldownDuration == 0) {
revert CooldownDurationNotSetForGroupIndex(claim.groupProperties.groupIndex);
}
if (_isOnCooldown(nullifier, cooldownDuration)) {
uint16 burnCount = _getNullifierBurnCount(nullifier);
revert NullifierOnCooldown(
nullifier,
previousNullifierDestination,
burnCount,
cooldownDuration
);
}
// Delete the old Attestation linked to the nullifier before recording the new one (accountbound feature)
_deletePreviousAttestation(claim, previousNullifierDestination);
_setNullifierOnCooldownAndIncrementBurnCount(nullifier);
}
_setDestinationForNullifier(nullifier, request.destination);
}
/*******************************************************
LOGIC FUNCTIONS RELATED TO ACCOUNTBOUND FEATURE
*******************************************************/
/**
* @dev Getter, returns the burnCount of a nullifier
* @param nullifier nullifier used
**/
function getNullifierBurnCount(uint256 nullifier) external view returns (uint16) {
return _getNullifierBurnCount(nullifier);
}
/**
* @dev Getter, returns the cooldown start of a nullifier
* @param nullifier nullifier used
**/
function getNullifierCooldownStart(uint256 nullifier) external view returns (uint32) {
return _getNullifierCooldownStart(nullifier);
}
/**
* @dev returns the nullifier for a given extraData
* @param extraData bytes where the nullifier is encoded
*/
function getNullifierFromExtraData(
bytes memory extraData
) external pure override(HydraS1SimpleAttester, IHydraS1AccountboundAttester) returns (uint256) {
(bytes memory nullifierBytes, ) = abi.decode(extraData, (bytes, uint16));
uint256 nullifier = abi.decode(nullifierBytes, (uint256));
return nullifier;
}
/**
* @dev Returns the burn count for a given extraData
* @param extraData bytes where the burnCount is encoded
*/
function getBurnCountFromExtraData(bytes memory extraData) external pure returns (uint16) {
(, uint16 burnCount) = abi.decode(extraData, (uint256, uint16));
return burnCount;
}
/**
* @dev Checks if a nullifier is on cooldown
* @param nullifier user nullifier
* @param cooldownDuration waiting time before the user can change its badge destination
*/
function _isOnCooldown(uint256 nullifier, uint32 cooldownDuration) internal view returns (bool) {
return _getNullifierCooldownStart(nullifier) + cooldownDuration > block.timestamp;
}
/**
* @dev Delete the previous attestation created with this nullifier
* @param claim user claim
* @param previousNullifierDestination previous destination chosen for this user nullifier
*/
function _deletePreviousAttestation(
HydraS1Claim memory claim,
address previousNullifierDestination
) internal {
address[] memory attestationOwners = new address[](1);
uint256[] memory attestationCollectionIds = new uint256[](1);
attestationOwners[0] = previousNullifierDestination;
attestationCollectionIds[0] = AUTHORIZED_COLLECTION_ID_FIRST + claim.groupProperties.groupIndex;
ATTESTATIONS_REGISTRY.deleteAttestations(attestationOwners, attestationCollectionIds);
}
function _setNullifierOnCooldownAndIncrementBurnCount(uint256 nullifier) internal {
_nullifiersCooldownStart[nullifier] = uint32(block.timestamp);
_nullifiersBurnCount[nullifier] += 1;
emit NullifierSetOnCooldown(nullifier, _nullifiersBurnCount[nullifier]);
}
function _getNullifierCooldownStart(uint256 nullifier) internal view returns (uint32) {
return _nullifiersCooldownStart[nullifier];
}
function _getNullifierBurnCount(uint256 nullifier) internal view returns (uint16) {
return _nullifiersBurnCount[nullifier];
}
/**
* @dev returns burn count or burn count + 1 if new burn will happen
* @param nullifier user nullifier
* @param claimDestination destination referenced in the user claim
*/
function _getNextBurnCount(
uint256 nullifier,
address claimDestination
) public view virtual returns (uint16) {
address previousNullifierDestination = _getDestinationOfNullifier(nullifier);
uint16 burnCount = _getNullifierBurnCount(nullifier);
// If the attestation is minted on a new destination address
// the burnCount that will be encoded in the extraData of the Attestation should be incremented
if (
previousNullifierDestination != address(0) && previousNullifierDestination != claimDestination
) {
burnCount += 1;
}
return burnCount;
}
/*******************************************************
GROUP CONFIGURATION LOGIC
*******************************************************/
/**
* @dev Setter, sets the cooldown duration of a groupIndex
* @notice set to 0 to deactivate the accountbound feature for this group
* @param groupIndex internal collection id
* @param cooldownDuration cooldown duration we want to set for the groupIndex
**/
function setCooldownDurationForGroupIndex(
uint256 groupIndex,
uint32 cooldownDuration
) external onlyOwner {
_cooldownDurations[groupIndex] = cooldownDuration;
emit CooldownDurationSetForGroupIndex(groupIndex, cooldownDuration);
}
/**
* @dev Getter, get the cooldown duration of a groupIndex
* @notice returns 0 when the accountbound feature is deactivated for this group
* @param groupIndex internal collection id
**/
function getCooldownDurationForGroupIndex(uint256 groupIndex) external view returns (uint32) {
return _getCooldownDurationForGroupIndex(groupIndex);
}
// = 0 means that the accountbound feature is deactivated for this group
function _getCooldownDurationForGroupIndex(uint256 groupIndex) internal view returns (uint32) {
return _cooldownDurations[groupIndex];
}
}