@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.
- package/LICENSE +201 -0
- package/README.md +99 -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 +116 -0
- package/artifacts/ILSP8Revokable.json +112 -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 +1781 -0
- package/artifacts/LSP8CustomizableTokenInit.json +1776 -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 +1373 -0
- package/artifacts/LSP8NonTransferableInitAbstract.json +1381 -0
- package/artifacts/LSP8RevokableAbstract.json +1354 -0
- package/artifacts/LSP8RevokableInitAbstract.json +1362 -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 +54 -0
- package/contracts/extensions/LSP8NonTransferable/LSP8NonTransferableAbstract.sol +199 -0
- package/contracts/extensions/LSP8NonTransferable/LSP8NonTransferableErrors.sol +14 -0
- package/contracts/extensions/LSP8NonTransferable/LSP8NonTransferableInitAbstract.sol +255 -0
- package/contracts/extensions/LSP8Revokable/ILSP8Revokable.sol +37 -0
- package/contracts/extensions/LSP8Revokable/LSP8RevokableAbstract.sol +140 -0
- package/contracts/extensions/LSP8Revokable/LSP8RevokableErrors.sol +4 -0
- package/contracts/extensions/LSP8Revokable/LSP8RevokableInitAbstract.sol +192 -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 +8351 -136
- package/dist/abi.d.cts +12122 -266
- package/dist/abi.d.mts +12122 -266
- package/dist/abi.d.ts +12122 -266
- package/dist/abi.mjs +8335 -136
- 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,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
|
+
}
|