@lukso/lsp8-contracts 0.16.7 → 0.18.1

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 (88) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +99 -4
  3. package/artifacts/IAccessControlExtended.json +285 -0
  4. package/artifacts/ILSP8CappedBalance.json +27 -0
  5. package/artifacts/ILSP8CappedSupply.json +27 -0
  6. package/artifacts/ILSP8IdentifiableDigitalAsset.json +6 -3
  7. package/artifacts/ILSP8Mintable.json +62 -0
  8. package/artifacts/ILSP8NonTransferable.json +116 -0
  9. package/artifacts/ILSP8Revokable.json +112 -0
  10. package/artifacts/LSP8Burnable.json +7 -4
  11. package/artifacts/LSP8BurnableInitAbstract.json +7 -4
  12. package/artifacts/LSP8CappedBalanceAbstract.json +1285 -0
  13. package/artifacts/LSP8CappedBalanceInitAbstract.json +1293 -0
  14. package/artifacts/{LSP8CappedSupply.json → LSP8CappedSupplyAbstract.json} +8 -15
  15. package/artifacts/LSP8CappedSupplyInitAbstract.json +7 -14
  16. package/artifacts/LSP8CustomizableToken.json +1781 -0
  17. package/artifacts/LSP8CustomizableTokenInit.json +1776 -0
  18. package/artifacts/LSP8Enumerable.json +7 -4
  19. package/artifacts/LSP8EnumerableInitAbstract.json +7 -4
  20. package/artifacts/LSP8IdentifiableDigitalAsset.json +6 -3
  21. package/artifacts/LSP8IdentifiableDigitalAssetInitAbstract.json +6 -3
  22. package/artifacts/LSP8Mintable.json +369 -5
  23. package/artifacts/LSP8MintableAbstract.json +1328 -0
  24. package/artifacts/LSP8MintableInit.json +369 -5
  25. package/artifacts/LSP8MintableInitAbstract.json +1336 -0
  26. package/artifacts/LSP8NonTransferableAbstract.json +1373 -0
  27. package/artifacts/LSP8NonTransferableInitAbstract.json +1381 -0
  28. package/artifacts/LSP8RevokableAbstract.json +1354 -0
  29. package/artifacts/LSP8RevokableInitAbstract.json +1362 -0
  30. package/artifacts/LSP8Votes.json +7 -4
  31. package/artifacts/LSP8VotesInitAbstract.json +7 -4
  32. package/contracts/ILSP8IdentifiableDigitalAsset.sol +1 -1
  33. package/contracts/LSP8Constants.sol +1 -1
  34. package/contracts/LSP8Errors.sol +1 -1
  35. package/contracts/LSP8IdentifiableDigitalAsset.sol +73 -114
  36. package/contracts/LSP8IdentifiableDigitalAssetInitAbstract.sol +69 -116
  37. package/contracts/extensions/AccessControlExtended/AccessControlExtendedAbstract.sol +378 -0
  38. package/contracts/extensions/AccessControlExtended/AccessControlExtendedConstants.sol +13 -0
  39. package/contracts/extensions/AccessControlExtended/AccessControlExtendedErrors.sol +23 -0
  40. package/contracts/extensions/AccessControlExtended/AccessControlExtendedInitAbstract.sol +390 -0
  41. package/contracts/extensions/AccessControlExtended/IAccessControlExtended.sol +51 -0
  42. package/contracts/extensions/{LSP8Burnable.sol → LSP8Burnable/LSP8Burnable.sol} +7 -6
  43. package/contracts/extensions/{LSP8BurnableInitAbstract.sol → LSP8Burnable/LSP8BurnableInitAbstract.sol} +7 -6
  44. package/contracts/extensions/LSP8CappedBalance/ILSP8CappedBalance.sol +11 -0
  45. package/contracts/extensions/LSP8CappedBalance/LSP8CappedBalanceAbstract.sol +124 -0
  46. package/contracts/extensions/LSP8CappedBalance/LSP8CappedBalanceErrors.sol +9 -0
  47. package/contracts/extensions/LSP8CappedBalance/LSP8CappedBalanceInitAbstract.sol +174 -0
  48. package/contracts/extensions/LSP8CappedSupply/ILSP8CappedSupply.sol +11 -0
  49. package/contracts/extensions/LSP8CappedSupply/LSP8CappedSupplyAbstract.sol +59 -0
  50. package/contracts/extensions/LSP8CappedSupply/LSP8CappedSupplyErrors.sol +6 -0
  51. package/contracts/extensions/LSP8CappedSupply/LSP8CappedSupplyInitAbstract.sol +97 -0
  52. package/contracts/extensions/{LSP8Enumerable.sol → LSP8Enumerable/LSP8Enumerable.sol} +2 -2
  53. package/contracts/extensions/{LSP8EnumerableInitAbstract.sol → LSP8Enumerable/LSP8EnumerableInitAbstract.sol} +2 -2
  54. package/contracts/extensions/LSP8Mintable/ILSP8Mintable.sol +27 -0
  55. package/contracts/extensions/LSP8Mintable/LSP8MintableAbstract.sol +105 -0
  56. package/contracts/extensions/LSP8Mintable/LSP8MintableErrors.sol +5 -0
  57. package/contracts/extensions/LSP8Mintable/LSP8MintableInitAbstract.sol +155 -0
  58. package/contracts/extensions/LSP8NonTransferable/ILSP8NonTransferable.sol +54 -0
  59. package/contracts/extensions/LSP8NonTransferable/LSP8NonTransferableAbstract.sol +199 -0
  60. package/contracts/extensions/LSP8NonTransferable/LSP8NonTransferableErrors.sol +14 -0
  61. package/contracts/extensions/LSP8NonTransferable/LSP8NonTransferableInitAbstract.sol +255 -0
  62. package/contracts/extensions/LSP8Revokable/ILSP8Revokable.sol +37 -0
  63. package/contracts/extensions/LSP8Revokable/LSP8RevokableAbstract.sol +140 -0
  64. package/contracts/extensions/LSP8Revokable/LSP8RevokableErrors.sol +4 -0
  65. package/contracts/extensions/LSP8Revokable/LSP8RevokableInitAbstract.sol +192 -0
  66. package/contracts/extensions/{LSP8Votes.sol → LSP8Votes/LSP8Votes.sol} +3 -4
  67. package/contracts/extensions/{LSP8VotesConstants.sol → LSP8Votes/LSP8VotesConstants.sol} +1 -1
  68. package/contracts/extensions/{LSP8VotesInitAbstract.sol → LSP8Votes/LSP8VotesInitAbstract.sol} +3 -3
  69. package/contracts/presets/LSP8CustomizableToken.sol +277 -0
  70. package/contracts/presets/LSP8CustomizableTokenConstants.sol +32 -0
  71. package/contracts/presets/LSP8CustomizableTokenInit.sol +318 -0
  72. package/contracts/presets/LSP8Mintable.sol +13 -28
  73. package/contracts/presets/LSP8MintableInit.sol +13 -6
  74. package/dist/abi.cjs +8351 -136
  75. package/dist/abi.d.cts +12122 -266
  76. package/dist/abi.d.mts +12122 -266
  77. package/dist/abi.d.ts +12122 -266
  78. package/dist/abi.mjs +8335 -136
  79. package/dist/constants.cjs +21 -0
  80. package/dist/constants.d.cts +12 -1
  81. package/dist/constants.d.mts +12 -1
  82. package/dist/constants.d.ts +12 -1
  83. package/dist/constants.mjs +16 -1
  84. package/package.json +32 -9
  85. package/contracts/extensions/LSP8CappedSupply.sol +0 -85
  86. package/contracts/extensions/LSP8CappedSupplyInitAbstract.sol +0 -88
  87. package/contracts/presets/ILSP8Mintable.sol +0 -33
  88. package/contracts/presets/LSP8MintableInitAbstract.sol +0 -62
