@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,54 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ pragma solidity ^0.8.27;
3
+
4
+ /// @title ILSP8NonTransferable
5
+ /// @dev Interface for a non-transferable LSP8 token, enabling control over transferability, lock periods, and role-based exemptions.
6
+ interface ILSP8NonTransferable {
7
+ /// @dev Emitted when the transfer lock period is updated.
8
+ /// @param nonTransferabilityEnabled Whether the non-transferability feature is enabled or not.
9
+ /// @param start The new start timestamp of the transfer lock period.
10
+ /// @param end The new end timestamp of the transfer lock period.
11
+ event TransferLockPeriodChanged(
12
+ bool indexed nonTransferabilityEnabled,
13
+ uint256 indexed start,
14
+ uint256 indexed end
15
+ );
16
+
17
+ /// @notice The start timestamp of the transfer lock period, at which point the token becomes non-transferable.
18
+ function transferLockStart() external view returns (uint256);
19
+
20
+ /// @notice The end timestamp of the transfer lock period, at which point the token becomes transferable again.
21
+ function transferLockEnd() external view returns (uint256);
22
+
23
+ /// @notice Returns whether the transfer lock feature is still enabled.
24
+ /// @dev When this returns `false`, the token has been permanently made transferable and the lock period can no longer be updated.
25
+ function nonTransferabilityEnabled() external view returns (bool);
26
+
27
+ /// @notice Checks if the token is currently transferable.
28
+ /// @dev Returns true if the token is transferable (based on the lock period). Note that transfers from addresses holding the bypass role and burning (transfers to address(0)) is always allowed, regardless of transferability status.
29
+ /// @return True if the token is transferable, false otherwise.
30
+ function isTransferable() external view returns (bool);
31
+
32
+ /// @notice Removes all transfer lock, enabling token transfers for all addresses.
33
+ /// @dev Can only be called by the contract owner. Sets both lock periods to 0.
34
+ /// @custom:emits {TransferLockPeriodChanged} event.
35
+ function makeTransferable() external;
36
+
37
+ /// @notice Updates the transfer lock period with new start and end timestamps.
38
+ /// - When `transferLockStart` is 0 and `transferLockEnd` is set to a non-zero value, it means no start time is set. The token is non-transferable immediately until `transferLockEnd`.
39
+ /// - When `transferLockStart` is set to a value and `transferLockEnd` is 0, it means the tokens becomes non-transferable at a certain point in time and indefinitely (no end time).
40
+ ///
41
+ /// - To make the token always non-transferable, set `transferLockStart` to 0 and `transferLockEnd` to type(uint256).max.
42
+ /// - To remove the active lock while keeping the non-transferability feature configurable, set both `newTransferLockStart` and `newTransferLockEnd` to 0. In this state, transfers are currently unrestricted, but the owner can still configure a new lock period later.
43
+ /// - To permanently disable the non-transferability feature and prevent future lock-period updates, use the {makeTransferable} function.
44
+ ///
45
+ /// @dev Can only be called by the contract owner. Reverts once {makeTransferable} has been called.
46
+ ///
47
+ /// @custom:emits {TransferLockPeriodChanged} event.
48
+ /// @param newTransferLockStart The new start timestamp for the transfer lock period.
49
+ /// @param newTransferLockEnd The new end timestamp for the transfer lock period.
50
+ function updateTransferLockPeriod(
51
+ uint256 newTransferLockStart,
52
+ uint256 newTransferLockEnd
53
+ ) external;
54
+ }
@@ -0,0 +1,199 @@
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 {ILSP8NonTransferable} from "./ILSP8NonTransferable.sol";
15
+
16
+ // errors
17
+ import {
18
+ LSP8TransferDisabled,
19
+ LSP8InvalidTransferLockPeriod,
20
+ LSP8CannotUpdateTransferLockPeriod,
21
+ LSP8TokenAlreadyTransferable
22
+ } from "./LSP8NonTransferableErrors.sol";
23
+
24
+ /// @title LSP8NonTransferableAbstract
25
+ /// @dev Abstract contract implementing non-transferable LSP8 token functionality with transfer lock periods and role-based bypass support.
26
+ abstract contract LSP8NonTransferableAbstract is
27
+ ILSP8NonTransferable,
28
+ LSP8IdentifiableDigitalAsset,
29
+ AccessControlExtendedAbstract
30
+ {
31
+ /// @dev keccak256("NON_TRANSFERABLE_BYPASS_ROLE")
32
+ bytes32 public constant NON_TRANSFERABLE_BYPASS_ROLE =
33
+ 0xb4b3a36d7c2b72add3151898671aaed843238e580f7d6d4bc5077ce2023b0659;
34
+
35
+ /// @inheritdoc ILSP8NonTransferable
36
+ uint256 public transferLockStart;
37
+
38
+ /// @inheritdoc ILSP8NonTransferable
39
+ uint256 public transferLockEnd;
40
+
41
+ /// @inheritdoc ILSP8NonTransferable
42
+ bool public nonTransferabilityEnabled;
43
+
44
+ /// @notice Initializes the contract with lock period.
45
+ /// @param transferLockStart_ The start timestamp of the transfer lock period, 0 to disable.
46
+ /// @param transferLockEnd_ The end timestamp of the transfer lock period, 0 to disable.
47
+ constructor(uint256 transferLockStart_, uint256 transferLockEnd_) {
48
+ require(
49
+ transferLockEnd_ == 0 || transferLockEnd_ >= transferLockStart_,
50
+ LSP8InvalidTransferLockPeriod()
51
+ );
52
+ transferLockStart = transferLockStart_;
53
+ transferLockEnd = transferLockEnd_;
54
+ nonTransferabilityEnabled = true;
55
+
56
+ emit TransferLockPeriodChanged({
57
+ nonTransferabilityEnabled: true,
58
+ start: transferLockStart_,
59
+ end: transferLockEnd_
60
+ });
61
+ _grantRole(NON_TRANSFERABLE_BYPASS_ROLE, owner());
62
+ }
63
+
64
+ function supportsInterface(
65
+ bytes4 interfaceId
66
+ )
67
+ public
68
+ view
69
+ virtual
70
+ override(AccessControlExtendedAbstract, LSP8IdentifiableDigitalAsset)
71
+ returns (bool)
72
+ {
73
+ return
74
+ AccessControlExtendedAbstract.supportsInterface(interfaceId) ||
75
+ LSP8IdentifiableDigitalAsset.supportsInterface(interfaceId);
76
+ }
77
+
78
+ /// @inheritdoc ILSP8NonTransferable
79
+ // solhint-disable not-rely-on-time
80
+ // Transfer-lock windows are inherently time-based; `block.timestamp` is the intended source.
81
+ function isTransferable() public view virtual override returns (bool) {
82
+ if (!nonTransferabilityEnabled) return true;
83
+
84
+ bool isTransferLockStartEnabled = transferLockStart != 0;
85
+ bool isTransferLockEndEnabled = transferLockEnd != 0;
86
+
87
+ // If both lock periods are disabled, the token is transferable
88
+ if (!isTransferLockStartEnabled && !isTransferLockEndEnabled) {
89
+ return true;
90
+ }
91
+
92
+ // If the token is non-transferable up to a certain point in time, check if we have passed this period
93
+ if (!isTransferLockStartEnabled && isTransferLockEndEnabled) {
94
+ return transferLockEnd < block.timestamp;
95
+ }
96
+
97
+ // If the token becomes non-transferable starting at a specific point in time, check if we have reached this lock starting period
98
+ if (isTransferLockStartEnabled && !isTransferLockEndEnabled) {
99
+ return transferLockStart > block.timestamp;
100
+ }
101
+
102
+ // This last case checks if we are within the transfer lock period
103
+ return
104
+ transferLockStart > block.timestamp ||
105
+ transferLockEnd < block.timestamp;
106
+ }
107
+ // solhint-enable not-rely-on-time
108
+
109
+ /// @inheritdoc ILSP8NonTransferable
110
+ /// @custom:info The list of addresses holding the `NON_TRANSFERABLE_BYPASS_ROLE` remains populated after the non-transferable feature is switched off.
111
+ function makeTransferable() public virtual override onlyOwner {
112
+ require(nonTransferabilityEnabled, LSP8TokenAlreadyTransferable());
113
+
114
+ nonTransferabilityEnabled = false;
115
+ transferLockStart = 0;
116
+ transferLockEnd = 0;
117
+
118
+ emit TransferLockPeriodChanged({
119
+ nonTransferabilityEnabled: false,
120
+ start: 0,
121
+ end: 0
122
+ });
123
+ }
124
+
125
+ /// @inheritdoc ILSP8NonTransferable
126
+ function updateTransferLockPeriod(
127
+ uint256 newTransferLockStart,
128
+ uint256 newTransferLockEnd
129
+ ) public virtual override onlyOwner {
130
+ require(nonTransferabilityEnabled, LSP8CannotUpdateTransferLockPeriod());
131
+
132
+ // When transferLockEnd is 0, it means no end time is set (transfers locked indefinitely after transferLockStart)
133
+ // When transferLockStart is 0, it means no start time is set (transfers locked up until transferLockEnd)
134
+ // Allow to make the token always non-transferable, or ensure the end period for locking transfers is always later than the starting period
135
+ require(
136
+ newTransferLockEnd == 0 ||
137
+ newTransferLockEnd >= newTransferLockStart,
138
+ LSP8InvalidTransferLockPeriod()
139
+ );
140
+
141
+ transferLockStart = newTransferLockStart;
142
+ transferLockEnd = newTransferLockEnd;
143
+
144
+ emit TransferLockPeriodChanged({
145
+ nonTransferabilityEnabled: true,
146
+ start: newTransferLockStart,
147
+ end: newTransferLockEnd
148
+ });
149
+ }
150
+
151
+ /// @notice Checks if a token transfer is allowed based on transferability status.
152
+ /// @dev Allows burning to address(0) even when transfers are disabled, bypassing transferability restrictions. Reverts with {LSP8TransferDisabled} if the token is non-transferable and the destination is not address(0).
153
+ /// @param to The address receiving the token.
154
+ function _nonTransferableCheck(
155
+ address from,
156
+ address to,
157
+ bytes32,
158
+ /* tokenId */
159
+ bool,
160
+ /* force */
161
+ bytes memory /* data */
162
+ ) internal virtual {
163
+ // Allow minting and burning
164
+ if (from == address(0) || to == address(0)) return;
165
+
166
+ // Do not check for addresses exempted from non transferable check
167
+ if (hasRole(NON_TRANSFERABLE_BYPASS_ROLE, from)) return;
168
+
169
+ // transferring tokens only if the transferability status is enabled
170
+ require(isTransferable(), LSP8TransferDisabled());
171
+ }
172
+
173
+ /// @notice Hook called before a token transfer to enforce transfer restrictions.
174
+ /// @dev Bypasses transfer restrictions for addresses holding `NON_TRANSFERABLE_BYPASS_ROLE`, allowing them to transfer tokens even when {isTransferable} returns false. For all other addresses, applies non-transferable checks.
175
+ /// @param from The address sending the token.
176
+ /// @param to The address receiving the token.
177
+ /// @param tokenId The unique identifier of the token being transferred.
178
+ /// @param force Whether to force the transfer (passed to _nonTransferableCheck).
179
+ /// @param data Additional data for the transfer (passed to _nonTransferableCheck).
180
+ function _beforeTokenTransfer(
181
+ address from,
182
+ address to,
183
+ bytes32 tokenId,
184
+ bool force,
185
+ bytes memory data
186
+ ) internal virtual override {
187
+ _nonTransferableCheck(from, to, tokenId, force, data);
188
+ super._beforeTokenTransfer(from, to, tokenId, force, data);
189
+ }
190
+
191
+ function _transferOwnership(
192
+ address newOwner
193
+ ) internal virtual override(AccessControlExtendedAbstract, Ownable) {
194
+ // restore default admin hierarchy so a previously-installed custom admin
195
+ // cannot grant NON_TRANSFERABLE_BYPASS_ROLE to new accounts post-transfer
196
+ _setRoleAdmin(NON_TRANSFERABLE_BYPASS_ROLE, DEFAULT_ADMIN_ROLE);
197
+ super._transferOwnership(newOwner);
198
+ }
199
+ }
@@ -0,0 +1,14 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ pragma solidity ^0.8.27;
3
+
4
+ /// @dev Error thrown when attempting a token transfer while transfers are disabled.
5
+ error LSP8TransferDisabled();
6
+
7
+ /// @dev Error thrown when the transfer lock period is invalid, such as when the end timestamp is earlier than the start timestamp.
8
+ error LSP8InvalidTransferLockPeriod();
9
+
10
+ /// @dev Error thrown when attempting to update the transfer lock period after it has begun.
11
+ error LSP8CannotUpdateTransferLockPeriod();
12
+
13
+ /// @dev Error thrown when attempting to make a token transferable that is already transferable.
14
+ error LSP8TokenAlreadyTransferable();
@@ -0,0 +1,255 @@
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 {ILSP8NonTransferable} from "./ILSP8NonTransferable.sol";
17
+
18
+ // errors
19
+ import {
20
+ LSP8TransferDisabled,
21
+ LSP8InvalidTransferLockPeriod,
22
+ LSP8CannotUpdateTransferLockPeriod,
23
+ LSP8TokenAlreadyTransferable
24
+ } from "./LSP8NonTransferableErrors.sol";
25
+
26
+ /// @title LSP8NonTransferableInitAbstract
27
+ /// @dev Abstract contract implementing non-transferable LSP8 token functionality with transfer lock periods
28
+ /// and support to bypass non-transferable checks through a role.
29
+ abstract contract LSP8NonTransferableInitAbstract is
30
+ ILSP8NonTransferable,
31
+ LSP8IdentifiableDigitalAssetInitAbstract,
32
+ AccessControlExtendedInitAbstract
33
+ {
34
+ /// @dev keccak256("NON_TRANSFERABLE_BYPASS_ROLE")
35
+ bytes32 public constant NON_TRANSFERABLE_BYPASS_ROLE =
36
+ 0xb4b3a36d7c2b72add3151898671aaed843238e580f7d6d4bc5077ce2023b0659;
37
+
38
+ /// @inheritdoc ILSP8NonTransferable
39
+ uint256 public transferLockStart;
40
+
41
+ /// @inheritdoc ILSP8NonTransferable
42
+ uint256 public transferLockEnd;
43
+
44
+ /// @inheritdoc ILSP8NonTransferable
45
+ bool public nonTransferabilityEnabled;
46
+
47
+ /// @notice Initializes the LSP8NonTransferable contract with base token params and transfer settings.
48
+ /// @dev Initializes the LSP8IdentifiableDigitalAsset base, the access control layer and transfer settings.
49
+ /// @param name_ The name of the token.
50
+ /// @param symbol_ The symbol of the token.
51
+ /// @param newOwner_ The owner of the contract.
52
+ /// @param lsp4TokenType_ The token type (see LSP4).
53
+ /// @param lsp8TokenIdFormat_ The format of tokenIds (= NFTs) that this contract will create.
54
+ /// @param transferLockStart_ The start timestamp of the transfer lock period, 0 to disable.
55
+ /// @param transferLockEnd_ The end timestamp of the transfer lock period, 0 to disable.
56
+ function __LSP8NonTransferable_init(
57
+ string memory name_,
58
+ string memory symbol_,
59
+ address newOwner_,
60
+ uint256 lsp4TokenType_,
61
+ uint256 lsp8TokenIdFormat_,
62
+ uint256 transferLockStart_,
63
+ uint256 transferLockEnd_
64
+ ) internal virtual onlyInitializing {
65
+ LSP8IdentifiableDigitalAssetInitAbstract._initialize(
66
+ name_,
67
+ symbol_,
68
+ newOwner_,
69
+ lsp4TokenType_,
70
+ lsp8TokenIdFormat_
71
+ );
72
+ __AccessControlExtended_init();
73
+ __LSP8NonTransferable_init_unchained(
74
+ transferLockStart_,
75
+ transferLockEnd_
76
+ );
77
+ }
78
+
79
+ /// @notice Unchained initializer for the transfer settings.
80
+ /// @dev Sets lock period.
81
+ /// @param transferLockStart_ The start timestamp of the transfer lock period, 0 to disable.
82
+ /// @param transferLockEnd_ The end timestamp of the transfer lock period, 0 to disable.
83
+ function __LSP8NonTransferable_init_unchained(
84
+ uint256 transferLockStart_,
85
+ uint256 transferLockEnd_
86
+ ) internal virtual onlyInitializing {
87
+ require(
88
+ transferLockEnd_ == 0 || transferLockEnd_ >= transferLockStart_,
89
+ LSP8InvalidTransferLockPeriod()
90
+ );
91
+ transferLockStart = transferLockStart_;
92
+ transferLockEnd = transferLockEnd_;
93
+ nonTransferabilityEnabled = true;
94
+
95
+ emit TransferLockPeriodChanged({
96
+ nonTransferabilityEnabled: true,
97
+ start: transferLockStart_,
98
+ end: transferLockEnd_
99
+ });
100
+ _grantRole(NON_TRANSFERABLE_BYPASS_ROLE, owner());
101
+ }
102
+
103
+ function supportsInterface(
104
+ bytes4 interfaceId
105
+ )
106
+ public
107
+ view
108
+ virtual
109
+ override(
110
+ AccessControlExtendedInitAbstract,
111
+ LSP8IdentifiableDigitalAssetInitAbstract
112
+ )
113
+ returns (bool)
114
+ {
115
+ return
116
+ AccessControlExtendedInitAbstract.supportsInterface(interfaceId) ||
117
+ LSP8IdentifiableDigitalAssetInitAbstract.supportsInterface(
118
+ interfaceId
119
+ );
120
+ }
121
+
122
+ /// @inheritdoc ILSP8NonTransferable
123
+ // solhint-disable not-rely-on-time
124
+ // Transfer-lock windows are inherently time-based; `block.timestamp` is the intended source.
125
+ function isTransferable() public view virtual override returns (bool) {
126
+ if (!nonTransferabilityEnabled) return true;
127
+
128
+ bool isTransferLockStartEnabled = transferLockStart != 0;
129
+ bool isTransferLockEndEnabled = transferLockEnd != 0;
130
+
131
+ // If both lock periods are disabled, the token is transferable
132
+ if (!isTransferLockStartEnabled && !isTransferLockEndEnabled) {
133
+ return true;
134
+ }
135
+
136
+ // If the token is non-transferable up to a certain point in time, check if we have passed this period
137
+ if (!isTransferLockStartEnabled && isTransferLockEndEnabled) {
138
+ return transferLockEnd < block.timestamp;
139
+ }
140
+
141
+ // If the token becomes non-transferable starting at a specific point in time, check if we have reached this lock starting period
142
+ if (isTransferLockStartEnabled && !isTransferLockEndEnabled) {
143
+ return transferLockStart > block.timestamp;
144
+ }
145
+
146
+ // This last case checks if we are within the transfer lock period
147
+ return
148
+ transferLockStart > block.timestamp ||
149
+ transferLockEnd < block.timestamp;
150
+ }
151
+ // solhint-enable not-rely-on-time
152
+
153
+ /// @inheritdoc ILSP8NonTransferable
154
+ /// @custom:info The list of addresses holding the `NON_TRANSFERABLE_BYPASS_ROLE` remains populated after the non-transferable feature is switched off.
155
+ function makeTransferable() public virtual override onlyOwner {
156
+ require(nonTransferabilityEnabled, LSP8TokenAlreadyTransferable());
157
+
158
+ nonTransferabilityEnabled = false;
159
+ transferLockStart = 0;
160
+ transferLockEnd = 0;
161
+
162
+ emit TransferLockPeriodChanged({
163
+ nonTransferabilityEnabled: false,
164
+ start: 0,
165
+ end: 0
166
+ });
167
+ }
168
+
169
+ /// @inheritdoc ILSP8NonTransferable
170
+ function updateTransferLockPeriod(
171
+ uint256 newTransferLockStart,
172
+ uint256 newTransferLockEnd
173
+ ) public virtual override onlyOwner {
174
+ require(nonTransferabilityEnabled, LSP8CannotUpdateTransferLockPeriod());
175
+
176
+ // When transferLockEnd is 0, it means no end time is set (transfers locked indefinitely after transferLockStart)
177
+ // When transferLockStart is 0, it means no start time is set (transfers locked up until transferLockEnd)
178
+ // Allow to make the token always non-transferable, or ensure the end period for locking transfers is always later than the starting period
179
+ require(
180
+ newTransferLockEnd == 0 ||
181
+ newTransferLockEnd >= newTransferLockStart,
182
+ LSP8InvalidTransferLockPeriod()
183
+ );
184
+
185
+ transferLockStart = newTransferLockStart;
186
+ transferLockEnd = newTransferLockEnd;
187
+
188
+ emit TransferLockPeriodChanged({
189
+ nonTransferabilityEnabled: true,
190
+ start: newTransferLockStart,
191
+ end: newTransferLockEnd
192
+ });
193
+ }
194
+
195
+ /// @notice Checks if a token transfer is allowed based on transferability status.
196
+ /// @dev Allows burning to address(0) even when transfers are disabled, bypassing transferability restrictions. Reverts with {LSP8TransferDisabled} if the token is non-transferable and the destination is not address(0).
197
+ /// @param to The address receiving the token.
198
+ function _nonTransferableCheck(
199
+ address from,
200
+ address to,
201
+ bytes32 /* tokenId */,
202
+ bool /* force */,
203
+ bytes memory /* data */
204
+ ) internal virtual {
205
+ // Allow minting and burning
206
+ if (from == address(0) || to == address(0)) return;
207
+
208
+ // Do not check for addresses exempted from non transferable check
209
+ if (hasRole(NON_TRANSFERABLE_BYPASS_ROLE, from)) return;
210
+
211
+ // transferring tokens only if the transferability status is enabled
212
+ require(isTransferable(), LSP8TransferDisabled());
213
+ }
214
+
215
+ /// @notice Hook called before a token transfer to enforce transfer restrictions.
216
+ /// @dev Bypasses transfer restrictions for addresses holding `NON_TRANSFERABLE_BYPASS_ROLE`, allowing them to transfer tokens even when {isTransferable} returns false. For all other addresses, applies non-transferable checks.
217
+ /// @param from The address sending the token.
218
+ /// @param to The address receiving the token.
219
+ /// @param tokenId The unique identifier of the token being transferred.
220
+ /// @param force Whether to force the transfer (passed to _nonTransferableCheck).
221
+ /// @param data Additional data for the transfer (passed to _nonTransferableCheck).
222
+ function _beforeTokenTransfer(
223
+ address from,
224
+ address to,
225
+ bytes32 tokenId,
226
+ bool force,
227
+ bytes memory data
228
+ ) internal virtual override {
229
+ _nonTransferableCheck(from, to, tokenId, force, data);
230
+ super._beforeTokenTransfer(from, to, tokenId, force, data);
231
+ }
232
+
233
+ function _transferOwnership(
234
+ address newOwner
235
+ )
236
+ internal
237
+ virtual
238
+ override(AccessControlExtendedInitAbstract, OwnableUpgradeable)
239
+ {
240
+ // restore default admin hierarchy so a previously-installed custom admin
241
+ // cannot grant NON_TRANSFERABLE_BYPASS_ROLE to new accounts post-transfer
242
+ _setRoleAdmin(NON_TRANSFERABLE_BYPASS_ROLE, DEFAULT_ADMIN_ROLE);
243
+ super._transferOwnership(newOwner);
244
+ }
245
+
246
+ /**
247
+ * @dev This empty reserved space is put in place to allow future versions to add new
248
+ * variables without shifting down storage in the inheritance chain.
249
+ * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
250
+ *
251
+ * @custom:info The size of the `__gap` array is calculated so that the amount of storage used by the contract
252
+ * always adds up to the same number (in this case 50 storage slots).
253
+ */
254
+ uint256[47] private __gap;
255
+ }
@@ -0,0 +1,37 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ pragma solidity ^0.8.27;
3
+
4
+ /// @title ILSP8Revokable
5
+ /// @dev Interface for LSP8 tokens that can be revoked by addresses holding `REVOKER_ROLE`.
6
+ /// This extension allows authorized revokers to reclaim NFTs from any holder back to the
7
+ /// contract owner or another authorized revoker.
8
+ interface ILSP8Revokable {
9
+ /// @dev Emitted when revokable status is changed.
10
+ event RevokableStatusChanged(bool indexed enabled);
11
+
12
+ /// @dev Emitted when a token is revoked from a holder.
13
+ event TokenRevoked(
14
+ address indexed revoker,
15
+ address indexed from,
16
+ address indexed to,
17
+ bytes32 tokenId,
18
+ bytes data
19
+ );
20
+
21
+ /// @notice Returns whether the feature to revoke tokens from users is enabled or not.
22
+ function isRevokable() external view returns (bool);
23
+
24
+ /// @notice Disables token revocation permanently.
25
+ /// @dev Can only be called by the contract owner. Prevents further calls to revoke after invocation.
26
+ function disableRevokable() external;
27
+
28
+ /// @notice Revokes `tokenId` from a holder and transfers it to `to`.
29
+ /// @dev Can only be called by an address holding `REVOKER_ROLE`.
30
+ /// The destination must be either the contract owner or an address holding `REVOKER_ROLE`.
31
+ /// The original token holder will be notified via LSP1 universalReceiver.
32
+ /// @param from The address to revoke the token from.
33
+ /// @param to The address receiving the revoked token.
34
+ /// @param tokenId The tokenId to revoke.
35
+ /// @param data Additional data to include in the transfer notification.
36
+ function revoke(address from, address to, bytes32 tokenId, bytes memory data) external;
37
+ }