@lukso/lsp8-contracts 0.15.0 → 0.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +2 -2
  2. package/artifacts/ILSP8IdentifiableDigitalAsset.json +0 -112
  3. package/artifacts/LSP8Burnable.json +0 -11
  4. package/artifacts/LSP8BurnableInitAbstract.json +0 -16
  5. package/artifacts/LSP8CappedSupply.json +0 -11
  6. package/artifacts/LSP8CappedSupplyInitAbstract.json +0 -16
  7. package/artifacts/LSP8Enumerable.json +0 -11
  8. package/artifacts/LSP8EnumerableInitAbstract.json +0 -16
  9. package/artifacts/LSP8IdentifiableDigitalAsset.json +0 -11
  10. package/artifacts/LSP8IdentifiableDigitalAssetInitAbstract.json +0 -16
  11. package/artifacts/LSP8Mintable.json +2 -13
  12. package/artifacts/LSP8MintableInit.json +2 -13
  13. package/artifacts/LSP8Votes.json +1230 -0
  14. package/artifacts/LSP8VotesInitAbstract.json +1222 -0
  15. package/contracts/ILSP8IdentifiableDigitalAsset.sol +2 -8
  16. package/contracts/LSP8Constants.sol +4 -0
  17. package/contracts/LSP8IdentifiableDigitalAsset.sol +796 -36
  18. package/contracts/LSP8IdentifiableDigitalAssetInitAbstract.sol +806 -36
  19. package/contracts/extensions/LSP8CappedSupply.sol +1 -1
  20. package/contracts/extensions/LSP8CappedSupplyInitAbstract.sol +1 -1
  21. package/contracts/extensions/LSP8Enumerable.sol +6 -5
  22. package/contracts/extensions/LSP8EnumerableInitAbstract.sol +5 -5
  23. package/contracts/extensions/LSP8Votes.sol +94 -0
  24. package/contracts/extensions/LSP8VotesConstants.sol +8 -0
  25. package/contracts/extensions/LSP8VotesInitAbstract.sol +116 -0
  26. package/dist/index.cjs +5 -1
  27. package/dist/index.d.cts +18 -16
  28. package/dist/index.d.mts +18 -16
  29. package/dist/index.d.ts +18 -16
  30. package/dist/index.mjs +5 -1
  31. package/package.json +5 -6
  32. package/types/index.ts +3958 -1854
  33. package/contracts/LSP8IdentifiableDigitalAssetCore.sol +0 -809
  34. package/types/common.ts +0 -131
  35. package/types/contracts/ILSP8IdentifiableDigitalAsset.ts +0 -706
  36. package/types/contracts/LSP8IdentifiableDigitalAsset.ts +0 -778
  37. package/types/contracts/LSP8IdentifiableDigitalAssetInitAbstract.ts +0 -813
  38. package/types/contracts/extensions/LSP8Burnable.ts +0 -797
  39. package/types/contracts/extensions/LSP8BurnableInitAbstract.ts +0 -829
  40. package/types/contracts/extensions/LSP8CappedSupply.ts +0 -792
  41. package/types/contracts/extensions/LSP8CappedSupplyInitAbstract.ts +0 -824
  42. package/types/contracts/extensions/LSP8Enumerable.ts +0 -790
  43. package/types/contracts/extensions/LSP8EnumerableInitAbstract.ts +0 -821
  44. package/types/contracts/presets/LSP8Mintable.ts +0 -797
  45. package/types/contracts/presets/LSP8MintableInit.ts +0 -860
@@ -2,43 +2,75 @@
2
2
  pragma solidity ^0.8.12;
3
3
 
4
4
  // interfaces
5
- import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
6
-
7
- // modules
8
- import {ERC725YCore} from "@erc725/smart-contracts/contracts/ERC725YCore.sol";
9
5
  import {
10
- LSP8IdentifiableDigitalAssetCore
11
- } from "./LSP8IdentifiableDigitalAssetCore.sol";
6
+ ILSP1UniversalReceiver as ILSP1
7
+ } from "@lukso/lsp1-contracts/contracts/ILSP1UniversalReceiver.sol";
12
8
  import {
13
- LSP4DigitalAssetMetadataInitAbstract
14
- } from "@lukso/lsp4-contracts/contracts/LSP4DigitalAssetMetadataInitAbstract.sol";
9
+ ILSP8IdentifiableDigitalAsset
10
+ } from "./ILSP8IdentifiableDigitalAsset.sol";
15
11
 
12
+ // modules
16
13
  import {
17
- LSP4DigitalAssetMetadataCore
18
- } from "@lukso/lsp4-contracts/contracts/LSP4DigitalAssetMetadataCore.sol";
14
+ LSP4DigitalAssetMetadataInitAbstract,
15
+ ERC725YInitAbstract
16
+ } from "@lukso/lsp4-contracts/contracts/LSP4DigitalAssetMetadataInitAbstract.sol";
19
17
 
20
18
  import {
21
- LSP17Extendable
22
- } from "@lukso/lsp17contractextension-contracts/contracts/LSP17Extendable.sol";
19
+ LSP17ExtendableInitAbstract
20
+ } from "@lukso/lsp17contractextension-contracts/contracts/LSP17ExtendableInitAbstract.sol";
23
21
 