@@ -0,0 +1,140 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ pragma solidity ^0.8.27;
3
+
4
+ // modules
5
+ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
6
+ import {
7
+ LSP8IdentifiableDigitalAsset
8
+ } from "../../LSP8IdentifiableDigitalAsset.sol";
9
+ import {
10
+ AccessControlExtendedAbstract
11
+ } from "../AccessControlExtended/AccessControlExtendedAbstract.sol";
12
+
13
+ // interfaces
14
+ import {ILSP8Revokable} from "./ILSP8Revokable.sol";
15
+
16
+ // errors
17
+ import {
18
+ AccessControlUnauthorizedAccount
19
+ } from "../AccessControlExtended/AccessControlExtendedErrors.sol";
20
+ import {LSP8RevokableFeatureDisabled} from "./LSP8RevokableErrors.sol";
21
+
22
+ /// @title LSP8RevokableAbstract
23
+ /// @dev Abstract contract implementing revokable functionality for LSP8 tokens.
24
+ /// Allows addresses with the `REVOKER_ROLE` to revoke NFTs from any holder
25
+ /// back to the contract owner or any other address that also has revoke rights.
26
+ ///
27
+ /// Use cases include:
28
+ /// - Memberships: Revoke membership NFTs when they expire or are terminated
29
+ /// - Role badges: Remove role badge NFTs from community members
30
+ /// - Compliance: Freeze or reverse NFTs for regulatory requirements
31
+ /// - Ticketing: Reclaim tickets or access NFTs when conditions are no longer met
32
+ abstract contract LSP8RevokableAbstract is
33
+ ILSP8Revokable,
34
+ LSP8IdentifiableDigitalAsset,
35
+ AccessControlExtendedAbstract
36
+ {
37
+ bool internal _isRevokable;
38
+
39
+ /// @dev keccak256("REVOKER_ROLE")
40
+ bytes32 public constant REVOKER_ROLE =
41
+ 0xce3f34913921da558f105cefb578d87278debbbd073a8d552b5de0d168deee30;
42
+
43
+ constructor(bool isRevokable_) {
44
+ _isRevokable = isRevokable_;
45
+
46
+ if (isRevokable_) {
47
+ _grantRole(REVOKER_ROLE, owner());
48
+ }
49
+ }
50
+
51
+ /// @inheritdoc ILSP8Revokable
52
+ function isRevokable() public view virtual override returns (bool) {
53
+ return _isRevokable;
54
+ }
55
+
56
+ /// @inheritdoc ILSP8Revokable
57
+ /// @custom:warning Once this function is called, any address holding the `REVOKER_ROLE` will be inoperable.
58
+ /// @custom:info The list of addresses holding the `REVOKER_ROLE` remains populated after the revokable feature is switched off.
59
+ function disableRevokable() public virtual override onlyOwner {
60
+ require(isRevokable(), LSP8RevokableFeatureDisabled());
61
+ _isRevokable = false;
62
+ emit RevokableStatusChanged({enabled: false});
63
+ }
64
+
65
+ /// @inheritdoc ILSP8Revokable
66
+ function revoke(
67
+ address from,
68
+ address to,
69
+ bytes32 tokenId,
70
+ bytes memory data
71
+ ) public virtual override onlyRole(REVOKER_ROLE) {
72
+ require(isRevokable(), LSP8RevokableFeatureDisabled());
73
+ require(
74
+ to == owner() || hasRole(REVOKER_ROLE, to),
75
+ AccessControlUnauthorizedAccount(to, REVOKER_ROLE)
76
+ );
77
+
78
+ emit TokenRevoked({
79
+ revoker: msg.sender,
80
+ from: from,
81
+ to: to,
82
+ tokenId: tokenId,
83
+ data: data
84
+ });
85
+
86
+ // We assume revokers are trusted when specifying revocation destinations.
87
+ // Therefore, we bypass LSP1 receiver checks.
88
+ _transfer({
89
+ from: from,
90
+ to: to,
91
+ tokenId: tokenId,
92
+ force: true,
93
+ data: data
94
+ });
95
+ }
96
+
97
+ function supportsInterface(
98
+ bytes4 interfaceId
99
+ )
100
+ public
101
+ view
102
+ virtual
103
+ override(AccessControlExtendedAbstract, LSP8IdentifiableDigitalAsset)
104
+ returns (bool)
105
+ {
106
+ return
107
+ AccessControlExtendedAbstract.supportsInterface(interfaceId) ||
108
+ LSP8IdentifiableDigitalAsset.supportsInterface(interfaceId);
109
+ }
110
+
111
+ /// @dev Overridden function to ensure previous revokers do not persist after contract ownership has been transferred.
112
+ /// The only exception is if the old contract owner had the `REVOKER_ROLE`. This role will be given to the new owner.
113
+ ///
114
+ /// @custom:warning This function clears the entire `REVOKER_ROLE` member set.
115
+ /// - Gas cost scales linearly with the number of addresses with the `REVOKER_ROLE`.
116
+ /// - If the number of addresses with the `REVOKER_ROLE` is large, it might consume a lot of gas,
117
+ /// leading the transaction to approach or exceed the block gas limit and fail.
118
+ /// Consider revoking addresses with the `REVOKER_ROLE` in batches in separate transactions to mitigate this.
119
+ function _transferOwnership(
120
+ address newOwner
121
+ ) internal virtual override(AccessControlExtendedAbstract, Ownable) {
122
+ // restore default admin hierarchy so a previously-installed custom admin
123
+ // cannot grant REVOKER_ROLE to new accounts post-transfer
124
+ _setRoleAdmin(REVOKER_ROLE, DEFAULT_ADMIN_ROLE);
125
+
126
+ // Transfer all roles from old owner to new owner first (including the `REVOKER_ROLE`)
127
+ // before clearing the list of revokers.
128
+ super._transferOwnership(newOwner);
129
+
130
+ address[] memory revokers = getRoleMembers(REVOKER_ROLE);
131
+
132
+ for (uint256 ii = 0; ii < revokers.length; ++ii) {
133
+ // Exclude the new owner from the list of revokers to delete.
134
+ address revoker = revokers[ii];
135
+ if (revoker == newOwner) continue;
136
+
137
+ _revokeRole(REVOKER_ROLE, revoker);
138
+ }
139
+ }
140
+ }
@@ -0,0 +1,4 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ pragma solidity ^0.8.27;
3
+
4
+ error LSP8RevokableFeatureDisabled();
@@ -0,0 +1,192 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ pragma solidity ^0.8.27;
3
+
4
+ // modules
5
+ import {
6
+ OwnableUpgradeable
7
+ } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
8
+ import {
9
+ LSP8IdentifiableDigitalAssetInitAbstract
10
+ } from "../../LSP8IdentifiableDigitalAssetInitAbstract.sol";
11
+ import {
12
+ AccessControlExtendedInitAbstract
13
+ } from "../AccessControlExtended/AccessControlExtendedInitAbstract.sol";
14
+
15
+ // interfaces
16
+ import {ILSP8Revokable} from "./ILSP8Revokable.sol";
17
+
18
+ // errors
19
+ import {
20
+ AccessControlUnauthorizedAccount
21
+ } from "../AccessControlExtended/AccessControlExtendedErrors.sol";
22
+ import {LSP8RevokableFeatureDisabled} from "./LSP8RevokableErrors.sol";
23
+
24
+ /// @title LSP8RevokableInitAbstract
25
+ /// @dev Abstract contract implementing revokable functionality for LSP8 tokens (initializer version).
26
+ /// Allows addresses with the `REVOKER_ROLE` to revoke NFTs from any holder
27
+ /// back to the contract owner or any other address that also has revoke rights.
28
+ ///
29
+ /// This version is for proxy deployments using the initializer pattern.
30
+ ///
31
+ /// Use cases include:
32
+ /// - Memberships: Revoke membership NFTs when they expire or are terminated
33
+ /// - Role badges: Remove role badge NFTs from community members
34
+ /// - Compliance: Freeze or reverse NFTs for regulatory requirements
35
+ /// - Ticketing: Reclaim tickets or access NFTs when conditions are no longer met
36
+ abstract contract LSP8RevokableInitAbstract is
37
+ ILSP8Revokable,
38
+ LSP8IdentifiableDigitalAssetInitAbstract,
39
+ AccessControlExtendedInitAbstract
40
+ {
41
+ bool internal _isRevokable;
42
+
43
+ /// @dev keccak256("REVOKER_ROLE")
44
+ bytes32 public constant REVOKER_ROLE =
45
+ 0xce3f34913921da558f105cefb578d87278debbbd073a8d552b5de0d168deee30;
46
+
47
+ /// @notice Initializes the LSP8Revokable contract with base token params.
48
+ /// @dev Initializes the LSP8IdentifiableDigitalAsset base contract.
49
+ /// @param name_ The name of the token.
50
+ /// @param symbol_ The symbol of the token.
51
+ /// @param newOwner_ The owner of the contract (implicitly a revoker).
52
+ /// @param lsp4TokenType_ The token type (see LSP4).
53
+ /// @param lsp8TokenIdFormat_ The format of tokenIds (= NFTs) that this contract will create.
54
+ /// @param isRevokable_ Whether token revocation is enabled.
55
+ function __LSP8Revokable_init(
56
+ string memory name_,
57
+ string memory symbol_,
58
+ address newOwner_,
59
+ uint256 lsp4TokenType_,
60
+ uint256 lsp8TokenIdFormat_,
61
+ bool isRevokable_
62
+ ) internal virtual onlyInitializing {
63
+ LSP8IdentifiableDigitalAssetInitAbstract._initialize(
64
+ name_,
65
+ symbol_,
66
+ newOwner_,
67
+ lsp4TokenType_,
68
+ lsp8TokenIdFormat_
69
+ );
70
+ __AccessControlExtended_init();
71
+ __LSP8Revokable_init_unchained(isRevokable_);
72
+ }
73
+
74
+ /// @notice Unchained initializer for LSP8Revokable.
75
+ function __LSP8Revokable_init_unchained(
76
+ bool isRevokable_
77
+ ) internal virtual onlyInitializing {
78
+ _isRevokable = isRevokable_;
79
+
80
+ if (isRevokable_) {
81
+ _grantRole(REVOKER_ROLE, owner());
82
+ }
83
+ }
84
+
85
+ /// @inheritdoc ILSP8Revokable
86
+ function isRevokable() public view virtual override returns (bool) {
87
+ return _isRevokable;
88
+ }
89
+
90
+ /// @inheritdoc ILSP8Revokable
91
+ /// @custom:warning Once this function is called, any address holding the `REVOKER_ROLE` will be inoperable.
92
+ /// @custom:info The list of addresses holding the `REVOKER_ROLE` remains populated after the revokable feature is switched off.
93
+ function disableRevokable() public virtual override onlyOwner {
94
+ require(isRevokable(), LSP8RevokableFeatureDisabled());
95
+ _isRevokable = false;
96
+ emit RevokableStatusChanged({enabled: false});
97
+ }
98
+
99
+ /// @inheritdoc ILSP8Revokable
100
+ function revoke(
101
+ address from,
102
+ address to,
103
+ bytes32 tokenId,
104
+ bytes memory data
105
+ ) public virtual override onlyRole(REVOKER_ROLE) {
106
+ require(isRevokable(), LSP8RevokableFeatureDisabled());
107
+ require(
108
+ to == owner() || hasRole(REVOKER_ROLE, to),
109
+ AccessControlUnauthorizedAccount(to, REVOKER_ROLE)
110
+ );
111
+
112
+ emit TokenRevoked({
113
+ revoker: msg.sender,
114
+ from: from,
115
+ to: to,
116
+ tokenId: tokenId,
117
+ data: data
118
+ });
119
+
120
+ // We assume revokers are trusted when specifying revocation destinations.
121
+ // Therefore, we bypass LSP1 receiver checks.
122
+ _transfer({
123
+ from: from,
124
+ to: to,
125
+ tokenId: tokenId,
126
+ force: true,
127
+ data: data
128
+ });
129
+ }
130
+
131
+ function supportsInterface(
132
+ bytes4 interfaceId
133
+ )
134
+ public
135
+ view
136
+ virtual
137
+ override(
138
+ AccessControlExtendedInitAbstract,
139
+ LSP8IdentifiableDigitalAssetInitAbstract
140
+ )
141
+ returns (bool)
142
+ {
143
+ return
144
+ AccessControlExtendedInitAbstract.supportsInterface(interfaceId) ||
145
+ LSP8IdentifiableDigitalAssetInitAbstract.supportsInterface(
146
+ interfaceId
147
+ );
148
+ }
149
+
150
+ /// @dev Overridden function to ensure previous revokers do not persist after contract ownership has been transferred.
151
+ /// The only exception is if the old contract owner had the `REVOKER_ROLE`. This role will be given to the new owner.
152
+ ///
153
+ /// @custom:warning This function clears the entire `REVOKER_ROLE` member set.
154
+ /// - Gas cost scales linearly with the number of addresses with the `REVOKER_ROLE`.
155
+ /// - If the number of addresses with the `REVOKER_ROLE` is large, it might consume a lot of gas,
156
+ /// leading the transaction to approach or exceed the block gas limit and fail.
157
+ /// Consider revoking addresses with the `REVOKER_ROLE` in batches in separate transactions to mitigate this.
158
+ function _transferOwnership(
159
+ address newOwner
160
+ )
161
+ internal
162
+ virtual
163
+ override(AccessControlExtendedInitAbstract, OwnableUpgradeable)
164
+ {
165
+ // restore default admin hierarchy so a previously-installed custom admin
166
+ // cannot grant REVOKER_ROLE to new accounts post-transfer
167
+ _setRoleAdmin(REVOKER_ROLE, DEFAULT_ADMIN_ROLE);
168
+
169
+ // Transfer all roles from old owner to new owner first (including the `REVOKER_ROLE`)
170
+ // before clearing the list of revokers.
171
+ super._transferOwnership(newOwner);
172
+
173
+ address[] memory revokers = getRoleMembers(REVOKER_ROLE);
174
+
175
+ // Exclude the new owner from the list of revokers to delete.
176
+ for (uint256 ii = 0; ii < revokers.length; ++ii) {
177
+ address revoker = revokers[ii];
178
+ if (revoker == newOwner) continue;
179
+ _revokeRole(REVOKER_ROLE, revoker);
180
+ }
181
+ }
182
+
183
+ /**
184
+ * @dev This empty reserved space is put in place to allow future versions to add new
185
+ * variables without shifting down storage in the inheritance chain.
186
+ * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
187
+ *
188
+ * @custom:info The size of the `__gap` array is calculated so that the amount of storage used by the contract
189
+ * always adds up to the same number (in this case 50 storage slots).
190
+ */
191
+ uint256[49] private __gap;
192
+ }
@@ -1,10 +1,9 @@
1
- // SPDX-License-Identifier: MIT
2
-
3
- pragma solidity ^0.8.0;
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ pragma solidity ^0.8.27;
4
3
 
