@lukso/lsp8-contracts 0.16.5 → 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 +55 -5
  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 +38 -15
  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,48 @@
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 start The new start timestamp of the transfer lock period.
9
+ /// @param end The new end timestamp of the transfer lock period.
10
+ event TransferLockPeriodChanged(uint256 indexed start, uint256 indexed end);
11
+
12
+ /// @notice The start timestamp of the transfer lock period, at which point the token becomes non-transferable.
13
+ function transferLockStart() external view returns (uint256);
14
+
15
+ /// @notice The end timestamp of the transfer lock period, at which point the token becomes transferable again.
16
+ function transferLockEnd() external view returns (uint256);
17
+
18
+ /// @notice Returns whether the transfer lock feature is still enabled.
19
+ /// @dev When this returns `false`, the token has been permanently made transferable and the lock period can no longer be updated.
20
+ function transferLockEnabled() external view returns (bool);
21
+
22
+ /// @notice Checks if the token is currently transferable.
23
+ /// @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.
24
+ /// @return True if the token is transferable, false otherwise.
25
+ function isTransferable() external view returns (bool);
26
+
27
+ /// @notice Removes all transfer lock, enabling token transfers for all addresses.
28
+ /// @dev Can only be called by the contract owner. Sets both lock periods to 0.
29
+ /// @custom:emits {TransferLockPeriodChanged} event.
30
+ function makeTransferable() external;
31
+
32
+ /// @notice Updates the transfer lock period with new start and end timestamps.
33
+ /// - 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`.
34
+ /// - 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).
35
+ ///
36
+ /// - To make the token always non-transferable, set `transferLockStart` to 0 and `transferLockEnd` to type(uint256).max.
37
+ /// - To disable completely the non-transferable feature (= make the token always transferable), set both `transferLockStart` and `transferLockEnd` to 0.
38
+ ///
39
+ /// @dev Can only be called by the contract owner. Reverts once {makeTransferable} has been called.
40
+ ///
41
+ /// @custom:emits {TransferLockPeriodChanged} event.
42
+ /// @param newTransferLockStart The new start timestamp for the transfer lock period.
43
+ /// @param newTransferLockEnd The new end timestamp for the transfer lock period.
44
+ function updateTransferLockPeriod(
45
+ uint256 newTransferLockStart,
46
+ uint256 newTransferLockEnd
47
+ ) external;
48
+ }
@@ -0,0 +1,190 @@
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 transferLockEnabled;
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
+ transferLockEnabled = true;
55
+
56
+ emit TransferLockPeriodChanged(transferLockStart_, transferLockEnd_);
57
+ _grantRole(NON_TRANSFERABLE_BYPASS_ROLE, owner());
58
+ }
59
+
60
+ function supportsInterface(
61
+ bytes4 interfaceId
62
+ )
63
+ public
64
+ view
65
+ virtual
66
+ override(AccessControlExtendedAbstract, LSP8IdentifiableDigitalAsset)
67
+ returns (bool)
68
+ {
69
+ return
70
+ AccessControlExtendedAbstract.supportsInterface(interfaceId) ||
71
+ LSP8IdentifiableDigitalAsset.supportsInterface(interfaceId);
72
+ }
73
+
74
+ /// @inheritdoc ILSP8NonTransferable
75
+ // solhint-disable not-rely-on-time
76
+ // Transfer-lock windows are inherently time-based; `block.timestamp` is the intended source.
77
+ function isTransferable() public view virtual override returns (bool) {
78
+ if (!transferLockEnabled) return true;
79
+
80
+ bool isTransferLockStartEnabled = transferLockStart != 0;
81
+ bool isTransferLockEndEnabled = transferLockEnd != 0;
82
+
83
+ // If both lock periods are disabled, the token is transferable
84
+ if (!isTransferLockStartEnabled && !isTransferLockEndEnabled) {
85
+ return true;
86
+ }
87
+
88
+ // If the token is non-transferable up to a certain point in time, check if we have passed this period
89
+ if (!isTransferLockStartEnabled && isTransferLockEndEnabled) {
90
+ return transferLockEnd < block.timestamp;
91
+ }
92
+
93
+ // If the token becomes non-transferable starting at a specific point in time, check if we have reached this lock starting period
94
+ if (isTransferLockStartEnabled && !isTransferLockEndEnabled) {
95
+ return transferLockStart > block.timestamp;
96
+ }
97
+
98
+ // This last case checks if we are within the transfer lock period
99
+ return
100
+ transferLockStart > block.timestamp ||
101
+ transferLockEnd < block.timestamp;
102
+ }
103
+ // solhint-enable not-rely-on-time
104
+
105
+ /// @inheritdoc ILSP8NonTransferable
106
+ /// @custom:info The list of addresses holding the `NON_TRANSFERABLE_BYPASS_ROLE` remains populated after the non-transferable feature is switched off.
107
+ function makeTransferable() public virtual override onlyOwner {
108
+ require(transferLockEnabled, LSP8TokenAlreadyTransferable());
109
+
110
+ transferLockEnabled = false;
111
+ transferLockStart = 0;
112
+ transferLockEnd = 0;
113
+
114
+ emit TransferLockPeriodChanged({start: 0, end: 0});
115
+ }
116
+
117
+ /// @inheritdoc ILSP8NonTransferable
118
+ function updateTransferLockPeriod(
119
+ uint256 newTransferLockStart,
120
+ uint256 newTransferLockEnd
121
+ ) public virtual override onlyOwner {
122
+ require(transferLockEnabled, LSP8CannotUpdateTransferLockPeriod());
123
+
124
+ // When transferLockEnd is 0, it means no end time is set (transfers locked indefinitely after transferLockStart)
125
+ // When transferLockStart is 0, it means no start time is set (transfers locked up until transferLockEnd)
126
+ // Allow to make the token always non-transferable, or ensure the end period for locking transfers is always later than the starting period
127
+ require(
128
+ newTransferLockEnd == 0 ||
129
+ newTransferLockEnd >= newTransferLockStart,
130
+ LSP8InvalidTransferLockPeriod()
131
+ );
132
+
133
+ transferLockStart = newTransferLockStart;
134
+ transferLockEnd = newTransferLockEnd;
135
+
136
+ emit TransferLockPeriodChanged({
137
+ start: newTransferLockStart,
138
+ end: newTransferLockEnd
139
+ });
140
+ }
141
+
142
+ /// @notice Checks if a token transfer is allowed based on transferability status.
143
+ /// @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).
144
+ /// @param to The address receiving the token.
145
+ function _nonTransferableCheck(
146
+ address from,
147
+ address to,
148
+ bytes32,
149
+ /* tokenId */
150
+ bool,
151
+ /* force */
152
+ bytes memory /* data */
153
+ ) internal virtual {
154
+ // Allow minting and burning
155
+ if (from == address(0) || to == address(0)) return;
156
+
157
+ // Do not check for addresses exempted from non transferable check
158
+ if (hasRole(NON_TRANSFERABLE_BYPASS_ROLE, from)) return;
159
+
160
+ // transferring tokens only if the transferability status is enabled
161
+ require(isTransferable(), LSP8TransferDisabled());
162
+ }
163
+
164
+ /// @notice Hook called before a token transfer to enforce transfer restrictions.
165
+ /// @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.
166
+ /// @param from The address sending the token.
167
+ /// @param to The address receiving the token.
168
+ /// @param tokenId The unique identifier of the token being transferred.
169
+ /// @param force Whether to force the transfer (passed to _nonTransferableCheck).
170
+ /// @param data Additional data for the transfer (passed to _nonTransferableCheck).
171
+ function _beforeTokenTransfer(
172
+ address from,
173
+ address to,
174
+ bytes32 tokenId,
175
+ bool force,
176
+ bytes memory data
177
+ ) internal virtual override {
178
+ _nonTransferableCheck(from, to, tokenId, force, data);
179
+ super._beforeTokenTransfer(from, to, tokenId, force, data);
180
+ }
181
+
182
+ function _transferOwnership(
183
+ address newOwner
184
+ ) internal virtual override(AccessControlExtendedAbstract, Ownable) {
185
+ // restore default admin hierarchy so a previously-installed custom admin
186
+ // cannot grant NON_TRANSFERABLE_BYPASS_ROLE to new accounts post-transfer
187
+ _setRoleAdmin(NON_TRANSFERABLE_BYPASS_ROLE, DEFAULT_ADMIN_ROLE);
188
+ super._transferOwnership(newOwner);
189
+ }
190
+ }
@@ -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,246 @@
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 transferLockEnabled;
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
+ transferLockEnabled = true;
94
+
95
+ emit TransferLockPeriodChanged(transferLockStart_, transferLockEnd_);
96
+ _grantRole(NON_TRANSFERABLE_BYPASS_ROLE, owner());
97
+ }
98
+
99
+ function supportsInterface(
100
+ bytes4 interfaceId
101
+ )
102
+ public
103
+ view
104
+ virtual
105
+ override(
106
+ AccessControlExtendedInitAbstract,
107
+ LSP8IdentifiableDigitalAssetInitAbstract
108
+ )
109
+ returns (bool)
110
+ {
111
+ return
112
+ AccessControlExtendedInitAbstract.supportsInterface(interfaceId) ||
113
+ LSP8IdentifiableDigitalAssetInitAbstract.supportsInterface(
114
+ interfaceId
115
+ );
116
+ }
117
+
118
+ /// @inheritdoc ILSP8NonTransferable
119
+ // solhint-disable not-rely-on-time
120
+ // Transfer-lock windows are inherently time-based; `block.timestamp` is the intended source.
121
+ function isTransferable() public view virtual override returns (bool) {
122
+ if (!transferLockEnabled) return true;
123
+
124
+ bool isTransferLockStartEnabled = transferLockStart != 0;
125
+ bool isTransferLockEndEnabled = transferLockEnd != 0;
126
+
127
+ // If both lock periods are disabled, the token is transferable
128
+ if (!isTransferLockStartEnabled && !isTransferLockEndEnabled) {
129
+ return true;
130
+ }
131
+
132
+ // If the token is non-transferable up to a certain point in time, check if we have passed this period
133
+ if (!isTransferLockStartEnabled && isTransferLockEndEnabled) {
134
+ return transferLockEnd < block.timestamp;
135
+ }
136
+
137
+ // If the token becomes non-transferable starting at a specific point in time, check if we have reached this lock starting period
138
+ if (isTransferLockStartEnabled && !isTransferLockEndEnabled) {
139
+ return transferLockStart > block.timestamp;
140
+ }
141
+
142
+ // This last case checks if we are within the transfer lock period
143
+ return
144
+ transferLockStart > block.timestamp ||
145
+ transferLockEnd < block.timestamp;
146
+ }
147
+ // solhint-enable not-rely-on-time
148
+
149
+ /// @inheritdoc ILSP8NonTransferable
150
+ /// @custom:info The list of addresses holding the `NON_TRANSFERABLE_BYPASS_ROLE` remains populated after the non-transferable feature is switched off.
151
+ function makeTransferable() public virtual override onlyOwner {
152
+ require(transferLockEnabled, LSP8TokenAlreadyTransferable());
153
+
154
+ transferLockEnabled = false;
155
+ transferLockStart = 0;
156
+ transferLockEnd = 0;
157
+
158
+ emit TransferLockPeriodChanged({start: 0, end: 0});
159
+ }
160
+
161
+ /// @inheritdoc ILSP8NonTransferable
162
+ function updateTransferLockPeriod(
163
+ uint256 newTransferLockStart,
164
+ uint256 newTransferLockEnd
165
+ ) public virtual override onlyOwner {
166
+ require(transferLockEnabled, LSP8CannotUpdateTransferLockPeriod());
167
+
168
+ // When transferLockEnd is 0, it means no end time is set (transfers locked indefinitely after transferLockStart)
169
+ // When transferLockStart is 0, it means no start time is set (transfers locked up until transferLockEnd)
170
+ // Allow to make the token always non-transferable, or ensure the end period for locking transfers is always later than the starting period
171
+ require(
172
+ newTransferLockEnd == 0 ||
173
+ newTransferLockEnd >= newTransferLockStart,
174
+ LSP8InvalidTransferLockPeriod()
175
+ );
176
+
177
+ transferLockStart = newTransferLockStart;
178
+ transferLockEnd = newTransferLockEnd;
179
+
180
+ emit TransferLockPeriodChanged({
181
+ start: newTransferLockStart,
182
+ end: newTransferLockEnd
183
+ });
184
+ }
185
+
186
+ /// @notice Checks if a token transfer is allowed based on transferability status.
187
+ /// @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).
188
+ /// @param to The address receiving the token.
189
+ function _nonTransferableCheck(
190
+ address from,
191
+ address to,
192
+ bytes32 /* tokenId */,
193
+ bool /* force */,
194
+ bytes memory /* data */
195
+ ) internal virtual {
196
+ // Allow minting and burning
197
+ if (from == address(0) || to == address(0)) return;
198
+
199
+ // Do not check for addresses exempted from non transferable check
200
+ if (hasRole(NON_TRANSFERABLE_BYPASS_ROLE, from)) return;
201
+
202
+ // transferring tokens only if the transferability status is enabled
203
+ require(isTransferable(), LSP8TransferDisabled());
204
+ }
205
+
206
+ /// @notice Hook called before a token transfer to enforce transfer restrictions.
207
+ /// @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.
208
+ /// @param from The address sending the token.
209
+ /// @param to The address receiving the token.
210
+ /// @param tokenId The unique identifier of the token being transferred.
211
+ /// @param force Whether to force the transfer (passed to _nonTransferableCheck).
212
+ /// @param data Additional data for the transfer (passed to _nonTransferableCheck).
213
+ function _beforeTokenTransfer(
214
+ address from,
215
+ address to,
216
+ bytes32 tokenId,
217
+ bool force,
218
+ bytes memory data
219
+ ) internal virtual override {
220
+ _nonTransferableCheck(from, to, tokenId, force, data);
221
+ super._beforeTokenTransfer(from, to, tokenId, force, data);
222
+ }
223
+
224
+ function _transferOwnership(
225
+ address newOwner
226
+ )
227
+ internal
228
+ virtual
229
+ override(AccessControlExtendedInitAbstract, OwnableUpgradeable)
230
+ {
231
+ // restore default admin hierarchy so a previously-installed custom admin
232
+ // cannot grant NON_TRANSFERABLE_BYPASS_ROLE to new accounts post-transfer
233
+ _setRoleAdmin(NON_TRANSFERABLE_BYPASS_ROLE, DEFAULT_ADMIN_ROLE);
234
+ super._transferOwnership(newOwner);
235
+ }
236
+
237
+ /**
238
+ * @dev This empty reserved space is put in place to allow future versions to add new
239
+ * variables without shifting down storage in the inheritance chain.
240
+ * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
241
+ *
242
+ * @custom:info The size of the `__gap` array is calculated so that the amount of storage used by the contract
243
+ * always adds up to the same number (in this case 50 storage slots).
244
+ */
245
+ uint256[47] private __gap;
246
+ }
@@ -0,0 +1,28 @@
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
+ /// @notice Returns whether the feature to revoke tokens from users is enabled or not.
13
+ function isRevokable() external view returns (bool);
14
+
15
+ /// @notice Disables token revocation permanently.
16
+ /// @dev Can only be called by the contract owner. Prevents further calls to revoke after invocation.
17
+ function disableRevokable() external;
18
+
19
+ /// @notice Revokes `tokenId` from a holder and transfers it to `to`.
20
+ /// @dev Can only be called by an address holding `REVOKER_ROLE`.
21
+ /// The destination must be either the contract owner or an address holding `REVOKER_ROLE`.
22
+ /// The original token holder will be notified via LSP1 universalReceiver.
23
+ /// @param from The address to revoke the token from.
24
+ /// @param to The address receiving the revoked token.
25
+ /// @param tokenId The tokenId to revoke.
26
+ /// @param data Additional data to include in the transfer notification.
27
+ function revoke(address from, address to, bytes32 tokenId, bytes memory data) external;
28
+ }
@@ -0,0 +1,132 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ pragma solidity ^0.8.27;
3
+
4
+ // modules
5
+ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
6
+ import {
7
+ LSP8IdentifiableDigitalAsset
8
+ } from "../../LSP8IdentifiableDigitalAsset.sol";
9
+ import {
10
+ AccessControlExtendedAbstract
11
+ } from "../AccessControlExtended/AccessControlExtendedAbstract.sol";
12
+
13
+ // interfaces
14
+ import {ILSP8Revokable} from "./ILSP8Revokable.sol";
15
+
16
+ // errors
17
+ import {
18
+ AccessControlUnauthorizedAccount
19
+ } from "../AccessControlExtended/AccessControlExtendedErrors.sol";
20
+ import {LSP8RevokableFeatureDisabled} from "./LSP8RevokableErrors.sol";
21
+
22
+ /// @title LSP8RevokableAbstract
23
+ /// @dev Abstract contract implementing revokable functionality for LSP8 tokens.
24
+ /// Allows addresses with the `REVOKER_ROLE` to revoke NFTs from any holder
25
+ /// back to the contract owner or any other address that also has revoke rights.
26
+ ///
27
+ /// Use cases include:
28
+ /// - Memberships: Revoke membership NFTs when they expire or are terminated
29
+ /// - Role badges: Remove role badge NFTs from community members
30
+ /// - Compliance: Freeze or reverse NFTs for regulatory requirements
31
+ /// - Ticketing: Reclaim tickets or access NFTs when conditions are no longer met
32
+ abstract contract LSP8RevokableAbstract is
33
+ ILSP8Revokable,
34
+ LSP8IdentifiableDigitalAsset,
35
+ AccessControlExtendedAbstract
36
+ {
37
+ bool internal _isRevokable;
38
+
39
+ /// @dev keccak256("REVOKER_ROLE")
40
+ bytes32 public constant REVOKER_ROLE =
41
+ 0xce3f34913921da558f105cefb578d87278debbbd073a8d552b5de0d168deee30;
42
+
43
+ constructor(bool isRevokable_) {
44
+ _isRevokable = isRevokable_;
45
+
46
+ if (isRevokable_) {
47
+ _grantRole(REVOKER_ROLE, owner());
48
+ }
49
+ }
50
+
51
+ /// @inheritdoc ILSP8Revokable
52
+ function isRevokable() public view virtual override returns (bool) {
53
+ return _isRevokable;
54
+ }
55
+
56
+ /// @inheritdoc ILSP8Revokable
57
+ /// @custom:warning Once this function is called, any address holding the `REVOKER_ROLE` will be inoperable.
58
+ /// @custom:info The list of addresses holding the `REVOKER_ROLE` remains populated after the revokable feature is switched off.
59
+ function disableRevokable() public virtual override onlyOwner {
60
+ require(isRevokable(), LSP8RevokableFeatureDisabled());
61
+ _isRevokable = false;
62
+ emit RevokableStatusChanged({enabled: false});
63
+ }
64
+
65
+ /// @inheritdoc ILSP8Revokable
66
+ function revoke(
67
+ address from,
68
+ address to,
69
+ bytes32 tokenId,
70
+ bytes memory data
71
+ ) public virtual override onlyRole(REVOKER_ROLE) {
72
+ require(isRevokable(), LSP8RevokableFeatureDisabled());
73
+ require(
74
+ to == owner() || hasRole(REVOKER_ROLE, to),
75
+ AccessControlUnauthorizedAccount(to, REVOKER_ROLE)
76
+ );
77
+
78
+ // We assume revokers are trusted when specifying revocation destinations.
79
+ // Therefore, we bypass LSP1 receiver checks.
80
+ _transfer({
81
+ from: from,
82
+ to: to,
83
+ tokenId: tokenId,
84
+ force: true,
85
+ data: data
86
+ });
87
+ }
88
+
89
+ function supportsInterface(
90
+ bytes4 interfaceId
91
+ )
92
+ public
93
+ view
94
+ virtual
95
+ override(AccessControlExtendedAbstract, LSP8IdentifiableDigitalAsset)
96
+ returns (bool)
97
+ {
98
+ return
99
+ AccessControlExtendedAbstract.supportsInterface(interfaceId) ||
100
+ LSP8IdentifiableDigitalAsset.supportsInterface(interfaceId);
101
+ }
102
+
103
+ /// @dev Overridden function to ensure previous revokers do not persist after contract ownership has been transferred.
104
+ /// The only exception is if the old contract owner had the `REVOKER_ROLE`. This role will be given to the new owner.
105
+ ///
106
+ /// @custom:warning This function clears the entire `REVOKER_ROLE` member set.
107
+ /// - Gas cost scales linearly with the number of addresses with the `REVOKER_ROLE`.
108
+ /// - If the number of addresses with the `REVOKER_ROLE` is large, it might consume a lot of gas,
109
+ /// leading the transaction to approach or exceed the block gas limit and fail.
110
+ /// Consider revoking addresses with the `REVOKER_ROLE` in batches in separate transactions to mitigate this.
111
+ function _transferOwnership(
112
+ address newOwner
113
+ ) internal virtual override(AccessControlExtendedAbstract, Ownable) {
114
+ // restore default admin hierarchy so a previously-installed custom admin
115
+ // cannot grant REVOKER_ROLE to new accounts post-transfer
116
+ _setRoleAdmin(REVOKER_ROLE, DEFAULT_ADMIN_ROLE);
117
+
118
+ // Transfer all roles from old owner to new owner first (including the `REVOKER_ROLE`)
119
+ // before clearing the list of revokers.
120
+ super._transferOwnership(newOwner);
121
+
122
+ address[] memory revokers = getRoleMembers(REVOKER_ROLE);
123
+
124
+ for (uint256 ii = 0; ii < revokers.length; ++ii) {
125
+ // Exclude the new owner from the list of revokers to delete.
126
+ address revoker = revokers[ii];
127
+ if (revoker == newOwner) continue;
128
+
129
+ _revokeRole(REVOKER_ROLE, revoker);
130
+ }
131
+ }
132
+ }
@@ -0,0 +1,4 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ pragma solidity ^0.8.27;
3
+
4
+ error LSP8RevokableFeatureDisabled();