// SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.4; // modules import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import { LSP7DigitalAssetInitAbstract } from "../../LSP7DigitalAssetInitAbstract.sol"; import { AccessControlExtendedInitAbstract } from "../AccessControlExtended/AccessControlExtendedInitAbstract.sol"; // interfaces import {ILSP7NonTransferable} from "./ILSP7NonTransferable.sol"; // errors import { LSP7TransferDisabled, LSP7InvalidTransferLockPeriod, LSP7CannotUpdateTransferLockPeriod, LSP7TokenAlreadyTransferable } from "./LSP7NonTransferableErrors.sol"; /// @title LSP7NonTransferableInitAbstract /// @dev Abstract contract implementing non-transferable LSP7 token functionality with transfer lock periods /// and support to bypass non-transferable checks through a role. abstract contract LSP7NonTransferableInitAbstract is ILSP7NonTransferable, LSP7DigitalAssetInitAbstract, AccessControlExtendedInitAbstract { /// @dev keccak256("NON_TRANSFERABLE_BYPASS_ROLE") bytes32 public constant NON_TRANSFERABLE_BYPASS_ROLE = 0xb4b3a36d7c2b72add3151898671aaed843238e580f7d6d4bc5077ce2023b0659; /// @inheritdoc ILSP7NonTransferable uint256 public transferLockStart; /// @inheritdoc ILSP7NonTransferable uint256 public transferLockEnd; /// @inheritdoc ILSP7NonTransferable bool public transferLockEnabled; /// @notice Initializes the LSP7NonTransferable contract with base token params, allowlist, and transfer settings. /// @dev Initializes the LSP7Allowlist (which initializes LSP7DigitalAsset) and sets the lock period. /// @param name_ The name of the token. /// @param symbol_ The symbol of the token. /// @param newOwner_ The owner of the contract, added to the allowlist. /// @param lsp4TokenType_ The token type (see LSP4). /// @param isNonDivisible_ Whether the token is non-divisible. /// @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. function __LSP7NonTransferable_init( string memory name_, string memory symbol_, address newOwner_, uint256 lsp4TokenType_, bool isNonDivisible_, uint256 transferLockStart_, uint256 transferLockEnd_ ) internal virtual onlyInitializing { LSP7DigitalAssetInitAbstract._initialize( name_, symbol_, newOwner_, lsp4TokenType_, isNonDivisible_ ); __AccessControlExtended_init(); __LSP7NonTransferable_init_unchained( transferLockStart_, transferLockEnd_ ); } /// @notice Unchained initializer for the 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. function __LSP7NonTransferable_init_unchained( uint256 transferLockStart_, uint256 transferLockEnd_ ) internal virtual onlyInitializing { require( transferLockEnd_ == 0 || transferLockEnd_ >= transferLockStart_, LSP7InvalidTransferLockPeriod() ); transferLockStart = transferLockStart_; transferLockEnd = transferLockEnd_; transferLockEnabled = true; emit TransferLockPeriodChanged(transferLockStart_, transferLockEnd_); _grantRole(NON_TRANSFERABLE_BYPASS_ROLE, owner()); } function supportsInterface( bytes4 interfaceId ) public view virtual override( AccessControlExtendedInitAbstract, LSP7DigitalAssetInitAbstract ) returns (bool) { return AccessControlExtendedInitAbstract.supportsInterface(interfaceId) || LSP7DigitalAssetInitAbstract.supportsInterface(interfaceId); } /// @inheritdoc ILSP7NonTransferable 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; } /// @inheritdoc ILSP7NonTransferable /// @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, LSP7TokenAlreadyTransferable()); transferLockEnabled = false; transferLockStart = 0; transferLockEnd = 0; emit TransferLockPeriodChanged({start: 0, end: 0}); } /// @inheritdoc ILSP7NonTransferable function updateTransferLockPeriod( uint256 newTransferLockStart, uint256 newTransferLockEnd ) public virtual override onlyOwner { require(transferLockEnabled, LSP7CannotUpdateTransferLockPeriod()); // 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, LSP7InvalidTransferLockPeriod() ); 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 {LSP7TransferDisabled} if the token is non-transferable and the destination is not address(0). /// @param to The address receiving the tokens. function _nonTransferableCheck( address from, address to, uint256 /* amount */, 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(), LSP7TransferDisabled()); } /// @notice Hook called before a token transfer to enforce transfer restrictions. /// @dev Bypasses transfer restrictions for addresses in the allowlist, allowing them to transfer tokens even when {isTransferable} returns false. For non-allowlisted addresses, applies non-transferable checks. /// @param from The address sending the tokens. /// @param to The address receiving the tokens. /// @param amount The amount of tokens 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, uint256 amount, bool force, bytes memory data ) internal virtual override { _nonTransferableCheck(from, to, amount, force, data); super._beforeTokenTransfer(from, to, amount, force, data); } function _transferOwnership( address newOwner ) internal virtual override(AccessControlExtendedInitAbstract, OwnableUpgradeable) { // 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); } /** * @dev This empty reserved space is put in place to allow future versions to add new * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps * * @custom:info The size of the `__gap` array is calculated so that the amount of storage used by the contract * always adds up to the same number (in this case 50 storage slots). */ uint256[47] private __gap; }