5
4
  import {
6
5
  LSP8IdentifiableDigitalAsset
7
- } from "../LSP8IdentifiableDigitalAsset.sol";
6
+ } from "../../LSP8IdentifiableDigitalAsset.sol";
8
7
  import {Votes} from "@openzeppelin/contracts/governance/utils/Votes.sol";
9
8
  import {
10
9
  _TYPEID_LSP8_VOTESDELEGATOR,
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- pragma solidity ^0.8.4;
2
+ pragma solidity ^0.8.27;
3
3
 
4
4
  // keccak256('LSP8Tokens_VotesDelegatorNotification')
5
5
  bytes32 constant _TYPEID_LSP8_VOTESDELEGATOR = 0x2f6d3f668c2e57dbae4c255f2d9e0b69d47a8848d69a2251cce137529e34743e;
@@ -1,9 +1,9 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.0;
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ pragma solidity ^0.8.27;
3
3
 
4
4
  import {
5
5
  LSP8IdentifiableDigitalAssetInitAbstract
6
- } from "../LSP8IdentifiableDigitalAssetInitAbstract.sol";
6
+ } from "../../LSP8IdentifiableDigitalAssetInitAbstract.sol";
7
7
  import {
8
8
  VotesUpgradeable
9
9
  } from "@openzeppelin/contracts-upgradeable/governance/utils/VotesUpgradeable.sol";