24
22
  // libraries
23
+ import {
24
+ EnumerableSet
25
+ } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
26
+ import {
27
+ ERC165Checker
28
+ } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
29
+ import {LSP1Utils} from "@lukso/lsp1-contracts/contracts/LSP1Utils.sol";
25
30
  import {LSP2Utils} from "@lukso/lsp2-contracts/contracts/LSP2Utils.sol";
26
31
 
27
32
  // constants
28
- import {_INTERFACEID_LSP8, _LSP8_TOKENID_FORMAT_KEY} from "./LSP8Constants.sol";
29
-
30
- // errors
31
33
  import {
32
- LSP8TokenContractCannotHoldValue,
33
- LSP8TokenIdFormatNotEditable
34
- } from "./LSP8Errors.sol";
35
-
34
+ _INTERFACEID_LSP1
35
+ } from "@lukso/lsp1-contracts/contracts/LSP1Constants.sol";
36
36
  import {
37
37
  _LSP17_EXTENSION_PREFIX
38
38
  } from "@lukso/lsp17contractextension-contracts/contracts/LSP17Constants.sol";
39
+ import {
40
+ _INTERFACEID_LSP8,
41
+ _LSP8_TOKENID_FORMAT_KEY,
42
+ _TYPEID_LSP8_TOKENOPERATOR,
43
+ _TYPEID_LSP8_TOKENSSENDER,
44
+ _TYPEID_LSP8_TOKENSRECIPIENT
45
+ } from "./LSP8Constants.sol";
39
46
 
40
47
  // errors
