@lukso/lsp8-contracts 0.16.7 → 0.17.3

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 +54 -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 +110 -0
  9. package/artifacts/ILSP8Revokable.json +75 -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 +1738 -0
  17. package/artifacts/LSP8CustomizableTokenInit.json +1733 -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 +1367 -0
  27. package/artifacts/LSP8NonTransferableInitAbstract.json +1375 -0
  28. package/artifacts/LSP8RevokableAbstract.json +1317 -0
  29. package/artifacts/LSP8RevokableInitAbstract.json +1325 -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 +48 -0
  59. package/contracts/extensions/LSP8NonTransferable/LSP8NonTransferableAbstract.sol +190 -0
  60. package/contracts/extensions/LSP8NonTransferable/LSP8NonTransferableErrors.sol +14 -0
  61. package/contracts/extensions/LSP8NonTransferable/LSP8NonTransferableInitAbstract.sol +246 -0
  62. package/contracts/extensions/LSP8Revokable/ILSP8Revokable.sol +28 -0
  63. package/contracts/extensions/LSP8Revokable/LSP8RevokableAbstract.sol +132 -0
  64. package/contracts/extensions/LSP8Revokable/LSP8RevokableErrors.sol +4 -0
  65. package/contracts/extensions/LSP8Revokable/LSP8RevokableInitAbstract.sol +178 -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 +8233 -158
  75. package/dist/abi.d.cts +12004 -323
  76. package/dist/abi.d.mts +12004 -323
  77. package/dist/abi.d.ts +12004 -323
  78. package/dist/abi.mjs +8217 -158
  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,390 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ pragma solidity ^0.8.27;
