forked from poa-box/POP
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathEligibilityModule.sol
More file actions
1142 lines (961 loc) · 50.2 KB
/
EligibilityModule.sol
File metadata and controls
1142 lines (961 loc) · 50.2 KB
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
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.19;
import "../lib/hats-protocol/src/Interfaces/IHats.sol";
import "../lib/hats-protocol/src/Interfaces/IHatsEligibility.sol";
import "@openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol";
/**
* @notice Minimal interface for ToggleModule - only includes functions we actually use
*/
interface IToggleModule {
function setHatStatus(uint256 hatId, bool _active) external;
}
/**
* @title EligibilityModule
* @notice A hat-based module for configuring eligibility and standing
* on a per-hat basis in the Hats Protocol, controlled by admin hats.
* Now supports optional N-Vouch eligibility system.
*/
contract EligibilityModule is Initializable, IHatsEligibility {
/*═══════════════════════════════════════════ ERRORS ═══════════════════════════════════════════*/
error NotSuperAdmin();
error NotAuthorizedAdmin();
error ZeroAddress();
error InvalidQuorum();
error InvalidMembershipHat();
error CannotVouchForSelf();
error InvalidHatId();
error InvalidUser();
error InvalidJoinTime();
error ArrayLengthMismatch();
error VouchingNotEnabled();
error NotAuthorizedToVouch();
error AlreadyVouched();
error HasNotVouched();
error VouchingRateLimitExceeded();
error InvalidMaxDailyVouches();
error NewUserVouchingRestricted();
error ApplicationAlreadyExists();
error NoActiveApplication();
error InvalidApplicationHash();
/*═════════════════════════════════════════ STRUCTS ═════════════════════════════════════════*/
/// @notice Per-wearer per-hat configuration for eligibility and standing (packed)
struct WearerRules {
uint8 flags; // Packed flags: bit 0 = eligible, bit 1 = standing
}
/// @notice Configuration for vouching system per hat (optimized packing)
struct VouchConfig {
uint32 quorum; // Number of vouches required
uint256 membershipHatId; // Hat ID whose wearers can vouch
uint8 flags; // Packed flags: bit 0 = enabled, bit 1 = combineWithHierarchy
}
/// @notice Parameters for creating a hat with eligibility configuration
struct CreateHatParams {
uint256 parentHatId;
string details;
uint32 maxSupply;
bool _mutable;
string imageURI;
bool defaultEligible;
bool defaultStanding;
address[] mintToAddresses;
bool[] wearerEligibleFlags;
bool[] wearerStandingFlags;
}
/*═════════════════════════════════════ ERC-7201 STORAGE ═════════════════════════════════════*/
/// @custom:storage-location erc7201:poa.eligibilitymodule.storage
struct Layout {
// Slot 1: Core addresses (40 bytes + 24 bytes padding)
IHats hats; // 20 bytes
address superAdmin; // 20 bytes
// Slot 2: Module addresses + hat ID (20 + 20 + 32 = 72 bytes across 3 slots)
address toggleModule; // 20 bytes
uint256 eligibilityModuleAdminHat; // 32 bytes (separate slot)
// Emergency pause state
bool _paused;
// Mappings (separate slots each)
mapping(address => mapping(uint256 => WearerRules)) wearerRules;
mapping(address => mapping(uint256 => bool)) hasSpecificWearerRules;
mapping(uint256 => WearerRules) defaultRules;
mapping(uint256 => VouchConfig) vouchConfigs;
mapping(uint256 => mapping(address => mapping(address => bool))) vouchers;
mapping(uint256 => mapping(address => uint32)) currentVouchCount;
// Rate limiting for vouching
mapping(address => uint256) userJoinTime;
mapping(address => mapping(uint256 => uint32)) dailyVouchCount; // user => day => count
// Role application system
mapping(uint256 => mapping(address => bytes32)) roleApplications; // hatId => applicant => applicationHash
mapping(uint256 => address[]) roleApplicants; // hatId => array of applicant addresses
uint256 _notEntered; // reentrancy guard (moved from slot 0 to ERC-7201 namespace)
// Vouch epoch tracking: invalidates stale vouch data after resetVouches/reconfigureVouching
mapping(uint256 => uint256) vouchConfigEpoch; // hatId => epoch counter
mapping(uint256 => mapping(address => uint256)) wearerVouchEpoch; // hatId => wearer => epoch of their count
mapping(uint256 => mapping(address => mapping(address => uint256))) voucherRecordEpoch; // hatId => wearer => voucher => epoch
// Configurable daily vouch limit (0 = use DEFAULT_MAX_DAILY_VOUCHES)
uint32 maxDailyVouches;
}
bytes32 private constant _STORAGE_SLOT = keccak256("poa.eligibilitymodule.storage");
/// @dev Use assembly for gas-optimized storage access
function _layout() private pure returns (Layout storage s) {
bytes32 slot = _STORAGE_SLOT;
assembly {
s.slot := slot
}
}
/// @dev Returns the effective max daily vouches. Uses storage value if set, otherwise the default.
/// This provides backward compatibility: existing deployments with maxDailyVouches = 0 (unset)
/// automatically get DEFAULT_MAX_DAILY_VOUCHES.
function _getMaxDailyVouches() internal view returns (uint32) {
uint32 stored = _layout().maxDailyVouches;
return stored > 0 ? stored : DEFAULT_MAX_DAILY_VOUCHES;
}
/*═══════════════════════════════════════ REENTRANCY PROTECTION ═══════════════════════════════════*/
modifier nonReentrant() {
Layout storage l = _layout();
require(l._notEntered != 2, "ReentrancyGuard: reentrant call");
l._notEntered = 2;
_;
l._notEntered = 1;
}
modifier whenNotPaused() {
require(!_layout()._paused, "Contract is paused");
_;
}
/*═══════════════════════════════════════ FLAG CONSTANTS ═══════════════════════════════════════*/
uint8 private constant ELIGIBLE_FLAG = 0x01; // bit 0
uint8 private constant STANDING_FLAG = 0x02; // bit 1
uint8 private constant ENABLED_FLAG = 0x01; // bit 0
uint8 private constant COMBINE_HIERARCHY_FLAG = 0x02; // bit 1
/*═══════════════════════════════════ METADATA CONSTANTS ═══════════════════════════════════════*/
bytes16 private constant HEX_DIGITS = "0123456789abcdef";
/*═══════════════════════════════════ RATE LIMITING CONSTANTS ═══════════════════════════════════*/
uint32 private constant DEFAULT_MAX_DAILY_VOUCHES = 20;
uint256 private constant NEW_USER_RESTRICTION_DAYS = 0; // Removed wait period for immediate vouching
uint256 private constant SECONDS_PER_DAY = 86400;
/*═══════════════════════════════════════════ EVENTS ═══════════════════════════════════════════*/
event WearerEligibilityUpdated(
address indexed wearer, uint256 indexed hatId, bool eligible, bool standing, address indexed admin
);
event DefaultEligibilityUpdated(uint256 indexed hatId, bool eligible, bool standing, address indexed admin);
event BulkWearerEligibilityUpdated(
address[] wearers, uint256 indexed hatId, bool eligible, bool standing, address indexed admin
);
event SuperAdminTransferred(address indexed oldSuperAdmin, address indexed newSuperAdmin);
event EligibilityModuleInitialized(address indexed superAdmin, address indexed hatsContract);
event Vouched(address indexed voucher, address indexed wearer, uint256 indexed hatId, uint32 newCount);
event VouchRevoked(address indexed voucher, address indexed wearer, uint256 indexed hatId, uint32 newCount);
event WearerVouchesCleared(address indexed wearer, uint256 indexed hatId, address indexed admin);
event VouchConfigSet(
uint256 indexed hatId, uint32 quorum, uint256 membershipHatId, bool enabled, bool combineWithHierarchy
);
event UserJoinTimeSet(address indexed user, uint256 indexed joinTime);
event VouchingRateLimitExceededEvent(address indexed user);
event NewUserVouchingRestrictedEvent(address indexed user);
event EligibilityModuleAdminHatSet(uint256 indexed hatId);
event HatClaimed(address indexed wearer, uint256 indexed hatId);
event HatCreatedWithEligibility(
address indexed creator,
uint256 indexed parentHatId,
uint256 indexed newHatId,
bool defaultEligible,
bool defaultStanding,
uint256 mintedCount
);
event Paused(address indexed account);
event Unpaused(address indexed account);
event HatMetadataUpdated(uint256 indexed hatId, string name, bytes32 metadataCID);
event RoleApplicationSubmitted(uint256 indexed hatId, address indexed applicant, bytes32 applicationHash);
event RoleApplicationWithdrawn(uint256 indexed hatId, address indexed applicant);
event MaxDailyVouchesSet(uint32 maxDailyVouches);
/*═════════════════════════════════════════ MODIFIERS ═════════════════════════════════════════*/
modifier onlySuperAdmin() {
if (msg.sender != _layout().superAdmin) revert NotSuperAdmin();
_;
}
modifier onlyHatAdmin(uint256 targetHatId) {
Layout storage l = _layout();
if (msg.sender != l.superAdmin && !l.hats.isAdminOfHat(msg.sender, targetHatId)) revert NotAuthorizedAdmin();
_;
}
/*═══════════════════════════════════════ INITIALIZATION ═══════════════════════════════════════*/
constructor() {
_disableInitializers();
}
function initialize(address _superAdmin, address _hats, address _toggleModule) external initializer {
if (_superAdmin == address(0) || _hats == address(0)) revert ZeroAddress();
Layout storage l = _layout();
l._notEntered = 1;
l.superAdmin = _superAdmin;
l.hats = IHats(_hats);
l.toggleModule = _toggleModule;
l._paused = false;
emit EligibilityModuleInitialized(_superAdmin, _hats);
}
/*═══════════════════════════════════ PAUSE MANAGEMENT ═══════════════════════════════════════*/
function pause() external onlySuperAdmin {
_layout()._paused = true;
emit Paused(msg.sender);
}
function unpause() external onlySuperAdmin {
_layout()._paused = false;
emit Unpaused(msg.sender);
}
function paused() external view returns (bool) {
return _layout()._paused;
}
/*═══════════════════════════════════ AUTHORIZATION LOGIC ═══════════════════════════════════════*/
// Authorization is now handled natively by the Hats tree structure using onlyHatAdmin modifier
/*═══════════════════════════════════ ELIGIBILITY MANAGEMENT ═══════════════════════════════════════*/
function setWearerEligibility(address wearer, uint256 hatId, bool _eligible, bool _standing)
external
onlyHatAdmin(hatId)
whenNotPaused
{
if (wearer == address(0)) revert ZeroAddress();
_setWearerEligibilityInternal(wearer, hatId, _eligible, _standing);
}
function setDefaultEligibility(uint256 hatId, bool _eligible, bool _standing)
external
onlyHatAdmin(hatId)
whenNotPaused
{
Layout storage l = _layout();
l.defaultRules[hatId] = WearerRules(_packWearerFlags(_eligible, _standing));
emit DefaultEligibilityUpdated(hatId, _eligible, _standing, msg.sender);
}
function clearWearerEligibility(address wearer, uint256 hatId) external onlyHatAdmin(hatId) whenNotPaused {
if (wearer == address(0)) revert ZeroAddress();
Layout storage l = _layout();
delete l.wearerRules[wearer][hatId];
delete l.hasSpecificWearerRules[wearer][hatId];
emit WearerEligibilityUpdated(wearer, hatId, false, false, msg.sender);
}
function setBulkWearerEligibility(address[] calldata wearers, uint256 hatId, bool _eligible, bool _standing)
external
onlyHatAdmin(hatId)
{
uint256 length = wearers.length;
if (length == 0) revert ArrayLengthMismatch();
uint8 packedFlags = _packWearerFlags(_eligible, _standing);
Layout storage l = _layout();
// Use unchecked for gas optimization in the loop only
for (uint256 i; i < length;) {
address wearer = wearers[i];
if (wearer == address(0)) revert ZeroAddress();
l.wearerRules[wearer][hatId] = WearerRules(packedFlags);
l.hasSpecificWearerRules[wearer][hatId] = true;
unchecked {
++i;
}
}
emit BulkWearerEligibilityUpdated(wearers, hatId, _eligible, _standing, msg.sender);
}
/// @dev Internal function to reduce code duplication
function _setWearerEligibilityInternal(address wearer, uint256 hatId, bool _eligible, bool _standing) internal {
Layout storage l = _layout();
l.wearerRules[wearer][hatId] = WearerRules(_packWearerFlags(_eligible, _standing));
l.hasSpecificWearerRules[wearer][hatId] = true;
emit WearerEligibilityUpdated(wearer, hatId, _eligible, _standing, msg.sender);
}
/*═══════════════════════════════════ BATCH OPERATIONS ═══════════════════════════════════════*/
function batchSetWearerEligibility(
uint256 hatId,
address[] calldata wearers,
bool[] calldata eligibleFlags,
bool[] calldata standingFlags
) external onlyHatAdmin(hatId) {
uint256 length = wearers.length;
if (length != eligibleFlags.length || length != standingFlags.length) {
revert ArrayLengthMismatch();
}
Layout storage l = _layout();
// Use unchecked for gas optimization
unchecked {
for (uint256 i; i < length; ++i) {
address wearer = wearers[i];
l.wearerRules[wearer][hatId] = WearerRules(_packWearerFlags(eligibleFlags[i], standingFlags[i]));
l.hasSpecificWearerRules[wearer][hatId] = true;
emit WearerEligibilityUpdated(wearer, hatId, eligibleFlags[i], standingFlags[i], msg.sender);
}
}
}
/**
* @notice Batch set wearer eligibility across multiple hats - optimized for HatsTreeSetup
* @dev Sets eligibility for multiple (wearer, hatId) pairs in a single call
* @param wearers Array of wearer addresses
* @param hatIds Array of hat IDs (must match wearers length)
* @param eligible Eligibility status to set for all pairs
* @param standing Standing status to set for all pairs
*/
function batchSetWearerEligibilityMultiHat(
address[] calldata wearers,
uint256[] calldata hatIds,
bool eligible,
bool standing
) external onlySuperAdmin whenNotPaused {
uint256 length = wearers.length;
if (length != hatIds.length) revert ArrayLengthMismatch();
Layout storage l = _layout();
uint8 packedFlags = _packWearerFlags(eligible, standing);
unchecked {
for (uint256 i; i < length; ++i) {
address wearer = wearers[i];
uint256 hatId = hatIds[i];
l.wearerRules[wearer][hatId] = WearerRules(packedFlags);
l.hasSpecificWearerRules[wearer][hatId] = true;
emit WearerEligibilityUpdated(wearer, hatId, eligible, standing, msg.sender);
}
}
}
/**
* @notice Batch set default eligibility for multiple hats
* @dev Sets default eligibility rules for multiple hats in a single call
* @param hatIds Array of hat IDs
* @param eligibles Array of eligibility flags
* @param standings Array of standing flags
*/
function batchSetDefaultEligibility(uint256[] calldata hatIds, bool[] calldata eligibles, bool[] calldata standings)
external
onlySuperAdmin
whenNotPaused
{
uint256 length = hatIds.length;
if (length != eligibles.length || length != standings.length) {
revert ArrayLengthMismatch();
}
Layout storage l = _layout();
unchecked {
for (uint256 i; i < length; ++i) {
uint256 hatId = hatIds[i];
l.defaultRules[hatId] = WearerRules(_packWearerFlags(eligibles[i], standings[i]));
emit DefaultEligibilityUpdated(hatId, eligibles[i], standings[i], msg.sender);
}
}
}
/**
* @notice Batch mint hats to multiple wearers
* @dev Mints multiple hats in a single call - optimized for HatsTreeSetup
* @param hatIds Array of hat IDs to mint
* @param wearers Array of addresses to receive hats
*/
function batchMintHats(uint256[] calldata hatIds, address[] calldata wearers) external onlySuperAdmin {
uint256 length = hatIds.length;
if (length != wearers.length) revert ArrayLengthMismatch();
Layout storage l = _layout();
unchecked {
for (uint256 i; i < length; ++i) {
bool success = l.hats.mintHat(hatIds[i], wearers[i]);
require(success, "Hat minting failed");
}
}
}
/**
* @notice Batch register hat creations for subgraph indexing
* @dev Registers multiple hats in a single call - optimized for HatsTreeSetup
* @param hatIds Array of hat IDs that were created
* @param parentHatIds Array of parent hat IDs
* @param defaultEligibles Array of default eligibility flags
* @param defaultStandings Array of default standing flags
*/
function batchRegisterHatCreation(
uint256[] calldata hatIds,
uint256[] calldata parentHatIds,
bool[] calldata defaultEligibles,
bool[] calldata defaultStandings
) external onlySuperAdmin {
uint256 length = hatIds.length;
if (length != parentHatIds.length || length != defaultEligibles.length || length != defaultStandings.length) {
revert ArrayLengthMismatch();
}
Layout storage l = _layout();
unchecked {
for (uint256 i; i < length; ++i) {
uint256 hatId = hatIds[i];
l.defaultRules[hatId] = WearerRules(_packWearerFlags(defaultEligibles[i], defaultStandings[i]));
emit DefaultEligibilityUpdated(hatId, defaultEligibles[i], defaultStandings[i], msg.sender);
emit HatCreatedWithEligibility(
msg.sender, parentHatIds[i], hatId, defaultEligibles[i], defaultStandings[i], 0
);
}
}
}
/**
* @dev Registers multiple hats with metadata in a single call - optimized for HatsTreeSetup
* @dev This version also emits HatMetadataUpdated events for subgraph indexing
* @param hatIds Array of hat IDs that were created
* @param parentHatIds Array of parent hat IDs
* @param defaultEligibles Array of default eligibility flags
* @param defaultStandings Array of default standing flags
* @param names Array of role names for metadata
* @param metadataCIDs Array of IPFS CIDs for extended metadata
*/
function batchRegisterHatCreationWithMetadata(
uint256[] calldata hatIds,
uint256[] calldata parentHatIds,
bool[] calldata defaultEligibles,
bool[] calldata defaultStandings,
string[] calldata names,
bytes32[] calldata metadataCIDs
) external onlySuperAdmin {
uint256 length = hatIds.length;
if (
length != parentHatIds.length || length != defaultEligibles.length || length != defaultStandings.length
|| length != names.length || length != metadataCIDs.length
) {
revert ArrayLengthMismatch();
}
Layout storage l = _layout();
unchecked {
for (uint256 i; i < length; ++i) {
uint256 hatId = hatIds[i];
l.defaultRules[hatId] = WearerRules(_packWearerFlags(defaultEligibles[i], defaultStandings[i]));
emit DefaultEligibilityUpdated(hatId, defaultEligibles[i], defaultStandings[i], msg.sender);
emit HatCreatedWithEligibility(
msg.sender, parentHatIds[i], hatId, defaultEligibles[i], defaultStandings[i], 0
);
// Also emit metadata event for subgraph indexing
emit HatMetadataUpdated(hatId, names[i], metadataCIDs[i]);
}
}
}
/*═══════════════════════════════════ HAT CREATION ═══════════════════════════════════════*/
function createHatWithEligibility(CreateHatParams calldata params)
external
onlyHatAdmin(params.parentHatId)
returns (uint256 newHatId)
{
Layout storage l = _layout();
// Create the new hat
newHatId = l.hats
.createHat(
params.parentHatId,
params.details,
params.maxSupply,
address(this),
l.toggleModule,
params._mutable,
params.imageURI
);
// Set default eligibility rules
l.defaultRules[newHatId] = WearerRules(_packWearerFlags(params.defaultEligible, params.defaultStanding));
// Automatically activate the hat
IToggleModule(l.toggleModule).setHatStatus(newHatId, true);
emit DefaultEligibilityUpdated(newHatId, params.defaultEligible, params.defaultStanding, msg.sender);
// Handle initial minting if specified
uint256 mintLength = params.mintToAddresses.length;
if (mintLength > 0) {
_handleInitialMinting(
newHatId, params.mintToAddresses, params.wearerEligibleFlags, params.wearerStandingFlags, mintLength
);
}
emit HatCreatedWithEligibility(
msg.sender, params.parentHatId, newHatId, params.defaultEligible, params.defaultStanding, mintLength
);
}
/// @notice Register a hat that was created externally and emit the HatCreatedWithEligibility event
/// @dev Used by HatsTreeSetup to emit events for subgraph indexing without needing admin rights to create hats
/// @param hatId The ID of the hat that was created
/// @param parentHatId The ID of the parent hat
/// @param defaultEligible Whether wearers are eligible by default
/// @param defaultStanding Whether wearers have good standing by default
function registerHatCreation(uint256 hatId, uint256 parentHatId, bool defaultEligible, bool defaultStanding)
external
onlyHatAdmin(parentHatId)
{
Layout storage l = _layout();
l.defaultRules[hatId] = WearerRules(_packWearerFlags(defaultEligible, defaultStanding));
emit DefaultEligibilityUpdated(hatId, defaultEligible, defaultStanding, msg.sender);
emit HatCreatedWithEligibility(msg.sender, parentHatId, hatId, defaultEligible, defaultStanding, 0);
}
/// @dev Internal function to handle initial minting logic
function _handleInitialMinting(
uint256 hatId,
address[] calldata addresses,
bool[] calldata eligibleFlags,
bool[] calldata standingFlags,
uint256 length
) internal {
Layout storage l = _layout();
// If specific eligibility flags provided, validate and set them
if (eligibleFlags.length > 0) {
if (length != eligibleFlags.length || length != standingFlags.length) {
revert ArrayLengthMismatch();
}
// Set specific eligibility and mint
unchecked {
for (uint256 i; i < length; ++i) {
address wearer = addresses[i];
l.wearerRules[wearer][hatId] = WearerRules(_packWearerFlags(eligibleFlags[i], standingFlags[i]));
l.hasSpecificWearerRules[wearer][hatId] = true;
bool success = l.hats.mintHat(hatId, wearer);
require(success, "Hat minting failed");
emit WearerEligibilityUpdated(wearer, hatId, eligibleFlags[i], standingFlags[i], msg.sender);
}
}
} else {
// Just mint with default eligibility
unchecked {
for (uint256 i; i < length; ++i) {
bool success = l.hats.mintHat(hatId, addresses[i]);
require(success, "Hat minting failed");
}
}
}
}
/*═══════════════════════════════════ MODULE MANAGEMENT ═══════════════════════════════════════*/
function setEligibilityModuleAdminHat(uint256 hatId) external onlySuperAdmin {
_layout().eligibilityModuleAdminHat = hatId;
emit EligibilityModuleAdminHatSet(hatId);
}
function mintHatToAddress(uint256 hatId, address wearer) external onlySuperAdmin {
bool success = _layout().hats.mintHat(hatId, wearer);
require(success, "Hat minting failed");
}
function setToggleModule(address _toggleModule) external onlySuperAdmin {
_layout().toggleModule = _toggleModule;
}
function transferSuperAdmin(address newSuperAdmin) external onlySuperAdmin {
if (newSuperAdmin == address(0)) revert ZeroAddress();
Layout storage l = _layout();
address oldSuperAdmin = l.superAdmin;
l.superAdmin = newSuperAdmin;
emit SuperAdminTransferred(oldSuperAdmin, newSuperAdmin);
}
function setUserJoinTime(address user, uint256 joinTime) external onlySuperAdmin {
_layout().userJoinTime[user] = joinTime;
emit UserJoinTimeSet(user, joinTime);
}
function setUserJoinTimeNow(address user) external onlySuperAdmin {
_layout().userJoinTime[user] = block.timestamp;
emit UserJoinTimeSet(user, block.timestamp);
}
/// @notice Set the maximum number of vouches a user can give per day
/// @param maxVouches New daily vouch limit (must be > 0)
function setMaxDailyVouches(uint32 maxVouches) external onlySuperAdmin {
if (maxVouches == 0) revert InvalidMaxDailyVouches();
_layout().maxDailyVouches = maxVouches;
emit MaxDailyVouchesSet(maxVouches);
}
/// @notice Get the current max daily vouch limit
function getMaxDailyVouches() external view returns (uint32) {
return _getMaxDailyVouches();
}
/*═══════════════════════════════════ METADATA MANAGEMENT ═══════════════════════════════════════*/
/**
* @notice Update hat metadata CID (uses native Hats Protocol changeHatDetails)
* @dev Emits HatDetailsChanged event from Hats Protocol (subgraph indexable)
* @param hatId The ID of the hat to update
* @param name The role name
* @param metadataCID The IPFS CID for extended metadata (bytes32(0) to clear)
*/
function updateHatMetadata(uint256 hatId, string memory name, bytes32 metadataCID)
external
onlyHatAdmin(hatId)
whenNotPaused
{
string memory details = _formatHatDetails(name, metadataCID);
_layout().hats.changeHatDetails(hatId, details);
// Native HatDetailsChanged event is emitted by Hats Protocol
emit HatMetadataUpdated(hatId, name, metadataCID);
}
/**
* @notice Format hat details string - uses CID if provided, otherwise name
* @param name The role name (fallback if no CID)
* @param metadataCID The IPFS CID for extended metadata (bytes32(0) if none)
* @return The formatted details string
*/
function _formatHatDetails(string memory name, bytes32 metadataCID) internal pure returns (string memory) {
if (metadataCID == bytes32(0)) {
return name;
}
return _bytes32ToHexString(metadataCID);
}
/**
* @notice Convert bytes32 to hex string with 0x prefix
* @param value The bytes32 value to convert
* @return The hex string representation
*/
function _bytes32ToHexString(bytes32 value) internal pure returns (string memory) {
bytes memory buffer = new bytes(66); // 2 for "0x" + 64 for hex chars
buffer[0] = "0";
buffer[1] = "x";
for (uint256 i = 0; i < 32; i++) {
buffer[2 + i * 2] = HEX_DIGITS[uint8(value[i] >> 4)];
buffer[3 + i * 2] = HEX_DIGITS[uint8(value[i] & 0x0f)];
}
return string(buffer);
}
/*═══════════════════════════════════ VOUCHING SYSTEM ═══════════════════════════════════════*/
function configureVouching(uint256 hatId, uint32 quorum, uint256 membershipHatId, bool combineWithHierarchy)
external
onlySuperAdmin
{
Layout storage l = _layout();
bool enabled = quorum > 0;
l.vouchConfigs[hatId] = VouchConfig({
quorum: quorum, membershipHatId: membershipHatId, flags: _packVouchFlags(enabled, combineWithHierarchy)
});
// Invalidate stale vouch data from prior configuration
l.vouchConfigEpoch[hatId]++;
emit VouchConfigSet(hatId, quorum, membershipHatId, enabled, combineWithHierarchy);
}
/**
* @notice Batch configure vouching for multiple hats
* @dev Sets vouching configuration for multiple hats in a single call - gas optimized for org deployment
* @param hatIds Array of hat IDs to configure
* @param quorums Array of quorum values (number of vouches required)
* @param membershipHatIds Array of hat IDs whose wearers can vouch
* @param combineWithHierarchyFlags Array of flags for combining with hierarchy eligibility
*/
function batchConfigureVouching(
uint256[] calldata hatIds,
uint32[] calldata quorums,
uint256[] calldata membershipHatIds,
bool[] calldata combineWithHierarchyFlags
) external onlySuperAdmin {
uint256 length = hatIds.length;
if (length != quorums.length || length != membershipHatIds.length || length != combineWithHierarchyFlags.length)
{
revert ArrayLengthMismatch();
}
Layout storage l = _layout();
unchecked {
for (uint256 i; i < length; ++i) {
uint256 hatId = hatIds[i];
bool enabled = quorums[i] > 0;
l.vouchConfigs[hatId] = VouchConfig({
quorum: quorums[i],
membershipHatId: membershipHatIds[i],
flags: _packVouchFlags(enabled, combineWithHierarchyFlags[i])
});
// Invalidate stale vouch data
l.vouchConfigEpoch[hatId]++;
emit VouchConfigSet(hatId, quorums[i], membershipHatIds[i], enabled, combineWithHierarchyFlags[i]);
}
}
}
function vouchFor(address wearer, uint256 hatId) external whenNotPaused {
if (wearer == address(0)) revert ZeroAddress();
if (wearer == msg.sender) revert CannotVouchForSelf();
Layout storage l = _layout();
VouchConfig memory config = l.vouchConfigs[hatId];
if (!_isVouchingEnabled(config.flags)) revert VouchingNotEnabled();
// Check vouching authorization
bool isAuthorized = l.hats.isWearerOfHat(msg.sender, config.membershipHatId);
// If combineWithHierarchy is enabled, also check if voucher has admin privileges for this hat
if (!isAuthorized && _shouldCombineWithHierarchy(config.flags)) {
isAuthorized = l.hats.isAdminOfHat(msg.sender, hatId);
}
if (!isAuthorized) revert NotAuthorizedToVouch();
// Epoch-aware stale data handling:
// If the wearer's count is from a prior epoch, reset it lazily.
uint256 currentEpoch = l.vouchConfigEpoch[hatId];
if (l.wearerVouchEpoch[hatId][wearer] != currentEpoch) {
l.currentVouchCount[hatId][wearer] = 0;
l.wearerVouchEpoch[hatId][wearer] = currentEpoch;
}
// AlreadyVouched: only if this specific voucher's record is from the current epoch
if (l.vouchers[hatId][wearer][msg.sender] && l.voucherRecordEpoch[hatId][wearer][msg.sender] == currentEpoch) {
revert AlreadyVouched();
}
// SECURITY: Rate limiting checks
_checkVouchingRateLimit(msg.sender);
// Record the vouch with its epoch
l.vouchers[hatId][wearer][msg.sender] = true;
l.voucherRecordEpoch[hatId][wearer][msg.sender] = currentEpoch;
uint32 newCount = l.currentVouchCount[hatId][wearer] + 1;
l.currentVouchCount[hatId][wearer] = newCount;
// Update daily vouch count
uint256 currentDay = block.timestamp / SECONDS_PER_DAY;
uint32 dailyCount = l.dailyVouchCount[msg.sender][currentDay] + 1;
l.dailyVouchCount[msg.sender][currentDay] = dailyCount;
emit Vouched(msg.sender, wearer, hatId, newCount);
}
function _checkVouchingRateLimit(address user) internal view {
Layout storage l = _layout();
// Check if user has been around long enough to vouch
// NEW_USER_RESTRICTION_DAYS = 0, so anyone can vouch immediately
uint256 joinTime = l.userJoinTime[user];
if (joinTime != 0) {
// Only check if join time is set
uint256 daysSinceJoined = (block.timestamp - joinTime) / SECONDS_PER_DAY;
if (daysSinceJoined < NEW_USER_RESTRICTION_DAYS) {
revert NewUserVouchingRestricted();
}
}
// If joinTime is 0 (never set), allow vouching since NEW_USER_RESTRICTION_DAYS = 0
// Check daily vouch limit
uint256 currentDay = block.timestamp / SECONDS_PER_DAY;
if (l.dailyVouchCount[user][currentDay] >= _getMaxDailyVouches()) {
revert VouchingRateLimitExceeded();
}
}
function revokeVouch(address wearer, uint256 hatId) external whenNotPaused {
if (wearer == address(0)) revert ZeroAddress();
Layout storage l = _layout();
VouchConfig memory config = l.vouchConfigs[hatId];
if (!_isVouchingEnabled(config.flags)) revert VouchingNotEnabled();
// Only current-epoch vouch records can be revoked
uint256 currentEpoch = l.vouchConfigEpoch[hatId];
if (l.wearerVouchEpoch[hatId][wearer] != currentEpoch) revert HasNotVouched();
if (!l.vouchers[hatId][wearer][msg.sender] || l.voucherRecordEpoch[hatId][wearer][msg.sender] != currentEpoch) {
revert HasNotVouched();
}
// Remove the vouch
l.vouchers[hatId][wearer][msg.sender] = false;
uint32 newCount = l.currentVouchCount[hatId][wearer] - 1;
l.currentVouchCount[hatId][wearer] = newCount;
// Note: dailyVouchCount is NOT decremented on revocation.
// It's a rate limiter only — revoking doesn't give back vouch slots.
emit VouchRevoked(msg.sender, wearer, hatId, newCount);
// Handle hat revocation if needed
if (
!_shouldCombineWithHierarchy(config.flags) && newCount < config.quorum
&& l.hats.isWearerOfHat(wearer, hatId) && !l.hasSpecificWearerRules[wearer][hatId]
) {
l.hats.setHatWearerStatus(hatId, wearer, false, false);
}
}
function resetVouches(uint256 hatId) external onlySuperAdmin {
Layout storage l = _layout();
delete l.vouchConfigs[hatId];
// Increment epoch to invalidate all stale vouch counts and AlreadyVouched records
l.vouchConfigEpoch[hatId]++;
emit VouchConfigSet(hatId, 0, 0, false, false);
}
/**
* @notice Surgical per-wearer vouch invalidation for a single hat.
* @dev Sets `wearerVouchEpoch` to a sentinel value that will never match
* `vouchConfigEpoch`, so the wearer's effective vouch count for this
* hat is permanently 0 from this point forward (until they get
* re-vouched, which writes a fresh epoch via `vouchFor`).
* Combined with `setWearerEligibility(wearer, hatId, false, false)`
* this is the surgical equivalent of `resetVouches` for one wearer
* — does NOT touch other wearers' vouches and does NOT disable
* vouching org-wide. Designed for the election-loser case on
* vouching-gated hats with available supply.
* @param wearer The address whose vouch state to clear for this hat
* @param hatId The hat for which to clear vouches
*/
function clearWearerVouches(address wearer, uint256 hatId) external onlySuperAdmin whenNotPaused {
if (wearer == address(0)) revert ZeroAddress();
Layout storage l = _layout();
// type(uint256).max guarantees wearerVouchEpoch != vouchConfigEpoch for
// any future epoch (epoch is incremented one-at-a-time by ~25 lines of
// code; reaching uint256.max would take 2^256 admin calls).
l.wearerVouchEpoch[hatId][wearer] = type(uint256).max;
delete l.currentVouchCount[hatId][wearer];
emit WearerVouchesCleared(wearer, hatId, msg.sender);
}
/**
* @notice Allows a user to claim a hat they are eligible for after being vouched
* @dev User must have sufficient vouches to be eligible. This is the claim-based pattern
* where users explicitly accept their role rather than having it auto-minted.
* The EligibilityModule contract mints the hat using its ELIGIBILITY_ADMIN permissions.
* @param hatId The ID of the hat to claim
*/
function claimVouchedHat(uint256 hatId) external whenNotPaused nonReentrant {
Layout storage l = _layout();
// Check if caller is eligible to claim this hat
(bool eligible, bool standing) = this.getWearerStatus(msg.sender, hatId);
require(eligible && standing, "Not eligible to claim hat");
// Check if already wearing the hat
require(!l.hats.isWearerOfHat(msg.sender, hatId), "Already wearing hat");
// State change BEFORE external call (CEI pattern)
delete l.roleApplications[hatId][msg.sender];
// Mint the hat to the caller using EligibilityModule's admin powers
bool success = l.hats.mintHat(hatId, msg.sender);
require(success, "Hat minting failed");
emit HatClaimed(msg.sender, hatId);
}
/*═══════════════════════════════════ ROLE APPLICATION SYSTEM ═══════════════════════════════════════*/
/// @notice Submit an application for a role (hat) that has vouching enabled.
/// This is a signaling mechanism — it does not grant eligibility.
/// @param hatId The hat ID to apply for
/// @param applicationHash IPFS CID sha256 digest of the application details
function applyForRole(uint256 hatId, bytes32 applicationHash) external whenNotPaused {
if (applicationHash == bytes32(0)) revert InvalidApplicationHash();
Layout storage l = _layout();
VouchConfig memory config = l.vouchConfigs[hatId];
if (!_isVouchingEnabled(config.flags)) revert VouchingNotEnabled();
if (l.roleApplications[hatId][msg.sender] != bytes32(0)) revert ApplicationAlreadyExists();
require(!l.hats.isWearerOfHat(msg.sender, hatId), "Already wearing hat");
l.roleApplicants[hatId].push(msg.sender);
l.roleApplications[hatId][msg.sender] = applicationHash;
emit RoleApplicationSubmitted(hatId, msg.sender, applicationHash);
}
/// @notice Withdraw a previously submitted role application.
/// @param hatId The hat ID to withdraw the application from
function withdrawApplication(uint256 hatId) external whenNotPaused {
Layout storage l = _layout();
if (l.roleApplications[hatId][msg.sender] == bytes32(0)) revert NoActiveApplication();
delete l.roleApplications[hatId][msg.sender];
emit RoleApplicationWithdrawn(hatId, msg.sender);
}
/*═══════════════════════════════════ ELIGIBILITY INTERFACE ═══════════════════════════════════════*/
function getWearerStatus(address wearer, uint256 hatId) external view returns (bool eligible, bool standing) {
Layout storage l = _layout();
VouchConfig memory config = l.vouchConfigs[hatId];
bool hierarchyEligible;
bool hierarchyStanding;
bool vouchEligible;
bool vouchStanding;
// Check hierarchy path
WearerRules memory rules;
if (l.hasSpecificWearerRules[wearer][hatId]) {
rules = l.wearerRules[wearer][hatId];
} else {
rules = l.defaultRules[hatId];
}
(hierarchyEligible, hierarchyStanding) = _unpackWearerFlags(rules.flags);
// Check vouch path if enabled (only count vouches from current epoch)
uint32 effectiveVouchCount =
(l.wearerVouchEpoch[hatId][wearer] == l.vouchConfigEpoch[hatId]) ? l.currentVouchCount[hatId][wearer] : 0;
if (_isVouchingEnabled(config.flags) && effectiveVouchCount >= config.quorum) {
vouchEligible = true;
vouchStanding = true;
}
// Combine results
if (_isVouchingEnabled(config.flags)) {
if (_shouldCombineWithHierarchy(config.flags)) {
eligible = hierarchyEligible || vouchEligible;
standing = hierarchyStanding || vouchStanding;
} else {
eligible = vouchEligible;
standing = vouchStanding;
}
} else {
eligible = hierarchyEligible;
standing = hierarchyStanding;
}
// If standing is false, eligibility MUST also be false per IHatsEligibility interface
if (!standing) {
eligible = false;
}
}
/*═════════════════════════════════════ VIEW FUNCTIONS ═════════════════════════════════════════*/
function getVouchConfig(uint256 hatId) external view returns (VouchConfig memory) {
return _layout().vouchConfigs[hatId];
}
function isVouchingEnabled(uint256 hatId) external view returns (bool) {
return _isVouchingEnabled(_layout().vouchConfigs[hatId].flags);
}
function combinesWithHierarchy(uint256 hatId) external view returns (bool) {
return _shouldCombineWithHierarchy(_layout().vouchConfigs[hatId].flags);
}
function getWearerRules(address wearer, uint256 hatId) external view returns (bool eligible, bool standing) {
Layout storage l = _layout();
if (l.hasSpecificWearerRules[wearer][hatId]) {
return _unpackWearerFlags(l.wearerRules[wearer][hatId].flags);
} else {
return _unpackWearerFlags(l.defaultRules[hatId].flags);