// SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.27; // modules import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import { LSP8IdentifiableDigitalAsset } from "../../LSP8IdentifiableDigitalAsset.sol"; import { AccessControlExtendedAbstract } from "../AccessControlExtended/AccessControlExtendedAbstract.sol"; // interfaces import {ILSP8NonTransferable} from "./ILSP8NonTransferable.sol"; // errors import { LSP8TransferDisabled, LSP8InvalidTransferLockPeriod, LSP8CannotUpdateTransferLockPeriod, LSP8TokenAlreadyTransferable } from "./LSP8NonTransferableErrors.sol"; /// @title LSP8NonTransferableAbstract /// @dev Abstract contract implementing non-transferable LSP8 token functionality with transfer lock periods and role-based bypass support. abstract contract LSP8NonTransferableAbstract is ILSP8NonTransferable, LSP8IdentifiableDigitalAsset, AccessControlExtendedAbstract { /// @dev keccak256("NON_TRANSFERABLE_BYPASS_ROLE") bytes32 public constant NON_TRANSFERABLE_BYPASS_ROLE = 0xb4b3a36d7c2b72add3151898671aaed843238e580f7d6d4bc5077ce2023b0659; /// @inheritdoc ILSP8NonTransferable uint256 public transferLockStart; /// @inheritdoc ILSP8NonTransferable uint256 public transferLockEnd; /// @inheritdoc ILSP8NonTransferable bool public transferLockEnabled; /// @notice Initializes the contract with lock period. /// @param transferLockStart_ The start timestamp of the transfer lock period, 0 to disable. /// @param transferLockEnd_ The end timestamp of the transfer lock period, 0 to disable. constructor(uint256 transferLockStart_, uint256 transferLockEnd_) { require( transferLockEnd_ == 0 || transferLockEnd_ >= transferLockStart_, LSP8InvalidTransferLockPeriod() ); transferLockStart = transferLockStart_; transferLockEnd = transferLockEnd_; transferLockEnabled = true; emit TransferLockPeriodChanged(transferLockStart_, transferLockEnd_); _grantRole(NON_TRANSFERABLE_BYPASS_ROLE, owner()); } function supportsInterface( bytes4 interfaceId ) public view virtual override(AccessControlExtendedAbstract, LSP8IdentifiableDigitalAsset) returns (bool) { return AccessControlExtendedAbstract.supportsInterface(interfaceId) || LSP8IdentifiableDigitalAsset.supportsInterface(interfaceId); } /// @inheritdoc ILSP8NonTransferable // solhint-disable not-rely-on-time // Transfer-lock windows are inherently time-based; `block.timestamp` is the intended source. function isTransferable() public view virtual override returns (bool) { if (!transferLockEnabled) return true; bool isTransferLockStartEnabled = transferLockStart != 0; bool isTransferLockEndEnabled = transferLockEnd != 0; // If both lock periods are disabled, the token is transferable if (!isTransferLockStartEnabled && !isTransferLockEndEnabled) { return true; } // If the token is non-transferable up to a certain point in time, check if we have passed this period if (!isTransferLockStartEnabled && isTransferLockEndEnabled) { return transferLockEnd < block.timestamp; } // If the token becomes non-transferable starting at a specific point in time, check if we have reached this lock starting period if (isTransferLockStartEnabled && !isTransferLockEndEnabled) { return transferLockStart > block.timestamp; } // This last case checks if we are within the transfer lock period return transferLockStart > block.timestamp || transferLockEnd < block.timestamp; } // solhint-enable not-rely-on-time /// @inheritdoc ILSP8NonTransferable /// @custom:info The list of addresses holding the `NON_TRANSFERABLE_BYPASS_ROLE` remains populated after the non-transferable feature is switched off. function makeTransferable() public virtual override onlyOwner { require(transferLockEnabled, LSP8TokenAlreadyTransferable()); transferLockEnabled = false; transferLockStart = 0; transferLockEnd = 0; emit TransferLockPeriodChanged({start: 0, end: 0}); } /// @inheritdoc ILSP8NonTransferable function updateTransferLockPeriod( uint256 newTransferLockStart, uint256 newTransferLockEnd ) public virtual override onlyOwner { require(transferLockEnabled, LSP8CannotUpdateTransferLockPeriod()); // When transferLockEnd is 0, it means no end time is set (transfers locked indefinitely after transferLockStart) // When transferLockStart is 0, it means no start time is set (transfers locked up until transferLockEnd) // Allow to make the token always non-transferable, or ensure the end period for locking transfers is always later than the starting period require( newTransferLockEnd == 0 || newTransferLockEnd >= newTransferLockStart, LSP8InvalidTransferLockPeriod() ); transferLockStart = newTransferLockStart; transferLockEnd = newTransferLockEnd; emit TransferLockPeriodChanged({ start: newTransferLockStart, end: newTransferLockEnd }); } /// @notice Checks if a token transfer is allowed based on transferability status. /// @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). /// @param to The address receiving the token. function _nonTransferableCheck( address from, address to, bytes32, /* tokenId */ bool, /* force */ bytes memory /* data */ ) internal virtual { // Allow minting and burning if (from == address(0) || to == address(0)) return; // Do not check for addresses exempted from non transferable check if (hasRole(NON_TRANSFERABLE_BYPASS_ROLE, from)) return; // transferring tokens only if the transferability status is enabled require(isTransferable(), LSP8TransferDisabled()); } /// @notice Hook called before a token transfer to enforce transfer restrictions. /// @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. /// @param from The address sending the token. /// @param to The address receiving the token. /// @param tokenId The unique identifier of the token being transferred. /// @param force Whether to force the transfer (passed to _nonTransferableCheck). /// @param data Additional data for the transfer (passed to _nonTransferableCheck). function _beforeTokenTransfer( address from, address to, bytes32 tokenId, bool force, bytes memory data ) internal virtual override { _nonTransferableCheck(from, to, tokenId, force, data); super._beforeTokenTransfer(from, to, tokenId, force, data); } function _transferOwnership( address newOwner ) internal virtual override(AccessControlExtendedAbstract, Ownable) { // restore default admin hierarchy so a previously-installed custom admin // cannot grant NON_TRANSFERABLE_BYPASS_ROLE to new accounts post-transfer _setRoleAdmin(NON_TRANSFERABLE_BYPASS_ROLE, DEFAULT_ADMIN_ROLE); super._transferOwnership(newOwner); } }