3
+
4
+ // interfaces
5
+ import {
6
+ IAccessControl
7
+ } from "@openzeppelin/contracts/access/IAccessControl.sol";
8
+ import {
9
+ IAccessControlEnumerable
10
+ } from "@openzeppelin/contracts/access/IAccessControlEnumerable.sol";
11
+ import {IAccessControlExtended} from "./IAccessControlExtended.sol";
12
+
13
+ // modules
14
+ import {
15
+ OwnableUpgradeable
16
+ } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
17
+
18
+ // libraries
19
+ import {
20
+ EnumerableSet
21
+ } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
22
+
23
+ // constants
24
+ import {
25
+ _INTERFACEID_ACCESSCONTROL,
26
+ _INTERFACEID_ACCESSCONTROLENUMERABLE,
27
+ _INTERFACEID_ACCESSCONTROLEXTENDED
28
+ } from "./AccessControlExtendedConstants.sol";
29
+
30
+ // errors
31
+ import {
32
+ AccessControlUnauthorizedAccount,
33
+ AccessControlBadConfirmation,
34
+ AccessControlCannotSetAdminForDefaultAdminRole
35
+ } from "./AccessControlExtendedErrors.sol";
36
+
37
+ /**
38
+ * @title AccessControlExtendedInitAbstract
39
+ * @dev Proxy/initializable variant of {AccessControlExtendedAbstract}. Uses
40
+ * `__AccessControlExtended_init` / `__AccessControlExtended_init_unchained`
41
+ * instead of a constructor, both guarded by `onlyInitializing`.
42
+ */
43
+ abstract contract AccessControlExtendedInitAbstract is
44
+ IAccessControlExtended,
45
+ OwnableUpgradeable
46
+ {
47
+ using EnumerableSet for EnumerableSet.AddressSet;
48
+ using EnumerableSet for EnumerableSet.Bytes32Set;
49
+
50
+ // --- Constants
51
+
52
+ /// @dev The default admin role. Value is `bytes32(0)` per OZ convention.
53
+ bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;
54
+
55
+ // --- Storage
56
+
57
+ /// @dev Mapping from role to its admin role.
58
+ mapping(bytes32 role => bytes32 adminRole) private _roleAdmins;
59
+
60
+ /// @dev Forward lookup: role -> set of member addresses.
61
+ mapping(bytes32 role => EnumerableSet.AddressSet members)
62
+ private _roleMembers;
63
+
64
+ /// @dev Reverse lookup: address -> set of roles held.
65
+ mapping(address account => EnumerableSet.Bytes32Set rolesAssigned)
66
+ private _addressRoles;
67
+
68
+ // --- Modifier
69
+
70
+ /**
71
+ * @dev Modifier that checks the caller has `role`.
72
+ * Reverts with {AccessControlUnauthorizedAccount} if the check fails.
73
+ */
74
+ modifier onlyRole(bytes32 role) {
75
+ _checkRole(role);
76
+ _;
77
+ }
78
+
79
+ // --- Initializer
80
+
81
+ /**
82
+ * @dev Chained initializer that grants DEFAULT_ADMIN_ROLE to the initial owner,
83
+ * so they appear in enumeration (getRoleMember, rolesOf) and can administer roles from initialization.
84
+ */
85
+ function __AccessControlExtended_init() internal virtual onlyInitializing {
86
+ __AccessControlExtended_init_unchained();
87
+ }
88
+
89
+ /**
90
+ * @dev Standalone initializer. Only grants DEFAULT_ADMIN_ROLE to the contract owner.
91
+ * Use when the LSP8 base is already initialized through another path.
92
+ */
93
+ function __AccessControlExtended_init_unchained()
94
+ internal
95
+ virtual
96
+ onlyInitializing
97
+ {
98
+ _grantRole(DEFAULT_ADMIN_ROLE, owner());
99
+ }
100
+
101
+ // --- ERC-165
102
+
103
+ /**
104
+ * @dev Returns true for {IAccessControl}, {IAccessControlEnumerable} and {IAccessControlExtended}.
105
+ */
106
+ function supportsInterface(
107
+ bytes4 interfaceId
108
+ ) public view virtual returns (bool) {
109
+ return
110
+ interfaceId == _INTERFACEID_ACCESSCONTROL ||
111
+ interfaceId == _INTERFACEID_ACCESSCONTROLENUMERABLE ||
112
+ interfaceId == _INTERFACEID_ACCESSCONTROLEXTENDED;
113
+ }
114
+
115
+ // --- IAccessControl
116
+
117
+ /**
118
+ * @inheritdoc IAccessControl
119
+ */
120
+ function hasRole(
121
+ bytes32 role,
122
+ address account
123
+ ) public view virtual returns (bool) {
124
+ return _hasRole(role, account);
125
+ }
126
+
127
+ /**
128
+ * @inheritdoc IAccessControl
129
+ */
130
+ function getRoleAdmin(bytes32 role) public view virtual returns (bytes32) {
131
+ return _roleAdmins[role];
132
+ }
133
+
134
+ /**
135
+ * @inheritdoc IAccessControlExtended
136
+ *
137
+ * @dev Sets `adminRole` as the admin of `role`. Available for extensions to configure custom admin hierarchies.
138
+ *
139
+ * @custom:warning
140
+ * - DO NOT expose this function without `onlyOwner` or `onlyRole(DEFAULT_ADMIN_ROLE)` access control.
141
+ * - Be aware that calling `setRoleAdmin(X, X)` creates a self-admin where nobody can grant role `X` unless someone already holds role `X`.
142
+ *
143
+ * @custom:requirements
144
+ * - `role` cannot be the `DEFAULT_ADMIN_ROLE`.
145
+ * - The caller must hold the `DEFAULT_ADMIN_ROLE`.
146
+ *
147
+ * @custom:events {RoleAdminChanged} with the previous and new admin roles.
148
+ */
149
+ function setRoleAdmin(
150
+ bytes32 role,
151
+ bytes32 adminRole
152
+ ) public virtual onlyRole(DEFAULT_ADMIN_ROLE) {
153
+ _setRoleAdmin(role, adminRole);
154
+ }
155
+
156
+ /**
157
+ * @inheritdoc IAccessControl
158
+ *
159
+ * @dev Grants `role` to `account`.
160
+ *
161
+ * @custom:requirements The caller must hold the admin role for `role`.
162
+ */
163
+ function grantRole(
164
+ bytes32 role,
165
+ address account
166
+ ) public virtual onlyRole(getRoleAdmin(role)) {
167
+ _grantRole(role, account);
168
+ }
169
+
170
+ /**
171
+ * @inheritdoc IAccessControl
172
+ * @dev Revokes `role` from `account`. The caller must hold the admin role for `role`.
173
+ *
174
+ * @custom:warning `DEFAULT_ADMIN_ROLE` cannot be removed from the current owner to prevent lockout.
175
+ */
176
+ function revokeRole(
177
+ bytes32 role,
178
+ address account
179
+ ) public virtual onlyRole(getRoleAdmin(role)) {
180
+ require(
181
+ !(role == DEFAULT_ADMIN_ROLE && account == owner()),
182
+ AccessControlUnauthorizedAccount(account, role)
183
+ );
184
+ _revokeRole(role, account);
185
+ }
186
+
187
+ /**
188
+ * @inheritdoc IAccessControl
189
+ *
190
+ * @dev Allows `msg.sender` to renounce their own `role`. The `callerConfirmation`
191
+ * parameter must equal `msg.sender` to prevent accidental renouncement (OZ pattern).
192
+ * Renouncing triggers data cleanup.
193
+ *
194
+ * @custom:warning The current owner cannot renounce `DEFAULT_ADMIN_ROLE`
195
+ * to prevent locking the contract out of role administration.
196
+ *
197
+ * @custom:events Emits {RoleRevoked} if `msg.sender` currently holds `role` and successfully revokes it for itself.
198
+ */
199
+ function renounceRole(
200
+ bytes32 role,
201
+ address callerConfirmation
202
+ ) public virtual {
203
+ require(
204
+ callerConfirmation == msg.sender,
205
+ AccessControlBadConfirmation()
206
+ );
207
+ require(
208
+ !(role == DEFAULT_ADMIN_ROLE && msg.sender == owner()),
209
+ AccessControlUnauthorizedAccount(msg.sender, role)
210
+ );
211
+ _revokeRole(role, msg.sender);
212
+ }
213
+
214
+ // --- IAccessControlEnumerable
215
+
216
+ /**
217
+ * @inheritdoc IAccessControlEnumerable
218
+ */
219
+ function getRoleMember(
220
+ bytes32 role,
221
+ uint256 index
222
+ ) public view virtual returns (address) {
223
+ return _roleMembers[role].at(index);
224
+ }
225
+
226
+ /**
227
+ * @inheritdoc IAccessControlEnumerable
228
+ */
229
+ function getRoleMemberCount(
230
+ bytes32 role
231
+ ) public view virtual returns (uint256) {
232
+ return _roleMembers[role].length();
233
+ }
234
+
235
+ // --- IAccessControlExtended
236
+
237
+ /**
238
+ * @inheritdoc IAccessControlExtended
239
+ */
240
+ function rolesOf(
241
+ address account
242
+ ) public view virtual returns (bytes32[] memory) {
243
+ return _addressRoles[account].values();
244
+ }
245
+
246
+ /**
247
+ * @inheritdoc IAccessControlExtended
248
+ */
249
+ function getRoleMembers(
250
+ bytes32 role
251
+ ) public view virtual returns (address[] memory) {
252
+ return _roleMembers[role].values();
253
+ }
254
+
255
+ // --- Internal functions
256
+
257
+ /**
258
+ * @dev Grants `role` to `account`. No-op if the account already holds the role
259
+ * (matching OZ behavior). Updates both forward and reverse lookups.
260
+ *
261
+ * @custom:events {RoleGranted} if the role was newly granted.
262
+ */
263
+ function _grantRole(bytes32 role, address account) internal virtual {
264
+ bool added = _roleMembers[role].add(account);
265
+
266
+ if (added) {
267
+ _addressRoles[account].add(role);
268
+ emit RoleGranted({
269
+ role: role,
270
+ account: account,
271
+ sender: msg.sender
272
+ });
273
+ }
274
+ }
275
+
276
+ /**
277
+ * @dev Revokes `role` from `account`. No-op if the account does not hold the role.
278
+ * Auto-clears auxiliary data if any exists.
279
+ *
280
+ * @custom:events Emits {RoleRevoked} if the role was revoked.
281
+ */
282
+ function _revokeRole(bytes32 role, address account) internal virtual {
283
+ bool removed = _roleMembers[role].remove(account);
284
+
285
+ if (removed) {
286
+ _addressRoles[account].remove(role);
287
+ emit RoleRevoked({
288
+ role: role,
289
+ account: account,
290
+ sender: msg.sender
291
+ });
292
+ }
293
+ }
294
+
295
+ /**
296
+ * @dev Checks that `msg.sender` has `role`. Reverts with
297
+ * {AccessControlUnauthorizedAccount} if the check fails.
298
+ *
299
+ * @custom:warning Overriding this function changes the behavior of the {onlyRole} modifier.
300
+ */
301
+ function _checkRole(bytes32 role) internal view virtual {
302
+ _checkRole(role, msg.sender);
303
+ }
304
+
305
+ /**
306
+ * @dev Checks that `account` has `role`.
307
+ *
308
+ * Reverts with {AccessControlUnauthorizedAccount} if the account does not
309
+ * explicitly hold the role.
310
+ */
311
+ function _checkRole(bytes32 role, address account) internal view virtual {
312
+ require(
313
+ _hasRole(role, account),
314
+ AccessControlUnauthorizedAccount(account, role)
315
+ );
316
+ }
317
+
318
+ function _hasRole(
319
+ bytes32 role,
320
+ address account
321
+ ) internal view virtual returns (bool) {
322
+ return _roleMembers[role].contains(account);
323
+ }
324
+
325
+ function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
326
+ require(
327
+ role != DEFAULT_ADMIN_ROLE,
328
+ AccessControlCannotSetAdminForDefaultAdminRole()
329
+ );
330
+
331
+ bytes32 previousAdminRole = getRoleAdmin(role);
332
+ _roleAdmins[role] = adminRole;
333
+
334
+ emit RoleAdminChanged({
335
+ role: role,
336
+ previousAdminRole: previousAdminRole,
337
+ newAdminRole: adminRole
338
+ });
339
+ }
340
+
341
+ // --- Ownership sync
342
+
343
+ /**
344
+ * @dev Overrides `_transferOwnership` to automatically transfer ALL roles held by
345
+ * the old owner to the new owner. This includes `DEFAULT_ADMIN_ROLE` and any other
346
+ * custom roles the old owner was assigned.
347
+ *
348
+ * For each role held by the old owner:
349
+ * 1. The role is revoked from the old owner (including clearing auxiliary data).
350
+ * 2. The role is granted to the new owner (if not already held).
351
+ *
352
+ * @custom:info When renouncing ownership, roles are only removed from the old owner. Roles are not passed to `address(0)` (being the `newOwner` in the case of renounce ownership).
353
+ *
354
+ * @custom:warning
355
+ * - Gas cost scales linearly with the number of roles the old owner holds.
356
+ * - Auxiliary role data set on the old owner is transferred to the new owner if ownership is transferred.
357
+ * - Transferring ownership to self will still clear then restore the old owner's auxiliary role data.
358
+ * - If the old owner holds a large number of roles, the transaction may approach or exceed
359
+ * the block gas limit and fail. Avoid assigning too many roles to the owner to ensure
360
+ * ownership transfers remain callable.
361
+ */
362
+ function _transferOwnership(address newOwner) internal virtual override {
363
+ address oldOwner = owner();
364
+ OwnableUpgradeable._transferOwnership(newOwner);
365
+
366
+ // Snapshot the old owner's roles before mutating storage (values() returns a memory copy)
367
+ bytes32[] memory oldOwnerRoles = _addressRoles[oldOwner].values();
368
+
369
+ for (uint256 ii = 0; ii < oldOwnerRoles.length; ++ii) {
370
+ bytes32 role = oldOwnerRoles[ii];
371
+
372
+ _revokeRole(role, oldOwner);
373
+
374
+ // exclude case when renouncing ownership
375
+ if (newOwner != address(0)) {
376
+ _grantRole(role, newOwner);
377
+ }
378
+ }
379
+ }
380
+
381
+ /**
382
+ * @dev This empty reserved space is put in place to allow future versions to add new
383
+ * variables without shifting down storage in the inheritance chain.
384
+ * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
385
+ *
386
+ * @custom:info The size of the `__gap` array is calculated so that the amount of storage used by the contract
387
+ * always adds up to the same number (in this case 50 storage slots).
388
+ */
389
+ uint256[47] private __gap;
390
+ }
@@ -0,0 +1,51 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ pragma solidity ^0.8.27;
3
+
4
+ // interfaces
5
+ import {
6
+ IAccessControlEnumerable
7
+ } from "@openzeppelin/contracts/access/IAccessControlEnumerable.sol";
8
+
9
+ /**
10
+ * @title IAccessControlExtended
11
+ *
12
+ * @dev Interface extending IAccessControlEnumerable with reverse role lookups and function to set admin role for a given role.
13
+ * Inherits all functions from {IAccessControl} and {IAccessControlEnumerable}.
14
+ *
15
+ * @custom:info This interface only include the setter function `setRoleAdmin(bytes32,bytes32)`. The getter `getRoleAdmin(bytes32)` is inherited from {IAccessControl}
16
+ * (itself inherited through {IAccessControlEnumerable}). This ensures that the selector `getRoleAdmin(bytes32)` is not used twice to calculate the final interface Id.
17
+ */
18
+ interface IAccessControlExtended is IAccessControlEnumerable {
19
+ /**
20
+ * @notice Sets a new admin role for `role`.
21
+ * @param role The role identifier being configured.
22
+ * @param adminRole The role that will become the new admin of `role`.
23
+ */
24
+ function setRoleAdmin(bytes32 role, bytes32 adminRole) external;
25
+
26
+ /**
27
+ * @notice Returns all members that hold `role`.
28
+ *
29
+ * @dev Convenience function that returns the full membership array in a single call.
30
+ * Equivalent to calling {getRoleMember} for each index from `0` to `getRoleMemberCount(role) - 1`.
31
+ *
32
+ * @param role The role identifier to query members for.
33
+ * @return An array of addresses that currently hold the specified role.
34
+ *
35
+ * @custom:warning This function copies the entire role membership set into memory.
36
+ * For roles with a large number of members, this may consume a significant amount of gas. If calling this function on-chain,
37
+ * consider calling `{getRoleMember}` repeatedly, using `getRoleMemberCount` to know as max index.
38
+ * This function is primarily intended for off-chain usage.
39
+ */
40
+ function getRoleMembers(
41
+ bytes32 role
42
+ ) external view returns (address[] memory);
43
+
44
+ /**
45
+ * @notice Returns all roles assigned to `account`.
46
+ * @dev Uses a reverse lookup to enumerate all roles held by a given address.
47
+ * @param account The address to query roles for.
48
+ * @return An array of role identifiers assigned to the account.
49
+ */
50
+ function rolesOf(address account) external view returns (bytes32[] memory);
51
+ }
@@ -1,12 +1,12 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- pragma solidity ^0.8.12;
2
+ pragma solidity ^0.8.27;
3
3
 
