@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.
- package/LICENSE +201 -0
- package/README.md +54 -4
- package/artifacts/IAccessControlExtended.json +285 -0
- package/artifacts/ILSP8CappedBalance.json +27 -0
- package/artifacts/ILSP8CappedSupply.json +27 -0
- package/artifacts/ILSP8IdentifiableDigitalAsset.json +6 -3
- package/artifacts/ILSP8Mintable.json +62 -0
- package/artifacts/ILSP8NonTransferable.json +110 -0
- package/artifacts/ILSP8Revokable.json +75 -0
- package/artifacts/LSP8Burnable.json +7 -4
- package/artifacts/LSP8BurnableInitAbstract.json +7 -4
- package/artifacts/LSP8CappedBalanceAbstract.json +1285 -0
- package/artifacts/LSP8CappedBalanceInitAbstract.json +1293 -0
- package/artifacts/{LSP8CappedSupply.json → LSP8CappedSupplyAbstract.json} +8 -15
- package/artifacts/LSP8CappedSupplyInitAbstract.json +7 -14
- package/artifacts/LSP8CustomizableToken.json +1738 -0
- package/artifacts/LSP8CustomizableTokenInit.json +1733 -0
- package/artifacts/LSP8Enumerable.json +7 -4
- package/artifacts/LSP8EnumerableInitAbstract.json +7 -4
- package/artifacts/LSP8IdentifiableDigitalAsset.json +6 -3
- package/artifacts/LSP8IdentifiableDigitalAssetInitAbstract.json +6 -3
- package/artifacts/LSP8Mintable.json +369 -5
- package/artifacts/LSP8MintableAbstract.json +1328 -0
- package/artifacts/LSP8MintableInit.json +369 -5
- package/artifacts/LSP8MintableInitAbstract.json +1336 -0
- package/artifacts/LSP8NonTransferableAbstract.json +1367 -0
- package/artifacts/LSP8NonTransferableInitAbstract.json +1375 -0
- package/artifacts/LSP8RevokableAbstract.json +1317 -0
- package/artifacts/LSP8RevokableInitAbstract.json +1325 -0
- package/artifacts/LSP8Votes.json +7 -4
- package/artifacts/LSP8VotesInitAbstract.json +7 -4
- package/contracts/ILSP8IdentifiableDigitalAsset.sol +1 -1
- package/contracts/LSP8Constants.sol +1 -1
- package/contracts/LSP8Errors.sol +1 -1
- package/contracts/LSP8IdentifiableDigitalAsset.sol +73 -114
- package/contracts/LSP8IdentifiableDigitalAssetInitAbstract.sol +69 -116
- package/contracts/extensions/AccessControlExtended/AccessControlExtendedAbstract.sol +378 -0
- package/contracts/extensions/AccessControlExtended/AccessControlExtendedConstants.sol +13 -0
- package/contracts/extensions/AccessControlExtended/AccessControlExtendedErrors.sol +23 -0
- package/contracts/extensions/AccessControlExtended/AccessControlExtendedInitAbstract.sol +390 -0
- package/contracts/extensions/AccessControlExtended/IAccessControlExtended.sol +51 -0
- package/contracts/extensions/{LSP8Burnable.sol → LSP8Burnable/LSP8Burnable.sol} +7 -6
- package/contracts/extensions/{LSP8BurnableInitAbstract.sol → LSP8Burnable/LSP8BurnableInitAbstract.sol} +7 -6
- package/contracts/extensions/LSP8CappedBalance/ILSP8CappedBalance.sol +11 -0
- package/contracts/extensions/LSP8CappedBalance/LSP8CappedBalanceAbstract.sol +124 -0
- package/contracts/extensions/LSP8CappedBalance/LSP8CappedBalanceErrors.sol +9 -0
- package/contracts/extensions/LSP8CappedBalance/LSP8CappedBalanceInitAbstract.sol +174 -0
- package/contracts/extensions/LSP8CappedSupply/ILSP8CappedSupply.sol +11 -0
- package/contracts/extensions/LSP8CappedSupply/LSP8CappedSupplyAbstract.sol +59 -0
- package/contracts/extensions/LSP8CappedSupply/LSP8CappedSupplyErrors.sol +6 -0
- package/contracts/extensions/LSP8CappedSupply/LSP8CappedSupplyInitAbstract.sol +97 -0
- package/contracts/extensions/{LSP8Enumerable.sol → LSP8Enumerable/LSP8Enumerable.sol} +2 -2
- package/contracts/extensions/{LSP8EnumerableInitAbstract.sol → LSP8Enumerable/LSP8EnumerableInitAbstract.sol} +2 -2
- package/contracts/extensions/LSP8Mintable/ILSP8Mintable.sol +27 -0
- package/contracts/extensions/LSP8Mintable/LSP8MintableAbstract.sol +105 -0
- package/contracts/extensions/LSP8Mintable/LSP8MintableErrors.sol +5 -0
- package/contracts/extensions/LSP8Mintable/LSP8MintableInitAbstract.sol +155 -0
- package/contracts/extensions/LSP8NonTransferable/ILSP8NonTransferable.sol +48 -0
- package/contracts/extensions/LSP8NonTransferable/LSP8NonTransferableAbstract.sol +190 -0
- package/contracts/extensions/LSP8NonTransferable/LSP8NonTransferableErrors.sol +14 -0
- package/contracts/extensions/LSP8NonTransferable/LSP8NonTransferableInitAbstract.sol +246 -0
- package/contracts/extensions/LSP8Revokable/ILSP8Revokable.sol +28 -0
- package/contracts/extensions/LSP8Revokable/LSP8RevokableAbstract.sol +132 -0
- package/contracts/extensions/LSP8Revokable/LSP8RevokableErrors.sol +4 -0
- package/contracts/extensions/LSP8Revokable/LSP8RevokableInitAbstract.sol +178 -0
- package/contracts/extensions/{LSP8Votes.sol → LSP8Votes/LSP8Votes.sol} +3 -4
- package/contracts/extensions/{LSP8VotesConstants.sol → LSP8Votes/LSP8VotesConstants.sol} +1 -1
- package/contracts/extensions/{LSP8VotesInitAbstract.sol → LSP8Votes/LSP8VotesInitAbstract.sol} +3 -3
- package/contracts/presets/LSP8CustomizableToken.sol +277 -0
- package/contracts/presets/LSP8CustomizableTokenConstants.sol +32 -0
- package/contracts/presets/LSP8CustomizableTokenInit.sol +318 -0
- package/contracts/presets/LSP8Mintable.sol +13 -28
- package/contracts/presets/LSP8MintableInit.sol +13 -6
- package/dist/abi.cjs +8233 -158
- package/dist/abi.d.cts +12004 -323
- package/dist/abi.d.mts +12004 -323
- package/dist/abi.d.ts +12004 -323
- package/dist/abi.mjs +8217 -158
- package/dist/constants.cjs +21 -0
- package/dist/constants.d.cts +12 -1
- package/dist/constants.d.mts +12 -1
- package/dist/constants.d.ts +12 -1
- package/dist/constants.mjs +16 -1
- package/package.json +32 -9
- package/contracts/extensions/LSP8CappedSupply.sol +0 -85
- package/contracts/extensions/LSP8CappedSupplyInitAbstract.sol +0 -88
- package/contracts/presets/ILSP8Mintable.sol +0 -33
- 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.
|
|
2
|
+
pragma solidity ^0.8.27;
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
LSP8IdentifiableDigitalAsset
|
|
6
|
-
} from "
|
|
6
|
+
} from "../../LSP8IdentifiableDigitalAsset.sol";
|
|
7
7
|
|
|
8
8
|
// errors
|
|
9
|
-
import {LSP8NotTokenOperator} from "
|
|
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
|
-
|
|
26
|
-
|
|
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.
|
|
2
|
+
pragma solidity ^0.8.27;
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
LSP8IdentifiableDigitalAssetInitAbstract
|
|
6
|
-
} from "
|
|
6
|
+
} from "../../LSP8IdentifiableDigitalAssetInitAbstract.sol";
|
|
7
7
|
|
|
8
8
|
// errors
|
|
9
|
-
import {LSP8NotTokenOperator} from "
|
|
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
|
-
|
|
20
|
-
|
|
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
|
+
);
|