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