4
4
  import {
5
5
  LSP8IdentifiableDigitalAsset
6
- } from "../LSP8IdentifiableDigitalAsset.sol";
6
+ } from "../../LSP8IdentifiableDigitalAsset.sol";
7
7
 
8
8
  // errors
9
- import {LSP8NotTokenOperator} from "../LSP8Errors.sol";
9
+ import {LSP8NotTokenOperator} from "../../LSP8Errors.sol";
10
10
 
11
11
  /**
12
12
  * @dev LSP8 token extension that allows token holders to destroy both
@@ -22,9 +22,10 @@ abstract contract LSP8Burnable is LSP8IdentifiableDigitalAsset {
22
22
  * @param data Any extra data to be sent alongside burning the tokenId.
23
23
  */
24
24
  function burn(bytes32 tokenId, bytes memory data) public virtual {
25
- if (!_isOperatorOrOwner(msg.sender, tokenId)) {
26
- revert LSP8NotTokenOperator(tokenId, msg.sender);
27
- }
25
+ require(
26
+ _isOperatorOrOwner(msg.sender, tokenId),
27
+ LSP8NotTokenOperator(tokenId, msg.sender)
28
+ );
28
29
  _burn(tokenId, data);
29
30
  }
30
31
  }
@@ -1,12 +1,12 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- pragma solidity ^0.8.12;
2
+ pragma solidity ^0.8.27;
3
3
 