41
-
48
+ import {
49
+ NoExtensionFoundForFunctionSelector,
50
+ InvalidFunctionSelector,
51
+ InvalidExtensionAddress
52
+ } from "@lukso/lsp17contractextension-contracts/contracts/LSP17Errors.sol";
53
+ import {
54
+ LSP8TokenContractCannotHoldValue,
55
+ LSP8TokenIdFormatNotEditable,
56
+ LSP8NonExistentTokenId,
57
+ LSP8NotTokenOwner,
58
+ LSP8CannotUseAddressZeroAsOperator,
59
+ LSP8TokenOwnerCannotBeOperator,
60
+ LSP8OperatorAlreadyAuthorized,
61
+ LSP8NotTokenOperator,
62
+ LSP8InvalidTransferBatch,
63
+ LSP8NonExistingOperator,
64
+ LSP8CannotSendToAddressZero,
65
+ LSP8TokenIdAlreadyMinted,
66
+ LSP8NotifyTokenReceiverContractMissingLSP1Interface,
67
+ LSP8NotifyTokenReceiverIsEOA,
68
+ LSP8TokenIdsDataLengthMismatch,
69
+ LSP8TokenIdsDataEmptyArray,
70
+ LSP8BatchCallFailed,
71
+ LSP8TokenOwnerChanged,
72
+ LSP8RevokeOperatorNotAuthorized
73
+ } from "./LSP8Errors.sol";
42
74
  import {
43
75
  NoExtensionFoundForFunctionSelector,
44
76
  InvalidFunctionSelector,
@@ -57,10 +89,26 @@ import {
57
89
  * For a generic mechanism, see {LSP7Mintable}.
58
90
  */
59
91
  abstract contract LSP8IdentifiableDigitalAssetInitAbstract is
92
+ ILSP8IdentifiableDigitalAsset,
60
93
  LSP4DigitalAssetMetadataInitAbstract,
61
- LSP8IdentifiableDigitalAssetCore,
62
- LSP17Extendable
94
+ LSP17ExtendableInitAbstract
63
95
  {
96
+ using EnumerableSet for EnumerableSet.AddressSet;
97
+ using EnumerableSet for EnumerableSet.Bytes32Set;
98
+
99
+ // --- Storage
100
+
101
+ uint256 internal _existingTokens;
102
+
103
+ // Mapping from `tokenId` to `tokenOwner`
104
+ mapping(bytes32 => address) internal _tokenOwners;
105
+
106
+ // Mapping `tokenOwner` to owned tokenIds
107
+ mapping(address => EnumerableSet.Bytes32Set) internal _ownedTokens;
108
+
109
+ // Mapping a `tokenId` to its authorized operator addresses.
110
+ mapping(bytes32 => EnumerableSet.AddressSet) internal _operators;
111
+
64
112
  /**
65
113
  * @dev Initialize a `LSP8IdentifiableDigitalAsset` contract and set the tokenId format inside the ERC725Y storage of the contract.
66
114
  * This will also set the token `name_` and `symbol_` under the ERC725Y data keys `LSP4TokenName` and `LSP4TokenSymbol`.
@@ -94,7 +142,7 @@ abstract contract LSP8IdentifiableDigitalAssetInitAbstract is
94
142
  );
95
143
  }
96
144
 
97
- // fallback function
145
+ // fallback functions
98
146
 
99
147
  /**
100
148
  * @notice The `fallback` function was called with the following amount of native tokens: `msg.value`; and the following calldata: `callData`.
@@ -129,6 +177,7 @@ abstract contract LSP8IdentifiableDigitalAssetInitAbstract is
129
177
  * @dev Reverts whenever someone tries to send native tokens to a LSP8 contract.
130
178
  * @notice LSP8 contract cannot receive native tokens.
131
179
  */
180
+ // solhint-disable-next-line no-complex-fallback
132
181
  receive() external payable virtual {
133
182
  // revert on empty calls with no value
134
183
  if (msg.value == 0) {
@@ -197,9 +246,7 @@ abstract contract LSP8IdentifiableDigitalAssetInitAbstract is
197
246
  );
198
247
 
199
248
  // Check if there is an extension stored under the generated data key
200
- bytes memory extensionAddress = ERC725YCore._getData(
201
- mappedExtensionDataKey
202
- );
249
+ bytes memory extensionAddress = _getData(mappedExtensionDataKey);
203
250
  if (extensionAddress.length != 20 && extensionAddress.length != 0)
204
251
  revert InvalidExtensionAddress(extensionAddress);
205
252
 
@@ -215,13 +262,15 @@ abstract contract LSP8IdentifiableDigitalAssetInitAbstract is
215
262
  public
216
263
  view
217
264
  virtual
218
- override(IERC165, ERC725YCore, LSP17Extendable)
265
+ override(ERC725YInitAbstract, LSP17ExtendableInitAbstract)
219
266
  returns (bool)
220
267
  {
221
268
  return
222
269
  interfaceId == _INTERFACEID_LSP8 ||
223
270
  super.supportsInterface(interfaceId) ||
224
- LSP17Extendable._supportsInterfaceInERC165Extension(interfaceId);
271
+ LSP17ExtendableInitAbstract._supportsInterfaceInERC165Extension(
272
+ interfaceId
273
+ );
225
274
  }
226
275
 
227
276
  /**
@@ -232,17 +281,738 @@ abstract contract LSP8IdentifiableDigitalAssetInitAbstract is
232
281
  function _setData(
233
282
  bytes32 dataKey,
234
283
  bytes memory dataValue
235
- )
236
- internal
237
- virtual
238
- override(
239
- LSP4DigitalAssetMetadataInitAbstract,
240
- LSP4DigitalAssetMetadataCore
241
- )
242
- {
284
+ ) internal virtual override {
243
285
  if (dataKey == _LSP8_TOKENID_FORMAT_KEY) {
244
286
  revert LSP8TokenIdFormatNotEditable();
245
287
  }
246
288
  LSP4DigitalAssetMetadataInitAbstract._setData(dataKey, dataValue);
247
289
  }
290
+
291
+ // --- Token queries
292
+
293
+ /**
294
+ * @inheritdoc ILSP8IdentifiableDigitalAsset
295
+ */
296
+ function totalSupply() public view virtual override returns (uint256) {
297
+ return _existingTokens;
298
+ }
299
+
300
+ // --- Token owner queries
301
+
302
+ /**
303
+ * @inheritdoc ILSP8IdentifiableDigitalAsset
304
+ */
305
+ function balanceOf(
306
+ address tokenOwner
307
+ ) public view virtual override returns (uint256) {
308
+ return _ownedTokens[tokenOwner].length();
309
+ }
310
+
311
+ /**
312
+ * @inheritdoc ILSP8IdentifiableDigitalAsset
313
+ */
314
+ function tokenOwnerOf(
315
+ bytes32 tokenId
316
+ ) public view virtual override returns (address) {
317
+ address tokenOwner = _tokenOwners[tokenId];
318
+
319
+ if (tokenOwner == address(0)) {
320
+ revert LSP8NonExistentTokenId(tokenId);
321
+ }
322
+
323
+ return tokenOwner;
324
+ }
325
+
326
+ /**
327
+ * @inheritdoc ILSP8IdentifiableDigitalAsset
328
+ */
329
+ function tokenIdsOf(
330
+ address tokenOwner
331
+ ) public view virtual override returns (bytes32[] memory) {
332
+ return _ownedTokens[tokenOwner].values();
333
+ }
334
+
335
+ // --- TokenId Metadata functionality
336
+
337
+ /**
338
+ * @inheritdoc ILSP8IdentifiableDigitalAsset
339
+ */
340
+ function getDataForTokenId(
341
+ bytes32 tokenId,
342
+ bytes32 dataKey
343
+ ) public view virtual override returns (bytes memory dataValue) {
344
+ return _getDataForTokenId(tokenId, dataKey);
345
+ }
346
+
347
+ /**
348
+ * @inheritdoc ILSP8IdentifiableDigitalAsset
349
+ */
350
+ function getDataBatchForTokenIds(
351
+ bytes32[] memory tokenIds,
352
+ bytes32[] memory dataKeys
353
+ ) public view virtual override returns (bytes[] memory dataValues) {
354
+ if (tokenIds.length != dataKeys.length) {
355
+ revert LSP8TokenIdsDataLengthMismatch();
356
+ }
357
+
358
+ dataValues = new bytes[](tokenIds.length);
359
+
360
+ for (uint256 i; i < tokenIds.length; ) {
361
+ dataValues[i] = _getDataForTokenId(tokenIds[i], dataKeys[i]);
362
+
363
+ // Increment the iterator in unchecked block to save gas
364
+ unchecked {
365
+ ++i;
366
+ }
367
+ }
368
+
369
+ return dataValues;
370
+ }
371
+
372
+ /**
373
+ * @inheritdoc ILSP8IdentifiableDigitalAsset
374
+ */
375
+ function setDataForTokenId(
376
+ bytes32 tokenId,
377
+ bytes32 dataKey,
378
+ bytes memory dataValue
379
+ ) public virtual override onlyOwner {
380
+ _setDataForTokenId(tokenId, dataKey, dataValue);
381
+ }
382
+
383
+ /**
384
+ * @inheritdoc ILSP8IdentifiableDigitalAsset
385
+ */
386
+ function setDataBatchForTokenIds(
387
+ bytes32[] memory tokenIds,
388
+ bytes32[] memory dataKeys,
389
+ bytes[] memory dataValues
390
+ ) public virtual override onlyOwner {
391
+ if (
392
+ tokenIds.length != dataKeys.length ||
393
+ dataKeys.length != dataValues.length
394
+ ) {
395
+ revert LSP8TokenIdsDataLengthMismatch();
396
+ }
397
+
398
+ if (tokenIds.length == 0) {
399
+ revert LSP8TokenIdsDataEmptyArray();
400
+ }
401
+
402
+ for (uint256 i; i < tokenIds.length; ) {
403
+ _setDataForTokenId(tokenIds[i], dataKeys[i], dataValues[i]);
404
+
405
+ // Increment the iterator in unchecked block to save gas
406
+ unchecked {
407
+ ++i;
408
+ }
409
+ }
410
+ }
411
+
412
+ // --- General functionality
413
+
414
+ /**
415
+ * @inheritdoc ILSP8IdentifiableDigitalAsset
416
+ *
417
+ * @custom:info It's not possible to send value along the functions call due to the use of `delegatecall`.
418
+ */
419
+ function batchCalls(
420
+ bytes[] calldata data
421
+ ) public virtual override returns (bytes[] memory results) {
422
+ results = new bytes[](data.length);
423
+ for (uint256 i; i < data.length; ) {
424
+ (bool success, bytes memory result) = address(this).delegatecall(
425
+ data[i]
426
+ );
427
+
428
+ if (!success) {
429
+ // Look for revert reason and bubble it up if present
430
+ if (result.length != 0) {
431
+ // The easiest way to bubble the revert reason is using memory via assembly
432
+ // solhint-disable no-inline-assembly
433
+ /// @solidity memory-safe-assembly
434
+ assembly {
435
+ let returndata_size := mload(result)
436
+ revert(add(32, result), returndata_size)
437
+ }
438
+ } else {
439
+ revert LSP8BatchCallFailed({callIndex: i});
440
+ }
441
+ }
442
+
443
+ results[i] = result;
444
+
445
+ unchecked {
446
+ ++i;
447
+ }
448
+ }
449
+ }
450
+
451
+ // --- Operator functionality
452
+
453
+ /**
454
+ * @inheritdoc ILSP8IdentifiableDigitalAsset
455
+ */
456
+ function authorizeOperator(
457
+ address operator,
458
+ bytes32 tokenId,
459
+ bytes memory operatorNotificationData
460
+ ) public virtual override {
461
+ address tokenOwner = tokenOwnerOf(tokenId);
462
+
463
+ if (tokenOwner != msg.sender) {
464
+ revert LSP8NotTokenOwner(tokenOwner, tokenId, msg.sender);
465
+ }
466
+
467
+ if (operator == address(0)) {
468
+ revert LSP8CannotUseAddressZeroAsOperator();
469
+ }
470
+
471
+ if (tokenOwner == operator) {
472
+ revert LSP8TokenOwnerCannotBeOperator();
473
+ }
474
+
475
+ bool isAdded = _operators[tokenId].add(operator);
476
+ if (!isAdded) revert LSP8OperatorAlreadyAuthorized(operator, tokenId);
477
+
478
+ emit OperatorAuthorizationChanged(
479
+ operator,
480
+ tokenOwner,
481
+ tokenId,
482
+ operatorNotificationData
483
+ );
484
+
485
+ bytes memory lsp1Data = abi.encode(
486
+ msg.sender,
487
+ tokenId,
488
+ true, // authorized
489
+ operatorNotificationData
490
+ );
491
+
492
+ _notifyTokenOperator(operator, lsp1Data);
493
+ }
494
+
495
+ /**
496
+ * @inheritdoc ILSP8IdentifiableDigitalAsset
497
+ */
498
+ function revokeOperator(
499
+ address operator,
500
+ bytes32 tokenId,
501
+ bool notify,
502
+ bytes memory operatorNotificationData
503
+ ) public virtual override {
504
+ address tokenOwner = tokenOwnerOf(tokenId);
505
+
506
+ if (msg.sender != tokenOwner) {
507
+ if (operator != msg.sender) {
508
+ revert LSP8RevokeOperatorNotAuthorized(
509
+ msg.sender,
510
+ tokenOwner,
511
+ tokenId
512
+ );
513
+ }
514
+ }
515
+
516
+ if (operator == address(0)) {
517
+ revert LSP8CannotUseAddressZeroAsOperator();
518
+ }
519
+
520
+ if (tokenOwner == operator) {
521
+ revert LSP8TokenOwnerCannotBeOperator();
522
+ }
523
+
524
+ _revokeOperator(
525
+ operator,
526
+ tokenOwner,
527
+ tokenId,
528
+ notify,
529
+ operatorNotificationData
530
+ );
531
+
532
+ if (notify) {
533
+ bytes memory lsp1Data = abi.encode(
534
+ tokenOwner,
535
+ tokenId,
536
+ false, // unauthorized
537
+ operatorNotificationData
538
+ );
539
+
540
+ _notifyTokenOperator(operator, lsp1Data);
541
+ }
542
+ }
543
+
544
+ /**
545
+ * @inheritdoc ILSP8IdentifiableDigitalAsset
546
+ */
547
+ function isOperatorFor(
548
+ address operator,
549
+ bytes32 tokenId
550
+ ) public view virtual override returns (bool) {
551
+ return _isOperatorOrOwner(operator, tokenId);
552
+ }
553
+
554
+ /**
555
+ * @inheritdoc ILSP8IdentifiableDigitalAsset
556
+ */
557
+ function getOperatorsOf(
558
+ bytes32 tokenId
559
+ ) public view virtual override returns (address[] memory) {
560
+ _existsOrError(tokenId);
561
+
562
+ return _operators[tokenId].values();
563
+ }
564
+
565
+ /**
566
+ * @dev verifies if the `caller` is operator or owner for the `tokenId`
567
+ * @return true if `caller` is either operator or owner
568
+ */
569
+ function _isOperatorOrOwner(
570
+ address caller,
571
+ bytes32 tokenId
572
+ ) internal view virtual returns (bool) {
573
+ return (caller == tokenOwnerOf(tokenId) ||
574
+ _operators[tokenId].contains(caller));
575
+ }
576
+
577
+ // --- Transfer functionality
578
+
579
+ /**
580
+ * @inheritdoc ILSP8IdentifiableDigitalAsset
581
+ */
582
+ function transfer(
583
+ address from,
584
+ address to,
585
+ bytes32 tokenId,
586
+ bool force,
587
+ bytes memory data
588
+ ) public virtual override {
589
+ if (!_isOperatorOrOwner(msg.sender, tokenId)) {
590
+ revert LSP8NotTokenOperator(tokenId, msg.sender);
591
+ }
592
+
593
+ _transfer(from, to, tokenId, force, data);
594
+ }
595
+
596
+ /**
597
+ * @inheritdoc ILSP8IdentifiableDigitalAsset
598
+ */
599
+ function transferBatch(
600
+ address[] memory from,
601
+ address[] memory to,
602
+ bytes32[] memory tokenId,
603
+ bool[] memory force,
604
+ bytes[] memory data
605
+ ) public virtual override {
606
+ uint256 fromLength = from.length;
607
+ if (
608
+ fromLength != to.length ||
609
+ fromLength != tokenId.length ||
610
+ fromLength != force.length ||
611
+ fromLength != data.length
612
+ ) {
613
+ revert LSP8InvalidTransferBatch();
614
+ }
615
+
616
+ for (uint256 i; i < fromLength; ) {
617
+ transfer(from[i], to[i], tokenId[i], force[i], data[i]);
618
+
619
+ unchecked {
620
+ ++i;
621
+ }
622
+ }
623
+ }
624
+
625
+ /**
626
+ * @dev removes `operator` from the list of operators for the `tokenId`
627
+ */
628
+ function _revokeOperator(
629
+ address operator,
630
+ address tokenOwner,
631
+ bytes32 tokenId,
632
+ bool notified,
633
+ bytes memory operatorNotificationData
634
+ ) internal virtual {
635
+ bool isRemoved = _operators[tokenId].remove(operator);
636
+ if (!isRemoved) revert LSP8NonExistingOperator(operator, tokenId);
637
+
638
+ emit OperatorRevoked(
639
+ operator,
640
+ tokenOwner,
641
+ tokenId,
642
+ notified,
643
+ operatorNotificationData
644
+ );
645
+ }
646
+
647
+ /**
648
+ * @dev revoke all the current operators for a specific `tokenId` token which belongs to `tokenOwner`.
649
+ *
650
+ * @param tokenOwner The address that is the owner of the `tokenId`.
651
+ * @param tokenId The token to remove the associated operators for.
652
+ */
653
+ function _clearOperators(
654
+ address tokenOwner,
655
+ bytes32 tokenId
656
+ ) internal virtual {
657
+ // here is a good example of why having multiple operators will be expensive.. we
658
+ // need to clear them on token transfer
659
+ //
660
+ // NOTE: this may cause a tx to fail if there is too many operators to clear, in which case
661
+ // the tokenOwner needs to call `revokeOperator` until there is less operators to clear and
662
+ // the desired `transfer` or `burn` call can succeed.
663
+ EnumerableSet.AddressSet storage operatorsForTokenId = _operators[
664
+ tokenId
665
+ ];
666
+
667
+ uint256 operatorListLength = operatorsForTokenId.length();
668
+ address operator;
669
+ for (uint256 i; i < operatorListLength; ) {
670
+ // we are emptying the list, always remove from index 0
671
+ operator = operatorsForTokenId.at(0);
672
+ _revokeOperator(operator, tokenOwner, tokenId, false, "");
673
+
674
+ unchecked {
675
+ ++i;
676
+ }
677
+ }
678
+ }
679
+
680
+ /**
681
+ * @dev Returns whether `tokenId` exists.
682
+ *
683
+ * Tokens start existing when they are minted ({_mint}), and stop existing when they are burned ({_burn}).
684
+ */
685
+ function _exists(bytes32 tokenId) internal view virtual returns (bool) {
686
+ return _tokenOwners[tokenId] != address(0);
687
+ }
688
+
689
+ /**
690
+ * @dev When `tokenId` does not exist then revert with an error.
691
+ */
692
+ function _existsOrError(bytes32 tokenId) internal view virtual {
693
+ if (!_exists(tokenId)) {
694
+ revert LSP8NonExistentTokenId(tokenId);
695
+ }
696
+ }
697
+
698
+ /**
699
+ * @dev Create `tokenId` by minting it and transfers it to `to`.
700
+ *
701
+ * @custom:info Any logic in the:
702
+ * - {_beforeTokenTransfer} function will run before updating the balances and ownership of `tokenId`s.
703
+ * - {_afterTokenTransfer} function will run after updating the balances and ownership of `tokenId`s, **but before notifying the recipient via LSP1**.
704
+ *
705
+ * @param to The address that will receive the minted `tokenId`.
706
+ * @param tokenId The token ID to create (= mint).
707
+ * @param force When set to `true`, `to` may be any address. When set to `false`, `to` must be a contract that supports the LSP1 standard.
708
+ * @param data Any additional data the caller wants included in the emitted event, and sent in the hook of the `to` address.
709
+ *
710
+ * @custom:requirements
711
+ * - `tokenId` must not exist and not have been already minted.
712
+ * - `to` cannot be the zero address.
713
+
714
+ * @custom:events {Transfer} event with `address(0)` as `from` address.
715
+ */
716
+ function _mint(
717
+ address to,
718
+ bytes32 tokenId,
719
+ bool force,
720
+ bytes memory data
721
+ ) internal virtual {
722
+ if (to == address(0)) {
723
+ revert LSP8CannotSendToAddressZero();
724
+ }
725
+
726
+ // Check that `tokenId` is not already minted
727
+ if (_exists(tokenId)) {
728
+ revert LSP8TokenIdAlreadyMinted(tokenId);
729
+ }
730
+
731
+ _beforeTokenTransfer(address(0), to, tokenId, force, data);
732
+
733
+ // Check that `tokenId` was not minted inside the `_beforeTokenTransfer` hook
734
+ if (_exists(tokenId)) {
735
+ revert LSP8TokenIdAlreadyMinted(tokenId);
736
+ }
737
+
738
+ // token being minted
739
+ ++_existingTokens;
740
+
741
+ _ownedTokens[to].add(tokenId);
742
+ _tokenOwners[tokenId] = to;
743
+
744
+ emit Transfer(msg.sender, address(0), to, tokenId, force, data);
745
+
746
+ _afterTokenTransfer(address(0), to, tokenId, force, data);
747
+
748
+ bytes memory lsp1Data = abi.encode(
749
+ msg.sender,
750
+ address(0),
751
+ to,
752
+ tokenId,
753
+ data
754
+ );
755
+ _notifyTokenReceiver(to, force, lsp1Data);
756
+ }
757
+
758
+ /**
759
+ * @dev Burn a specific `tokenId`, removing the `tokenId` from the {tokenIdsOf} the caller and decreasing its {balanceOf} by -1.
760
+ * This will also clear all the operators allowed to transfer the `tokenId`.
761
+ *
762
+ * The owner of the `tokenId` will be notified about the `tokenId` being transferred through its LSP1 {universalReceiver}
763
+ * function, if it is a contract that supports the LSP1 interface. Its {universalReceiver} function will receive
764
+ * all the parameters in the calldata packed encoded.
765
+ *
766
+ * @custom:info Any logic in the:
767
+ * - {_beforeTokenTransfer} function will run before updating the balances and ownership of `tokenId`s.
768
+ * - {_afterTokenTransfer} function will run after updating the balances and ownership of `tokenId`s, **but before notifying the sender via LSP1**.
769
+ *
770
+ * @param tokenId The token to burn.
771
+ * @param data Any additional data the caller wants included in the emitted event, and sent in the LSP1 hook on the token owner's address.
772
+ *
773
+ * @custom:hint In dApps, you can know which addresses are burning tokens by listening for the `Transfer` event and filter with the zero address as `to`.
774
+ *
775
+ * @custom:requirements
776
+ * - `tokenId` must exist.
777
+ *
778
+ * @custom:events {Transfer} event with `address(0)` as the `to` address.
779
+ */
780
+ function _burn(bytes32 tokenId, bytes memory data) internal virtual {
781
+ address tokenOwner = tokenOwnerOf(tokenId);
782
+
783
+ _beforeTokenTransfer(tokenOwner, address(0), tokenId, false, data);
784
+
785
+ // Re-fetch and update `tokenOwner` in case `tokenId`
786
+ // was transferred inside the `_beforeTokenTransfer` hook
787
+ tokenOwner = tokenOwnerOf(tokenId);
788
+
789
+ // token being burned
790
+ --_existingTokens;
791
+
792
+ _clearOperators(tokenOwner, tokenId);
793
+
794
+ _ownedTokens[tokenOwner].remove(tokenId);
795
+ delete _tokenOwners[tokenId];
796
+
797
+ emit Transfer(msg.sender, tokenOwner, address(0), tokenId, false, data);
798
+
799
+ _afterTokenTransfer(tokenOwner, address(0), tokenId, false, data);
800
+
801
+ bytes memory lsp1Data = abi.encode(
802
+ msg.sender,
803
+ tokenOwner,
804
+ address(0),
805
+ tokenId,
806
+ data
807
+ );
808
+
809
+ _notifyTokenSender(tokenOwner, lsp1Data);
810
+ }
811
+
812
+ /**
813
+ * @dev Change the owner of the `tokenId` from `from` to `to`.
814
+ *
815
+ * Both the sender and recipient will be notified of the `tokenId` being transferred through their LSP1 {universalReceiver}
816
+ * function, if they are contracts that support the LSP1 interface. Their `universalReceiver` function will receive
817
+ * all the parameters in the calldata packed encoded.
818
+ *
819
+ * @custom:info Any logic in the:
820
+ * - {_beforeTokenTransfer} function will run before updating the balances and ownership of `tokenId`s.
821
+ * - {_afterTokenTransfer} function will run after updating the balances and ownership of `tokenId`s, **but before notifying the sender/recipient via LSP1**.
822
+ *
823
+ * @param from The sender address.
824
+ * @param to The recipient address.
825
+ * @param tokenId The token to transfer.
826
+ * @param force When set to `true`, `to` may be any address. When set to `false`, `to` must be a contract that supports the LSP1 standard.
827
+ * @param data Additional data the caller wants included in the emitted event, and sent in the hooks to `from` and `to` addresses.
828
+ *
829
+ * @custom:requirements
830
+ * - `to` cannot be the zero address.
831
+ * - `tokenId` token must be owned by `from`.
832
+ *
833
+ * @custom:events {Transfer} event.
834
+ *
835
+ * @custom:danger This internal function does not check if the sender is authorized or not to operate on the `tokenId`.
836
+ */
837
+ function _transfer(
838
+ address from,
839
+ address to,
840
+ bytes32 tokenId,
841
+ bool force,
842
+ bytes memory data
843
+ ) internal virtual {
844
+ address tokenOwner = tokenOwnerOf(tokenId);
845
+ if (tokenOwner != from) {
846
+ revert LSP8NotTokenOwner(tokenOwner, tokenId, from);
847
+ }
848
+
849
+ if (to == address(0)) {
850
+ revert LSP8CannotSendToAddressZero();
851
+ }
852
+
853
+ _beforeTokenTransfer(from, to, tokenId, force, data);
854
+
855
+ // Check that `tokenId`'s owner was not changed inside the `_beforeTokenTransfer` hook
856
+ address currentTokenOwner = tokenOwnerOf(tokenId);
857
+ if (tokenOwner != currentTokenOwner) {
858
+ revert LSP8TokenOwnerChanged(
859
+ tokenId,
860
+ tokenOwner,
861
+ currentTokenOwner
862
+ );
863
+ }
864
+
865
+ _clearOperators(from, tokenId);
866
+
867
+ _ownedTokens[from].remove(tokenId);
868
+ _ownedTokens[to].add(tokenId);
869
+ _tokenOwners[tokenId] = to;
870
+
871
+ emit Transfer(msg.sender, from, to, tokenId, force, data);
872
+
873
+ _afterTokenTransfer(from, to, tokenId, force, data);
874
+
875
+ bytes memory lsp1Data = abi.encode(msg.sender, from, to, tokenId, data);
876
+
877
+ _notifyTokenSender(from, lsp1Data);
878
+ _notifyTokenReceiver(to, force, lsp1Data);
879
+ }
880
+
881
+ /**
882
+ * @dev Sets data for a specific `tokenId` and `dataKey` in the ERC725Y storage
883
+ * The ERC725Y data key is the hash of the `tokenId` and `dataKey` concatenated
884
+ * @param tokenId The unique identifier for a token.
885
+ * @param dataKey The key for the data to set.
886
+ * @param dataValue The value to set for the given data key.
887
+ * @custom:events {TokenIdDataChanged} event.
888
+ */
889
+ function _setDataForTokenId(
890
+ bytes32 tokenId,
891
+ bytes32 dataKey,
892
+ bytes memory dataValue
893
+ ) internal virtual {
894
+ _store[keccak256(bytes.concat(tokenId, dataKey))] = dataValue;
895
+ emit TokenIdDataChanged(tokenId, dataKey, dataValue);
896
+ }
897
+
898
+ /**
899
+ * @dev Retrieves data for a specific `tokenId` and `dataKey` from the ERC725Y storage
900
+ * The ERC725Y data key is the hash of the `tokenId` and `dataKey` concatenated
901
+ * @param tokenId The unique identifier for a token.
902
+ * @param dataKey The key for the data to retrieve.
903
+ * @return dataValues The data value associated with the given `tokenId` and `dataKey`.
904
+ */
905
+ function _getDataForTokenId(
906
+ bytes32 tokenId,
907
+ bytes32 dataKey
908
+ ) internal view virtual returns (bytes memory dataValues) {
909
+ return _store[keccak256(bytes.concat(tokenId, dataKey))];
910
+ }
911
+
912
+ /**
913
+ * @dev Hook that is called before any token transfer, including minting and burning.
914
+ * Allows to run custom logic before updating balances and notifying sender/recipient by overriding this function.
915
+ *
916
+ * @param from The sender address
917
+ * @param to The recipient address
918
+ * @param tokenId The tokenId to transfer
919
+ * @param force A boolean that describe if transfer to a `to` address that does not support LSP1 is allowed or not.
920
+ * @param data The data sent alongside the transfer
921
+ */
922
+ function _beforeTokenTransfer(
923
+ address from,
924
+ address to,
925
+ bytes32 tokenId,
926
+ bool force,
927
+ bytes memory data // solhint-disable-next-line no-empty-blocks
928
+ ) internal virtual {}
929
+
930
+ /**
931
+ * @dev Hook that is called after any token transfer, including minting and burning.
932
+ * Allows to run custom logic after updating balances, but **before notifying sender/recipient via LSP1** by overriding this function.
933
+ *
934
+ * @param from The sender address
935
+ * @param to The recipient address
936
+ * @param tokenId The tokenId to transfer
937
+ * @param force A boolean that describe if transfer to a `to` address that does not support LSP1 is allowed or not.
938
+ * @param data The data sent alongside the transfer
939
+ */
940
+ function _afterTokenTransfer(
941
+ address from,
942
+ address to,
943
+ bytes32 tokenId,
944
+ bool force,
945
+ bytes memory data // solhint-disable-next-line no-empty-blocks
946
+ ) internal virtual {}
947
+
948
+ /**
949
+ * @dev Attempt to notify the operator `operator` about the `tokenId` being authorized.
950
+ * This is done by calling its {universalReceiver} function with the `_TYPEID_LSP8_TOKENOPERATOR` as typeId, if `operator` is a contract that supports the LSP1 interface.
951
+ * If `operator` is an EOA or a contract that does not support the LSP1 interface, nothing will happen and no notification will be sent.
952
+
953
+ * @param operator The address to call the {universalReceiver} function on.
954
+ * @param lsp1Data the data to be sent to the `operator` address in the `universalReceiver` call.
955
+ */
956
+ function _notifyTokenOperator(
957
+ address operator,
958
+ bytes memory lsp1Data
959
+ ) internal virtual {
960
+ LSP1Utils.notifyUniversalReceiver(
961
+ operator,
962
+ _TYPEID_LSP8_TOKENOPERATOR,
963
+ lsp1Data
964
+ );
965
+ }
966
+
967
+ /**
968
+ * @dev Attempt to notify the token sender `from` about the `tokenId` being transferred.
969
+ * This is done by calling its {universalReceiver} function with the `_TYPEID_LSP8_TOKENSSENDER` as typeId, if `from` is a contract that supports the LSP1 interface.
970
+ * If `from` is an EOA or a contract that does not support the LSP1 interface, nothing will happen and no notification will be sent.
971
+
972
+ * @param from The address to call the {universalReceiver} function on.
973
+ * @param lsp1Data the data to be sent to the `from` address in the `universalReceiver` call.
974
+ */
975
+ function _notifyTokenSender(
976
+ address from,
977
+ bytes memory lsp1Data
978
+ ) internal virtual {
979
+ LSP1Utils.notifyUniversalReceiver(
980
+ from,
981
+ _TYPEID_LSP8_TOKENSSENDER,
982
+ lsp1Data
983
+ );
984
+ }
985
+
986
+ /**
987
+ * @dev Attempt to notify the token receiver `to` about the `tokenId` being received.
988
+ * This is done by calling its {universalReceiver} function with the `_TYPEID_LSP8_TOKENSRECIPIENT` as typeId, if `to` is a contract that supports the LSP1 interface.
989
+ *
990
+ * If `to` is is an EOA or a contract that does not support the LSP1 interface, the behaviour will depend on the `force` boolean flag.
991
+ * - if `force` is set to `true`, nothing will happen and no notification will be sent.
992
+ * - if `force` is set to `false, the transaction will revert.
993
+ *
994
+ * @param to The address to call the {universalReceiver} function on.
995
+ * @param force A boolean that describe if transfer to a `to` address that does not support LSP1 is allowed or not.
996
+ * @param lsp1Data The data to be sent to the `to` address in the `universalReceiver(...)` call.
997
+ */
998
+ function _notifyTokenReceiver(
999
+ address to,
1000
+ bool force,
1001
+ bytes memory lsp1Data
1002
+ ) internal virtual {
1003
+ if (
1004
+ ERC165Checker.supportsERC165InterfaceUnchecked(
1005
+ to,
1006
+ _INTERFACEID_LSP1
1007
+ )
1008
+ ) {
1009
+ ILSP1(to).universalReceiver(_TYPEID_LSP8_TOKENSRECIPIENT, lsp1Data);
1010
+ } else if (!force) {
1011
+ if (to.code.length != 0) {
1012
+ revert LSP8NotifyTokenReceiverContractMissingLSP1Interface(to);
1013
+ } else {
1014
+ revert LSP8NotifyTokenReceiverIsEOA(to);
1015
+ }
1016
+ }
1017
+ }
248
1018
  }