@@ -0,0 +1,277 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ pragma solidity ^0.8.27;
3
+
4
+ // modules
5
+ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
6
+ import {
7
+ AccessControlExtendedAbstract
8
+ } from "../extensions/AccessControlExtended/AccessControlExtendedAbstract.sol";
9
+ import {
10
+ LSP8IdentifiableDigitalAsset
11
+ } from "../LSP8IdentifiableDigitalAsset.sol";
12
+
13
+ // extensions
14
+ import {LSP8Burnable} from "../extensions/LSP8Burnable/LSP8Burnable.sol";
15
+ import {
16
+ LSP8MintableAbstract
17
+ } from "../extensions/LSP8Mintable/LSP8MintableAbstract.sol";
18
+ import {
19
+ LSP8CappedSupplyAbstract
20
+ } from "../extensions/LSP8CappedSupply/LSP8CappedSupplyAbstract.sol";
21
+ import {
22
+ LSP8CappedBalanceAbstract
23
+ } from "../extensions/LSP8CappedBalance/LSP8CappedBalanceAbstract.sol";
24
+ import {
25
+ LSP8NonTransferableAbstract
26
+ } from "../extensions/LSP8NonTransferable/LSP8NonTransferableAbstract.sol";
27
+ import {
28
+ LSP8RevokableAbstract
29
+ } from "../extensions/LSP8Revokable/LSP8RevokableAbstract.sol";
30
+
31
+ // constants
32
+ import {
33
+ LSP8MintableParams,
34
+ LSP8NonTransferableParams,
35
+ LSP8CappedParams,
36
+ LSP8RevokableParams
37
+ } from "./LSP8CustomizableTokenConstants.sol";
38
+
39
+ // errors
40
+ import {
41
+ LSP8MintDisabled
42
+ } from "../extensions/LSP8Mintable/LSP8MintableErrors.sol";
43
+ import {
44
+ LSP8CappedSupplyCannotMintOverCap
45
+ } from "../extensions/LSP8CappedSupply/LSP8CappedSupplyErrors.sol";
46
+
47
+ /// @title LSP8CustomizableToken
48
+ /// @dev A customizable LSP8 token that implements multiple features and uses role-based exemptions.
49
+ /// Implements {LSP8Burnable} to allow burning.
50
+ /// Implements {LSP8Mintable} to allow minting.
51
+ /// Implements {LSP8CappedSupply} to set total supply cap.
52
+ /// Implements {LSP8CappedBalance} to set balance caps.
53
+ /// Implements {LSP8NonTransferable} to restrict transfers.
54
+ /// Implements {LSP8Revokable} to allow revoking tokens.
55
+ contract LSP8CustomizableToken is
56
+ LSP8Burnable,
57
+ LSP8MintableAbstract,
58
+ LSP8CappedSupplyAbstract,
59
+ LSP8CappedBalanceAbstract,
60
+ LSP8NonTransferableAbstract,
61
+ LSP8RevokableAbstract
62
+ {
63
+ /// @notice Initializes the token with name, symbol, owner, and customizable features.
64
+ /// @dev Sets up minting, balance cap, transfer restrictions and supply cap. Mints initial tokens if specified. Reverts if initialMintTokenIds length exceeds tokenSupplyCap. Inherits constructor logic from parent contracts.
65
+ /// @param name_ The name of the token.
66
+ /// @param symbol_ The symbol of the token.
67
+ /// @param newOwner_ The initial owner of the token.
68
+ /// @param lsp4TokenType_ The LSP4 token type (e.g., 1 for NFT, 2 for Collection).
69
+ /// @param lsp8TokenIdFormat_ The format of tokenIds (= NFTs) that this contract will create.
70
+ /// @param mintableParams Deployment configuration for minting feature (see above).
71
+ /// @param cappedParams Deployment configuration for capped balance and capped supply features (see above).
72
+ /// @param nonTransferableParams Deployment configuration for non-transferable feature (see above).
73
+ /// @param revokableParams Deployment configuration for revokable feature (see above).
74
+ constructor(
75
+ string memory name_,
76
+ string memory symbol_,
77
+ address newOwner_,
78
+ uint256 lsp4TokenType_,
79
+ uint256 lsp8TokenIdFormat_,
80
+ LSP8MintableParams memory mintableParams,
81
+ LSP8CappedParams memory cappedParams,
82
+ LSP8NonTransferableParams memory nonTransferableParams,
83
+ LSP8RevokableParams memory revokableParams
84
+ )
85
+ LSP8IdentifiableDigitalAsset(
86
+ name_,
87
+ symbol_,
88
+ newOwner_,
89
+ lsp4TokenType_,
90
+ lsp8TokenIdFormat_
91
+ )
92
+ AccessControlExtendedAbstract()
93
+ LSP8MintableAbstract(mintableParams.isMintable)
94
+ LSP8CappedSupplyAbstract(cappedParams.tokenSupplyCap)
95
+ LSP8CappedBalanceAbstract(cappedParams.tokenBalanceCap)
96
+ LSP8NonTransferableAbstract(
97
+ nonTransferableParams.transferLockStart,
98
+ nonTransferableParams.transferLockEnd
99
+ )
100
+ LSP8RevokableAbstract(revokableParams.isRevokable)
101
+ {
102
+ _initialMint({
103
+ to: newOwner_,
104
+ initialMintTokenIds: mintableParams.initialMintTokenIds
105
+ });
106
+ }
107
+
108
+ /// @inheritdoc LSP8CappedSupplyAbstract
109
+ /// @notice Returns the token supply cap.
110
+ /// @dev If minting is enabled, returns the configured supply cap defining the maximum number of NFTs that can be minted.
111
+ /// If minting is disabled, returns the current total supply as the effective cap (no more NFTs can be created).
112
+ function tokenSupplyCap() public view virtual override returns (uint256) {
113
+ return isMintable ? super.tokenSupplyCap() : totalSupply();
114
+ }
115
+
116
+ /// @dev Required override to resolve multiple inheritance. Calls every parent {supportsInterface} functions
117
+ /// via `super` to aggregate the interface IDs supported across all inherited modules.
118
+ function supportsInterface(
119
+ bytes4 interfaceId
120
+ )
121
+ public
122
+ view
123
+ virtual
124
+ override(
125
+ LSP8IdentifiableDigitalAsset,
126
+ LSP8CappedBalanceAbstract,
127
+ LSP8MintableAbstract,
128
+ LSP8NonTransferableAbstract,
129
+ LSP8RevokableAbstract
130
+ )
131
+ returns (bool)
132
+ {
133
+ return super.supportsInterface(interfaceId);
134
+ }
135
+
136
+ /// @dev Override to bypass the non transferable check when revokers revoke users' tokens.
137
+ function _nonTransferableCheck(
138
+ address from,
139
+ address to,
140
+ bytes32 tokenId,
141
+ bool force,
142
+ bytes memory data
143
+ ) internal virtual override {
144
+ if (_isRevocationBypass(to)) return;
145
+ super._nonTransferableCheck(from, to, tokenId, force, data);
146
+ }
147
+
148
+ /// @dev Override to bypass the token balance cap check when revokers revoke users' tokens.
149
+ function _tokenBalanceCapCheck(
150
+ address from,
151
+ address to,
152
+ bytes32 tokenId,
153
+ bool force,
154
+ bytes memory data
155
+ ) internal virtual override {
156
+ if (_isRevocationBypass(to)) return;
157
+ super._tokenBalanceCapCheck(from, to, tokenId, force, data);
158
+ }
159
+
160
+ /// @inheritdoc LSP8MintableAbstract
161
+ /// @dev Overridden function to allow minting only if:
162
+ /// - the minting feature is enabled, from {LSP8MintableAbstract}
163
+ /// - the total number of NFTs does not exceed the capped supply after minting, from {LSP8CappedSupplyAbstract}
164
+ function _mint(
165
+ address to,
166
+ bytes32 tokenId,
167
+ bool force,
168
+ bytes memory data
169
+ )
170
+ internal
171
+ virtual
172
+ override(
173
+ LSP8IdentifiableDigitalAsset,
174
+ LSP8MintableAbstract,
175
+ LSP8CappedSupplyAbstract
176
+ )
177
+ {
178
+ require(isMintable, LSP8MintDisabled());
179
+ LSP8CappedSupplyAbstract._mint(to, tokenId, force, data);
180
+ }
181
+
182
+ /// @notice Hook called before a token transfer to enforce restrictions.
183
+ /// @dev Combines checks from {LSP8CappedBalance} and {LSP8NonTransferable}.
184
+ /// - Bypasses {LSP8NonTransferable} checks for senders (`from`) holding the `NON_TRANSFERABLE_BYPASS_ROLE` role.
185
+ /// - Bypasses {LSP8CappedBalance} checks for recipients (`to`) holding the `UNCAPPED_BALANCE_ROLE` role.
186
+ /// - Allows minting (from address(0)) and burning to address(0) regardless of restrictions.
187
+ /// @param from The address sending the token.
188
+ /// @param to The address receiving the token.
189
+ /// @param tokenId The unique identifier of the token being transferred.
190
+ /// @param force Whether to force the transfer.
191
+ /// @param data Additional data for the transfer.
192
+ function _beforeTokenTransfer(
193
+ address from,
194
+ address to,
195
+ bytes32 tokenId,
196
+ bool force,
197
+ bytes memory data
198
+ )
199
+ internal
200
+ virtual
201
+ override(
202
+ LSP8IdentifiableDigitalAsset,
203
+ LSP8CappedBalanceAbstract,
204
+ LSP8NonTransferableAbstract
205
+ )
206
+ {
207
+ super._beforeTokenTransfer(from, to, tokenId, force, data);
208
+ }
209
+
210
+ /// @dev Required override to resolve multiple inheritance. Calls every parent {_transferOwnership} function
211
+ /// via `super` so that each inherited module updates its ownership-dependent state.
212
+ ///
213
+ /// When contract ownership changes, this will:
214
+ /// - clear the admin role for: `MINTER_ROLE`, `REVOKER_ROLE`, `NON_TRANSFERABLE_BYPASS_ROLE`, `UNCAPPED_BALANCE_ROLE`
215
+ /// - clear the list of addresses holding the `REVOKER_ROLE`
216
+ function _transferOwnership(
217
+ address newOwner
218
+ )
219
+ internal
220
+ virtual
221
+ override(
222
+ Ownable,
223
+ LSP8CappedBalanceAbstract,
224
+ LSP8MintableAbstract,
225
+ LSP8NonTransferableAbstract,
226
+ LSP8RevokableAbstract
227
+ )
228
+ {
229
+ super._transferOwnership(newOwner);
230
+ }
231
+
232
+ /// @dev Mint initial NFTs without enforcing check if the token contract is mintable or not.
233
+ /// Enforces the configured capped-supply value directly (not {tokenSupplyCap} when minting is disabled).
234
+ function _initialMint(
235
+ address to,
236
+ bytes32[] memory initialMintTokenIds
237
+ ) private {
238
+ uint256 configuredTokenSupplyCap = LSP8CappedSupplyAbstract
239
+ .tokenSupplyCap();
240
+ bool isCappedSupplyConfigured = configuredTokenSupplyCap > 0;
241
+
242
+ if (isCappedSupplyConfigured) {
243
+ bool mintingExceedsSupplyCap = initialMintTokenIds.length >
244
+ configuredTokenSupplyCap;
245
+
246
+ require(
247
+ !mintingExceedsSupplyCap,
248
+ LSP8CappedSupplyCannotMintOverCap()
249
+ );
250
+ }
251
+
252
+ for (uint256 ii = 0; ii < initialMintTokenIds.length; ++ii) {
253
+ LSP8IdentifiableDigitalAsset._mint(
254
+ to,
255
+ initialMintTokenIds[ii],
256
+ true,
257
+ ""
258
+ );
259
+ }
260
+ }
261
+
262
+ /// @dev Returns whether the current call is a legitimate revocation that should bypass the
263
+ /// {LSP8NonTransferable} and {LSP8CappedBalance} restrictions when revoking tokens from a token holder.
264
+ ///
265
+ /// Returns `true` only when all of the following conditions are met:
266
+ /// - the function being called is {revoke}.
267
+ /// - the revoking feature is enabled.
268
+ /// - the caller (`msg.sender`) holds the `REVOKER_ROLE`.
269
+ /// - the recipient (`to`) is either the contract `owner()` or a revoker (an address holding the `REVOKER_ROLE`).
270
+ function _isRevocationBypass(address to) private view returns (bool) {
271
+ return
272
+ msg.sig == this.revoke.selector &&
273
+ isRevokable() &&
274
+ hasRole(REVOKER_ROLE, msg.sender) &&
275
+ (to == owner() || hasRole(REVOKER_ROLE, to));
276
+ }
277
+ }
@@ -0,0 +1,32 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ pragma solidity ^0.8.27;
3
+
4
+ /// @dev Deployment configuration for minting feature.
5
+ /// @param isMintable True to enable minting after deployment, false to disable it forever.
6
+ /// @param initialMintTokenIds Array of tokenIds to mint to `newOwner_` on deployment.
7
+ struct LSP8MintableParams {
8
+ bool isMintable;
9
+ bytes32[] initialMintTokenIds;
10
+ }
11
+
12
+ /// @dev Deployment configuration for capped balance and capped supply features.
13
+ /// @param tokenBalanceCap The maximum number of NFTs per address, 0 to disable.
14
+ /// @param tokenSupplyCap The maximum total supply of NFTs, 0 to disable.
15
+ struct LSP8CappedParams {
16
+ uint256 tokenBalanceCap;
17
+ uint256 tokenSupplyCap;
18
+ }
19
+
20
+ /// @dev Deployment configuration for non-transferable feature.
21
+ /// @param transferLockStart The start timestamp of the transfer lock period, 0 to disable.
22
+ /// @param transferLockEnd The end timestamp of the transfer lock period, 0 to disable.
23
+ struct LSP8NonTransferableParams {
24
+ uint256 transferLockStart;
25
+ uint256 transferLockEnd;
26
+ }
27
+
28
+ /// @dev Deployment configuration for revokable feature.
29
+ /// @param isRevokable True to enable token revocation after deployment, false to disable it.
30
+ struct LSP8RevokableParams {
31
+ bool isRevokable;
32
+ }