4
4
  import {
5
5
  LSP8IdentifiableDigitalAssetInitAbstract
6
- } from "../LSP8IdentifiableDigitalAssetInitAbstract.sol";
6
+ } from "../../LSP8IdentifiableDigitalAssetInitAbstract.sol";
7
7
 
8
8
  // errors
9
- import {LSP8NotTokenOperator} from "../LSP8Errors.sol";
9
+ import {LSP8NotTokenOperator} from "../../LSP8Errors.sol";
10
10
 
11
11
  /**
12
12
  * @dev LSP8 extension (proxy version) that allows token holders to destroy both
@@ -16,9 +16,10 @@ abstract contract LSP8BurnableInitAbstract is
16
16
  LSP8IdentifiableDigitalAssetInitAbstract
17
17
  {
18
18
  function burn(bytes32 tokenId, bytes memory data) public virtual {
19
- if (!_isOperatorOrOwner(msg.sender, tokenId)) {
20
- revert LSP8NotTokenOperator(tokenId, msg.sender);
21
- }
19
+ require(
20
+ _isOperatorOrOwner(msg.sender, tokenId),
21
+ LSP8NotTokenOperator(tokenId, msg.sender)
22
+ );
22
23
  _burn(tokenId, data);
23
24
  }
24
25
  }
@@ -0,0 +1,11 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ pragma solidity ^0.8.27;
3
+
4
+ /// @title ILSP8CappedBalance
5
+ /// @dev Interface for an LSP8 token extension that enforces a per-address NFT count cap, with exemptions for allowlisted addresses.
6
+ interface ILSP8CappedBalance {
7
+ /// @notice Retrieves the maximum number of NFTs allowed per address.
8
+ /// @dev Returns the immutable balance cap set during contract deployment.
9
+ /// @return The maximum number of NFTs allowed for any single address.
10
+ function tokenBalanceCap() external view returns (uint256);
11
+ }
@@ -0,0 +1,124 @@
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 {ILSP8CappedBalance} from "./ILSP8CappedBalance.sol";
15
+
16
+ // errors
17
+ import {LSP8CappedBalanceExceeded} from "./LSP8CappedBalanceErrors.sol";
18
+
19
+ /// @title LSP8CappedBalanceAbstract
20
+ /// @dev Abstract contract implementing a per-address NFT count cap for LSP8 tokens, with role-based exemptions.
21
+ abstract contract LSP8CappedBalanceAbstract is
22
+ ILSP8CappedBalance,
23
+ LSP8IdentifiableDigitalAsset,
24
+ AccessControlExtendedAbstract
25
+ {
26
+ /// @notice The dead address is also commonly used for burning tokens as an alternative to address(0).
27
+ address internal constant _DEAD_ADDRESS =
28
+ 0x000000000000000000000000000000000000dEaD;
29
+
30
+ /// @notice The immutable maximum number of NFTs allowed per address.
31
+ uint256 private immutable _TOKEN_BALANCE_CAP;
32
+
33
+ /// @dev keccak256("UNCAPPED_BALANCE_ROLE")
34
+ bytes32 public constant UNCAPPED_BALANCE_ROLE =
35
+ 0x975773d1e0a917a74b57f36a377f439ffff6271648aebdbff75a52ab58eb7bad;
36
+
37
+ /// @notice Initializes the contract with a token balance cap.
38
+ /// @dev Sets the immutable balance cap. If set, grants the initial uncapped balance role exemption to the contract owner.
39
+ /// @param tokenBalanceCap_ The maximum number of NFTs allowed per address. Set to 0 to disable.
40
+ constructor(uint256 tokenBalanceCap_) {
41
+ _TOKEN_BALANCE_CAP = tokenBalanceCap_;
42
+
43
+ if (tokenBalanceCap_ != 0) {
44
+ _grantRole(UNCAPPED_BALANCE_ROLE, owner());
45
+ }
46
+ }
47
+
48
+ /// @inheritdoc ILSP8CappedBalance
49
+ function tokenBalanceCap() public view virtual override returns (uint256) {
50
+ return _TOKEN_BALANCE_CAP;
51
+ }
52
+
53
+ function supportsInterface(
54
+ bytes4 interfaceId
55
+ )
56
+ public
57
+ view
58
+ virtual
59
+ override(AccessControlExtendedAbstract, LSP8IdentifiableDigitalAsset)
60
+ returns (bool)
61
+ {
62
+ return
63
+ AccessControlExtendedAbstract.supportsInterface(interfaceId) ||
64
+ LSP8IdentifiableDigitalAsset.supportsInterface(interfaceId);
65
+ }
66
+
67
+ /// @notice Checks if a token transfer complies with the balance cap.
68
+ /// @dev The address(0) is not subject to balance cap checks as this address is used for burning tokens. Reverts with {LSP8CappedBalanceExceeded} if the recipient's NFT count after receiving the token would exceed the maximum allowed.
69
+ /// @param from The address sending the token.
70
+ /// @param to The address receiving the token.
71
+ function _tokenBalanceCapCheck(
72
+ address from,
73
+ address to,
74
+ bytes32 /* tokenId */,
75
+ bool /* force */,
76
+ bytes memory /* data */
77
+ ) internal virtual {
78
+ // self-transfers do not increase the balance of the recipient, so we skip the check
79
+ if (from == to) return;
80
+
81
+ // Address(0) and 0x0000...dead addresses are used for burning tokens
82
+ if (to == address(0) || to == _DEAD_ADDRESS) return;
83
+
84
+ // Do not check for addresses exempted from balance cap
85
+ if (hasRole(UNCAPPED_BALANCE_ROLE, to)) return;
86
+
87
+ uint256 maxBalanceAllowed = tokenBalanceCap();
88
+ bool isBalanceCapEnabled = maxBalanceAllowed != 0;
89
+
90
+ if (!isBalanceCapEnabled) return;
91
+
92
+ require(
93
+ (balanceOf(to) + 1) <= tokenBalanceCap(),
94
+ LSP8CappedBalanceExceeded(to, balanceOf(to), tokenBalanceCap())
95
+ );
96
+ }
97
+
98
+ /// @notice Hook called before a token transfer to enforce balance cap restrictions.
99
+ /// @dev Bypasses balance cap checks for recipients holding `UNCAPPED_BALANCE_ROLE`. Applies cap checks for all other recipients.
100
+ /// @param from The address sending the token.
101
+ /// @param to The address receiving the token.
102
+ /// @param tokenId The unique identifier of the token being transferred.
103
+ /// @param force Whether to force the transfer.
104
+ /// @param data Additional data for the transfer.
105
+ function _beforeTokenTransfer(
106
+ address from,
107
+ address to,
108
+ bytes32 tokenId,
109
+ bool force,
110
+ bytes memory data
111
+ ) internal virtual override {
112
+ _tokenBalanceCapCheck(from, to, tokenId, force, data);
113
+ super._beforeTokenTransfer(from, to, tokenId, force, data);
114
+ }
115
+
116
+ function _transferOwnership(
117
+ address newOwner
118
+ ) internal virtual override(AccessControlExtendedAbstract, Ownable) {
119
+ // restore default admin hierarchy so a previously-installed custom admin
120
+ // cannot grant UNCAPPED_BALANCE_ROLE to new accounts post-transfer
121
+ _setRoleAdmin(UNCAPPED_BALANCE_ROLE, DEFAULT_ADMIN_ROLE);
122
+ super._transferOwnership(newOwner);
123
+ }
124
+ }
@@ -0,0 +1,9 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ pragma solidity ^0.8.27;
3
+
4
+ /// @dev Error thrown when a transfer would cause an address's NFT count to exceed the token balance cap.
5
+ error LSP8CappedBalanceExceeded(
6
+ address to,
7
+ uint256 currentBalance,
8
+ uint256 tokenBalanceCap
9
+ );