-
Notifications
You must be signed in to change notification settings - Fork 24
/
Copy pathExtendedJurisdiction.sol
1639 lines (1438 loc) · 62.9 KB
/
ExtendedJurisdiction.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
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
pragma solidity ^0.4.25;
import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
import "openzeppelin-solidity/contracts/lifecycle/Pausable.sol";
import "openzeppelin-solidity/contracts/cryptography/ECDSA.sol";
import "openzeppelin-solidity/contracts/math/SafeMath.sol";
import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol";
import "./AttributeRegistryInterface.sol";
import "./BasicJurisdictionInterface.sol";
import "./ExtendedJurisdictionInterface.sol";
/**
* @title An extended TPL jurisdiction for assigning attributes to addresses.
*/
contract ExtendedJurisdiction is Ownable, Pausable, AttributeRegistryInterface, BasicJurisdictionInterface, ExtendedJurisdictionInterface {
using ECDSA for bytes32;
using SafeMath for uint256;
// validators are entities who can add or authorize addition of new attributes
struct Validator {
bool exists;
uint256 index; // NOTE: consider use of uint88 to pack struct
address signingKey;
string description;
}
// attributes are properties that validators associate with specific addresses
struct IssuedAttribute {
bool exists;
bool setPersonally;
address operator;
address validator;
uint256 value;
uint256 stake;
}
// attributes also have associated type - metadata common to each attribute
struct AttributeType {
bool exists;
bool restricted;
bool onlyPersonal;
uint256 index; // NOTE: consider use of uint72 to pack struct
address secondarySource;
uint256 secondaryAttributeTypeID;
uint256 minimumStake;
uint256 jurisdictionFee;
string description;
mapping(address => bool) approvedValidators;
}
// top-level information about attribute types is held in a mapping of structs
mapping(uint256 => AttributeType) private _attributeTypes;
// the jurisdiction retains a mapping of addresses with assigned attributes
mapping(address => mapping(uint256 => IssuedAttribute)) private _issuedAttributes;
// there is also a mapping to identify all approved validators and their keys
mapping(address => Validator) private _validators;
// each registered signing key maps back to a specific validator
mapping(address => address) private _signingKeys;
// once attribute types are assigned to an ID, they cannot be modified
mapping(uint256 => bytes32) private _attributeTypeHashes;
// submitted attribute approvals are retained to prevent reuse after removal
mapping(bytes32 => bool) private _invalidAttributeApprovalHashes;
// attribute approvals by validator are held in a mapping
mapping(address => uint256[]) private _validatorApprovals;
// attribute approval index by validator is tracked as well
mapping(address => mapping(uint256 => uint256)) private _validatorApprovalsIndex;
// IDs for all supplied attributes are held in an array (enables enumeration)
uint256[] private _attributeIDs;
// addresses for all designated validators are also held in an array
address[] private _validatorAccounts;
// track any recoverable funds locked in the contract
uint256 private _recoverableFunds;
/**
* @notice Add an attribute type with ID `ID` and description `description` to
* the jurisdiction.
* @param ID uint256 The ID of the attribute type to add.
* @param description string A description of the attribute type.
* @dev Once an attribute type is added with a given ID, the description of the
* attribute type cannot be changed, even if the attribute type is removed and
* added back later.
*/
function addAttributeType(
uint256 ID,
string description
) external onlyOwner whenNotPaused {
// prevent existing attributes with the same id from being overwritten
require(
!isAttributeType(ID),
"an attribute type with the provided ID already exists"
);
// calculate a hash of the attribute type based on the type's properties
bytes32 hash = keccak256(
abi.encodePacked(
ID, false, description
)
);
// store hash if attribute type is the first one registered with provided ID
if (_attributeTypeHashes[ID] == bytes32(0)) {
_attributeTypeHashes[ID] = hash;
}
// prevent addition if different attribute type with the same ID has existed
require(
hash == _attributeTypeHashes[ID],
"attribute type properties must match initial properties assigned to ID"
);
// set the attribute mapping, assigning the index as the end of attributeID
_attributeTypes[ID] = AttributeType({
exists: true,
restricted: false, // when true: users can't remove attribute
onlyPersonal: false, // when true: operators can't add attribute
index: _attributeIDs.length,
secondarySource: address(0), // the address of a remote registry
secondaryAttributeTypeID: uint256(0), // the attribute type id to query
minimumStake: uint256(0), // when > 0: users must stake ether to set
jurisdictionFee: uint256(0),
description: description
// NOTE: no approvedValidators variable declaration - must be added later
});
// add the attribute type id to the end of the attributeID array
_attributeIDs.push(ID);
// log the addition of the attribute type
emit AttributeTypeAdded(ID, description);
}
/**
* @notice Add a restricted attribute type with ID `ID` and description
* `description` to the jurisdiction. Restricted attribute types can only be
* removed by the issuing validator or the jurisdiction.
* @param ID uint256 The ID of the restricted attribute type to add.
* @param description string A description of the restricted attribute type.
* @dev Once an attribute type is added with a given ID, the description or the
* restricted status of the attribute type cannot be changed, even if the
* attribute type is removed and added back later.
*/
function addRestrictedAttributeType(
uint256 ID,
string description
) external onlyOwner whenNotPaused {
// prevent existing attributes with the same id from being overwritten
require(
!isAttributeType(ID),
"an attribute type with the provided ID already exists"
);
// calculate a hash of the attribute type based on the type's properties
bytes32 hash = keccak256(
abi.encodePacked(
ID, true, description
)
);
// store hash if attribute type is the first one registered with provided ID
if (_attributeTypeHashes[ID] == bytes32(0)) {
_attributeTypeHashes[ID] = hash;
}
// prevent addition if different attribute type with the same ID has existed
require(
hash == _attributeTypeHashes[ID],
"attribute type properties must match initial properties assigned to ID"
);
// set the attribute mapping, assigning the index as the end of attributeID
_attributeTypes[ID] = AttributeType({
exists: true,
restricted: true, // when true: users can't remove attribute
onlyPersonal: false, // when true: operators can't add attribute
index: _attributeIDs.length,
secondarySource: address(0), // the address of a remote registry
secondaryAttributeTypeID: uint256(0), // the attribute type id to query
minimumStake: uint256(0), // when > 0: users must stake ether to set
jurisdictionFee: uint256(0),
description: description
// NOTE: no approvedValidators variable declaration - must be added later
});
// add the attribute type id to the end of the attributeID array
_attributeIDs.push(ID);
// log the addition of the attribute type
emit AttributeTypeAdded(ID, description);
}
/**
* @notice Enable or disable a restriction for a given attribute type ID `ID`
* that prevents attributes of the given type from being set by operators based
* on the provided value for `onlyPersonal`.
* @param ID uint256 The attribute type ID in question.
* @param onlyPersonal bool Whether the address may only be set personally.
*/
function setAttributeTypeOnlyPersonal(uint256 ID, bool onlyPersonal) external {
// if the attribute type ID does not exist, there is nothing to remove
require(
isAttributeType(ID),
"unable to set to only personal, no attribute type with the provided ID"
);
// modify the attribute type in the mapping
_attributeTypes[ID].onlyPersonal = onlyPersonal;
}
/**
* @notice Set a secondary source for a given attribute type ID `ID`, with an
* address `registry` of the secondary source in question and a given
* `sourceAttributeTypeID` for attribute type ID to check on the secondary
* source. The secondary source will only be checked for the given attribute in
* cases where no attribute of the given attribute type ID is assigned locally.
* @param ID uint256 The attribute type ID to set the secondary source for.
* @param attributeRegistry address The secondary attribute registry account.
* @param sourceAttributeTypeID uint256 The attribute type ID on the secondary
* source to check.
* @dev To remove a secondary source on an attribute type, the registry address
* should be set to the null address.
*/
function setAttributeTypeSecondarySource(
uint256 ID,
address attributeRegistry,
uint256 sourceAttributeTypeID
) external {
// if the attribute type ID does not exist, there is nothing to remove
require(
isAttributeType(ID),
"unable to set secondary source, no attribute type with the provided ID"
);
// modify the attribute type in the mapping
_attributeTypes[ID].secondarySource = attributeRegistry;
_attributeTypes[ID].secondaryAttributeTypeID = sourceAttributeTypeID;
}
/**
* @notice Set a minimum required stake for a given attribute type ID `ID` and
* an amount of `stake`, to be locked in the jurisdiction upon assignment of
* attributes of the given type. The stake will be applied toward a transaction
* rebate in the event the attribute is revoked, with the remainder returned to
* the staker.
* @param ID uint256 The attribute type ID to set a minimum required stake for.
* @param minimumRequiredStake uint256 The minimum required funds to lock up.
* @dev To remove a stake requirement from an attribute type, the stake amount
* should be set to 0.
*/
function setAttributeTypeMinimumRequiredStake(
uint256 ID,
uint256 minimumRequiredStake
) external {
// if the attribute type ID does not exist, there is nothing to remove
require(
isAttributeType(ID),
"unable to set minimum stake, no attribute type with the provided ID"
);
// modify the attribute type in the mapping
_attributeTypes[ID].minimumStake = minimumRequiredStake;
}
/**
* @notice Set a required fee for a given attribute type ID `ID` and an amount
* of `fee`, to be paid to the owner of the jurisdiction upon assignment of
* attributes of the given type.
* @param ID uint256 The attribute type ID to set the required fee for.
* @param fee uint256 The required fee amount to be paid upon assignment.
* @dev To remove a fee requirement from an attribute type, the fee amount
* should be set to 0.
*/
function setAttributeTypeJurisdictionFee(uint256 ID, uint256 fee) external {
// if the attribute type ID does not exist, there is nothing to remove
require(
isAttributeType(ID),
"unable to set fee, no attribute type with the provided ID"
);
// modify the attribute type in the mapping
_attributeTypes[ID].jurisdictionFee = fee;
}
/**
* @notice Remove the attribute type with ID `ID` from the jurisdiction.
* @param ID uint256 The ID of the attribute type to remove.
* @dev All issued attributes of the given type will become invalid upon
* removal, but will become valid again if the attribute is reinstated.
*/
function removeAttributeType(uint256 ID) external onlyOwner whenNotPaused {
// if the attribute type ID does not exist, there is nothing to remove
require(
isAttributeType(ID),
"unable to remove, no attribute type with the provided ID"
);
// get the attribute ID at the last index of the array
uint256 lastAttributeID = _attributeIDs[_attributeIDs.length.sub(1)];
// set the attributeID at attribute-to-delete.index to the last attribute ID
_attributeIDs[_attributeTypes[ID].index] = lastAttributeID;
// update the index of the attribute type that was moved
_attributeTypes[lastAttributeID].index = _attributeTypes[ID].index;
// remove the (now duplicate) attribute ID at the end by trimming the array
_attributeIDs.length--;
// delete the attribute type's record from the mapping
delete _attributeTypes[ID];
// log the removal of the attribute type
emit AttributeTypeRemoved(ID);
}
/**
* @notice Add account `validator` as a validator with a description
* `description` who can be approved to set attributes of specific types.
* @param validator address The account to assign as the validator.
* @param description string A description of the validator.
* @dev Note that the jurisdiction can add iteslf as a validator if desired.
*/
function addValidator(
address validator,
string description
) external onlyOwner whenNotPaused {
// check that an empty address was not provided by mistake
require(validator != address(0), "must supply a valid address");
// prevent existing validators from being overwritten
require(
!isValidator(validator),
"a validator with the provided address already exists"
);
// prevent duplicate signing keys from being created
require(
_signingKeys[validator] == address(0),
"a signing key matching the provided address already exists"
);
// create a record for the validator
_validators[validator] = Validator({
exists: true,
index: _validatorAccounts.length,
signingKey: validator, // NOTE: this will be initially set to same address
description: description
});
// set the initial signing key (the validator's address) resolving to itself
_signingKeys[validator] = validator;
// add the validator to the end of the _validatorAccounts array
_validatorAccounts.push(validator);
// log the addition of the new validator
emit ValidatorAdded(validator, description);
}
/**
* @notice Remove the validator at address `validator` from the jurisdiction.
* @param validator address The account of the validator to remove.
* @dev Any attributes issued by the validator will become invalid upon their
* removal. If the validator is reinstated, those attributes will become valid
* again. Any approvals to issue attributes of a given type will need to be
* set from scratch in the event a validator is reinstated.
*/
function removeValidator(address validator) external onlyOwner whenNotPaused {
// check that a validator exists at the provided address
require(
isValidator(validator),
"unable to remove, no validator located at the provided address"
);
// first, start removing validator approvals until gas is exhausted
while (_validatorApprovals[validator].length > 0 && gasleft() > 25000) {
// locate the index of last attribute ID in the validator approval group
uint256 lastIndex = _validatorApprovals[validator].length.sub(1);
// locate the validator approval to be removed
uint256 targetApproval = _validatorApprovals[validator][lastIndex];
// remove the record of the approval from the associated attribute type
delete _attributeTypes[targetApproval].approvedValidators[validator];
// remove the record of the index of the approval
delete _validatorApprovalsIndex[validator][targetApproval];
// drop the last attribute ID from the validator approval group
_validatorApprovals[validator].length--;
}
// require that all approvals were successfully removed
require(
_validatorApprovals[validator].length == 0,
"Cannot remove validator - first remove any existing validator approvals"
);
// get the validator address at the last index of the array
address lastAccount = _validatorAccounts[_validatorAccounts.length.sub(1)];
// set the address at validator-to-delete.index to last validator address
_validatorAccounts[_validators[validator].index] = lastAccount;
// update the index of the attribute type that was moved
_validators[lastAccount].index = _validators[validator].index;
// remove (duplicate) validator address at the end by trimming the array
_validatorAccounts.length--;
// remove the validator's signing key from its mapping
delete _signingKeys[_validators[validator].signingKey];
// remove the validator record
delete _validators[validator];
// log the removal of the validator
emit ValidatorRemoved(validator);
}
/**
* @notice Approve the validator at address `validator` to issue attributes of
* the type with ID `attributeTypeID`.
* @param validator address The account of the validator to approve.
* @param attributeTypeID uint256 The ID of the approved attribute type.
*/
function addValidatorApproval(
address validator,
uint256 attributeTypeID
) external onlyOwner whenNotPaused {
// check that the attribute is predefined and that the validator exists
require(
isValidator(validator) && isAttributeType(attributeTypeID),
"must specify both a valid attribute and an available validator"
);
// check that the validator is not already approved
require(
!_attributeTypes[attributeTypeID].approvedValidators[validator],
"validator is already approved on the provided attribute"
);
// set the validator approval status on the attribute
_attributeTypes[attributeTypeID].approvedValidators[validator] = true;
// add the record of the index of the validator approval to be added
uint256 index = _validatorApprovals[validator].length;
_validatorApprovalsIndex[validator][attributeTypeID] = index;
// include the attribute type in the validator approval mapping
_validatorApprovals[validator].push(attributeTypeID);
// log the addition of the validator's attribute type approval
emit ValidatorApprovalAdded(validator, attributeTypeID);
}
/**
* @notice Deny the validator at address `validator` the ability to continue to
* issue attributes of the type with ID `attributeTypeID`.
* @param validator address The account of the validator with removed approval.
* @param attributeTypeID uint256 The ID of the attribute type to unapprove.
* @dev Any attributes of the specified type issued by the validator in
* question will become invalid once the approval is removed. If the approval
* is reinstated, those attributes will become valid again. The approval will
* also be removed if the approved validator is removed.
*/
function removeValidatorApproval(
address validator,
uint256 attributeTypeID
) external onlyOwner whenNotPaused {
// check that the attribute is predefined and that the validator exists
require(
canValidate(validator, attributeTypeID),
"unable to remove validator approval, attribute is already unapproved"
);
// remove the validator approval status from the attribute
delete _attributeTypes[attributeTypeID].approvedValidators[validator];
// locate the index of the last validator approval
uint256 lastIndex = _validatorApprovals[validator].length.sub(1);
// locate the last attribute ID in the validator approval group
uint256 lastAttributeID = _validatorApprovals[validator][lastIndex];
// locate the index of the validator approval to be removed
uint256 index = _validatorApprovalsIndex[validator][attributeTypeID];
// replace the validator approval with the last approval in the array
_validatorApprovals[validator][index] = lastAttributeID;
// drop the last attribute ID from the validator approval group
_validatorApprovals[validator].length--;
// update the record of the index of the swapped-in approval
_validatorApprovalsIndex[validator][lastAttributeID] = index;
// remove the record of the index of the removed approval
delete _validatorApprovalsIndex[validator][attributeTypeID];
// log the removal of the validator's attribute type approval
emit ValidatorApprovalRemoved(validator, attributeTypeID);
}
/**
* @notice Set the public address associated with a validator signing key, used
* to sign off-chain attribute approvals, as `newSigningKey`.
* @param newSigningKey address The address associated with signing key to set.
* @dev Consider having the validator submit a signed proof demonstrating that
* the provided signing key is indeed a signing key in their control - this
* helps mitigate the fringe attack vector where a validator could set the
* address of another validator candidate (especially in the case of a deployed
* smart contract) as their "signing key" in order to block them from being
* added to the jurisdiction (due to the required property of signing keys
* being unique, coupled with the fact that new validators are set up with
* their address as the default initial signing key).
*/
function setValidatorSigningKey(address newSigningKey) external {
require(
isValidator(msg.sender),
"only validators may modify validator signing keys");
// prevent duplicate signing keys from being created
require(
_signingKeys[newSigningKey] == address(0),
"a signing key matching the provided address already exists"
);
// remove validator address as the resolved value for the old key
delete _signingKeys[_validators[msg.sender].signingKey];
// set the signing key to the new value
_validators[msg.sender].signingKey = newSigningKey;
// add validator address as the resolved value for the new key
_signingKeys[newSigningKey] = msg.sender;
// log the modification of the signing key
emit ValidatorSigningKeyModified(msg.sender, newSigningKey);
}
/**
* @notice Issue an attribute of the type with ID `attributeTypeID` and a value
* of `value` to `account` if `message.caller.address()` is approved validator.
* @param account address The account to issue the attribute on.
* @param attributeTypeID uint256 The ID of the attribute type to issue.
* @param value uint256 An optional value for the issued attribute.
* @dev Existing attributes of the given type on the address must be removed
* in order to set a new attribute. Be aware that ownership of the account to
* which the attribute is assigned may still be transferable - restricting
* assignment to externally-owned accounts may partially alleviate this issue.
*/
function issueAttribute(
address account,
uint256 attributeTypeID,
uint256 value
) external payable whenNotPaused {
require(
canValidate(msg.sender, attributeTypeID),
"only approved validators may assign attributes of this type"
);
require(
!_issuedAttributes[account][attributeTypeID].exists,
"duplicate attributes are not supported, remove existing attribute first"
);
// retrieve required minimum stake and jurisdiction fees on attribute type
uint256 minimumStake = _attributeTypes[attributeTypeID].minimumStake;
uint256 jurisdictionFee = _attributeTypes[attributeTypeID].jurisdictionFee;
uint256 stake = msg.value.sub(jurisdictionFee);
require(
stake >= minimumStake,
"attribute requires a greater value than is currently provided"
);
// store attribute value and amount of ether staked in correct scope
_issuedAttributes[account][attributeTypeID] = IssuedAttribute({
exists: true,
setPersonally: false,
operator: address(0),
validator: msg.sender,
value: value,
stake: stake
});
// log the addition of the attribute
emit AttributeAdded(msg.sender, account, attributeTypeID, value);
// log allocation of staked funds to the attribute if applicable
if (stake > 0) {
emit StakeAllocated(msg.sender, attributeTypeID, stake);
}
// pay jurisdiction fee to the owner of the jurisdiction if applicable
if (jurisdictionFee > 0) {
// NOTE: send is chosen over transfer to prevent cases where a improperly
// configured fallback function could block addition of an attribute
if (owner().send(jurisdictionFee)) {
emit FeePaid(owner(), msg.sender, attributeTypeID, jurisdictionFee);
} else {
_recoverableFunds = _recoverableFunds.add(jurisdictionFee);
}
}
}
/**
* @notice Revoke the attribute of the type with ID `attributeTypeID` from
* `account` if `message.caller.address()` is the issuing validator.
* @param account address The account to issue the attribute on.
* @param attributeTypeID uint256 The ID of the attribute type to issue.
* @dev Validators may still revoke issued attributes even after they have been
* removed or had their approval to issue the attribute type removed - this
* enables them to address any objectionable issuances before being reinstated.
*/
function revokeAttribute(
address account,
uint256 attributeTypeID
) external whenNotPaused {
// ensure that an attribute with the given account and attribute exists
require(
_issuedAttributes[account][attributeTypeID].exists,
"only existing attributes may be removed"
);
// determine the assigned validator on the user attribute
address validator = _issuedAttributes[account][attributeTypeID].validator;
// caller must be either the jurisdiction owner or the assigning validator
require(
msg.sender == validator || msg.sender == owner(),
"only jurisdiction or issuing validators may revoke arbitrary attributes"
);
// determine if attribute has any stake in order to refund transaction fee
uint256 stake = _issuedAttributes[account][attributeTypeID].stake;
// determine the correct address to refund the staked amount to
address refundAddress;
if (_issuedAttributes[account][attributeTypeID].setPersonally) {
refundAddress = account;
} else {
address operator = _issuedAttributes[account][attributeTypeID].operator;
if (operator == address(0)) {
refundAddress = validator;
} else {
refundAddress = operator;
}
}
// remove the attribute from the designated user account
delete _issuedAttributes[account][attributeTypeID];
// log the removal of the attribute
emit AttributeRemoved(validator, account, attributeTypeID);
// pay out any refunds and return the excess stake to the user
if (stake > 0 && address(this).balance >= stake) {
// NOTE: send is chosen over transfer to prevent cases where a malicious
// fallback function could forcibly block an attribute's removal. Another
// option is to allow a user to pull the staked amount after the removal.
// NOTE: refine transaction rebate gas calculation! Setting this value too
// high gives validators the incentive to revoke valid attributes. Simply
// checking against gasLeft() & adding the final gas usage won't give the
// correct transaction cost, as freeing space refunds gas upon completion.
uint256 transactionGas = 37700; // <--- WARNING: THIS IS APPROXIMATE
uint256 transactionCost = transactionGas.mul(tx.gasprice);
// if stake exceeds allocated transaction cost, refund user the difference
if (stake > transactionCost) {
// refund the excess stake to the address that contributed the funds
if (refundAddress.send(stake.sub(transactionCost))) {
emit StakeRefunded(
refundAddress,
attributeTypeID,
stake.sub(transactionCost)
);
} else {
_recoverableFunds = _recoverableFunds.add(stake.sub(transactionCost));
}
// emit an event for the payment of the transaction rebate
emit TransactionRebatePaid(
tx.origin,
refundAddress,
attributeTypeID,
transactionCost
);
// refund the cost of the transaction to the trasaction submitter
tx.origin.transfer(transactionCost);
// otherwise, allocate entire stake to partially refunding the transaction
} else {
// emit an event for the payment of the partial transaction rebate
emit TransactionRebatePaid(
tx.origin,
refundAddress,
attributeTypeID,
stake
);
// refund the partial cost of the transaction to trasaction submitter
tx.origin.transfer(stake);
}
}
}
/**
* @notice Add an attribute of the type with ID `attributeTypeID`, an attribute
* value of `value`, and an associated validator fee of `validatorFee` to
* account of `msg.sender` by passing in a signed attribute approval with
* signature `signature`.
* @param attributeTypeID uint256 The ID of the attribute type to add.
* @param value uint256 The value for the attribute to add.
* @param validatorFee uint256 The fee to be paid to the issuing validator.
* @param signature bytes The signature from the validator attribute approval.
*/
function addAttribute(
uint256 attributeTypeID,
uint256 value,
uint256 validatorFee,
bytes signature
) external payable {
// NOTE: determine best course of action when the attribute already exists
// NOTE: consider utilizing bytes32 type for attributes and values
// NOTE: does not currently support an extraData parameter, consider adding
// NOTE: if msg.sender is a proxy contract, its ownership may be transferred
// at will, circumventing any token transfer restrictions. Restricting usage
// to only externally owned accounts may partially alleviate this concern.
// NOTE: cosider including a salt (or better, nonce) parameter so that when
// a user adds an attribute, then it gets revoked, the user can get a new
// signature from the validator and renew the attribute using that. The main
// downside is that everyone will have to keep track of the extra parameter.
// Another solution is to just modifiy the required stake or fee amount.
require(
!_issuedAttributes[msg.sender][attributeTypeID].exists,
"duplicate attributes are not supported, remove existing attribute first"
);
// retrieve required minimum stake and jurisdiction fees on attribute type
uint256 minimumStake = _attributeTypes[attributeTypeID].minimumStake;
uint256 jurisdictionFee = _attributeTypes[attributeTypeID].jurisdictionFee;
uint256 stake = msg.value.sub(validatorFee).sub(jurisdictionFee);
require(
stake >= minimumStake,
"attribute requires a greater value than is currently provided"
);
// signed data hash constructed according to EIP-191-0x45 to prevent replays
bytes32 hash = keccak256(
abi.encodePacked(
address(this),
msg.sender,
address(0),
msg.value,
validatorFee,
attributeTypeID,
value
)
);
require(
!_invalidAttributeApprovalHashes[hash],
"signed attribute approvals from validators may not be reused"
);
// extract the key used to sign the message hash
address signingKey = hash.toEthSignedMessageHash().recover(signature);
// retrieve the validator who controls the extracted key
address validator = _signingKeys[signingKey];
require(
canValidate(validator, attributeTypeID),
"signature does not match an approved validator for given attribute type"
);
// store attribute value and amount of ether staked in correct scope
_issuedAttributes[msg.sender][attributeTypeID] = IssuedAttribute({
exists: true,
setPersonally: true,
operator: address(0),
validator: validator,
value: value,
stake: stake
// NOTE: no extraData included
});
// flag the signed approval as invalid once it's been used to set attribute
_invalidAttributeApprovalHashes[hash] = true;
// log the addition of the attribute
emit AttributeAdded(validator, msg.sender, attributeTypeID, value);
// log allocation of staked funds to the attribute if applicable
if (stake > 0) {
emit StakeAllocated(msg.sender, attributeTypeID, stake);
}
// pay jurisdiction fee to the owner of the jurisdiction if applicable
if (jurisdictionFee > 0) {
// NOTE: send is chosen over transfer to prevent cases where a improperly
// configured fallback function could block addition of an attribute
if (owner().send(jurisdictionFee)) {
emit FeePaid(owner(), msg.sender, attributeTypeID, jurisdictionFee);
} else {
_recoverableFunds = _recoverableFunds.add(jurisdictionFee);
}
}
// pay validator fee to the issuing validator's address if applicable
if (validatorFee > 0) {
// NOTE: send is chosen over transfer to prevent cases where a improperly
// configured fallback function could block addition of an attribute
if (validator.send(validatorFee)) {
emit FeePaid(validator, msg.sender, attributeTypeID, validatorFee);
} else {
_recoverableFunds = _recoverableFunds.add(validatorFee);
}
}
}
/**
* @notice Remove an attribute of the type with ID `attributeTypeID` from
* account of `msg.sender`.
* @param attributeTypeID uint256 The ID of the attribute type to remove.
*/
function removeAttribute(uint256 attributeTypeID) external {
// attributes may only be removed by the user if they are not restricted
require(
!_attributeTypes[attributeTypeID].restricted,
"only jurisdiction or issuing validator may remove a restricted attribute"
);
require(
_issuedAttributes[msg.sender][attributeTypeID].exists,
"only existing attributes may be removed"
);
// determine the assigned validator on the user attribute
address validator = _issuedAttributes[msg.sender][attributeTypeID].validator;
// determine if the attribute has a staked value
uint256 stake = _issuedAttributes[msg.sender][attributeTypeID].stake;
// determine the correct address to refund the staked amount to
address refundAddress;
if (_issuedAttributes[msg.sender][attributeTypeID].setPersonally) {
refundAddress = msg.sender;
} else {
address operator = _issuedAttributes[msg.sender][attributeTypeID].operator;
if (operator == address(0)) {
refundAddress = validator;
} else {
refundAddress = operator;
}
}
// remove the attribute from the user address
delete _issuedAttributes[msg.sender][attributeTypeID];
// log the removal of the attribute
emit AttributeRemoved(validator, msg.sender, attributeTypeID);
// if the attribute has any staked balance, refund it to the user
if (stake > 0 && address(this).balance >= stake) {
// NOTE: send is chosen over transfer to prevent cases where a malicious
// fallback function could forcibly block an attribute's removal
if (refundAddress.send(stake)) {
emit StakeRefunded(refundAddress, attributeTypeID, stake);
} else {
_recoverableFunds = _recoverableFunds.add(stake);
}
}
}
/**
* @notice Add an attribute of the type with ID `attributeTypeID`, an attribute
* value of `value`, and an associated validator fee of `validatorFee` to
* account `account` by passing in a signed attribute approval with signature
* `signature`.
* @param account address The account to add the attribute to.
* @param attributeTypeID uint256 The ID of the attribute type to add.
* @param value uint256 The value for the attribute to add.
* @param validatorFee uint256 The fee to be paid to the issuing validator.
* @param signature bytes The signature from the validator attribute approval.
* @dev Restricted attribute types can only be removed by issuing validators or
* the jurisdiction itself.
*/
function addAttributeFor(
address account,
uint256 attributeTypeID,
uint256 value,
uint256 validatorFee,
bytes signature
) external payable {
// NOTE: determine best course of action when the attribute already exists
// NOTE: consider utilizing bytes32 type for attributes and values
// NOTE: does not currently support an extraData parameter, consider adding
// NOTE: if msg.sender is a proxy contract, its ownership may be transferred
// at will, circumventing any token transfer restrictions. Restricting usage
// to only externally owned accounts may partially alleviate this concern.
// NOTE: consider including a salt (or better, nonce) parameter so that when
// a user adds an attribute, then it gets revoked, the user can get a new
// signature from the validator and renew the attribute using that. The main
// downside is that everyone will have to keep track of the extra parameter.
// Another solution is to just modifiy the required stake or fee amount.
// attributes may only be added by a third party if onlyPersonal is false
require(
!_attributeTypes[attributeTypeID].onlyPersonal,
"only operatable attributes may be added on behalf of another address"
);
require(
!_issuedAttributes[account][attributeTypeID].exists,
"duplicate attributes are not supported, remove existing attribute first"
);
// retrieve required minimum stake and jurisdiction fees on attribute type
uint256 minimumStake = _attributeTypes[attributeTypeID].minimumStake;
uint256 jurisdictionFee = _attributeTypes[attributeTypeID].jurisdictionFee;
uint256 stake = msg.value.sub(validatorFee).sub(jurisdictionFee);
require(
stake >= minimumStake,
"attribute requires a greater value than is currently provided"
);
// signed data hash constructed according to EIP-191-0x45 to prevent replays
bytes32 hash = keccak256(
abi.encodePacked(
address(this),
account,
msg.sender,
msg.value,
validatorFee,
attributeTypeID,
value
)
);
require(
!_invalidAttributeApprovalHashes[hash],
"signed attribute approvals from validators may not be reused"
);
// extract the key used to sign the message hash
address signingKey = hash.toEthSignedMessageHash().recover(signature);
// retrieve the validator who controls the extracted key
address validator = _signingKeys[signingKey];
require(
canValidate(validator, attributeTypeID),
"signature does not match an approved validator for provided attribute"
);
// store attribute value and amount of ether staked in correct scope
_issuedAttributes[account][attributeTypeID] = IssuedAttribute({
exists: true,
setPersonally: false,
operator: msg.sender,
validator: validator,
value: value,
stake: stake
// NOTE: no extraData included
});
// flag the signed approval as invalid once it's been used to set attribute
_invalidAttributeApprovalHashes[hash] = true;
// log the addition of the attribute
emit AttributeAdded(validator, account, attributeTypeID, value);
// log allocation of staked funds to the attribute if applicable
// NOTE: the staker is the entity that pays the fee here!
if (stake > 0) {
emit StakeAllocated(msg.sender, attributeTypeID, stake);
}
// pay jurisdiction fee to the owner of the jurisdiction if applicable
if (jurisdictionFee > 0) {
// NOTE: send is chosen over transfer to prevent cases where a improperly
// configured fallback function could block addition of an attribute
if (owner().send(jurisdictionFee)) {