// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.18; import "./SmartAssetBase.sol"; import "../Interfaces/ISmartAssetRecoverable.sol"; import "../Utilities/BytesUtils.sol"; /** * @title SmartAssetRecoverable. * @author Arianee - Dynamic NFTs for real-world use cases and consumer engagement (www.arianee.org). */ abstract contract SmartAssetRecoverable is SmartAssetBase, ISmartAssetRecoverable { /** * @notice RECOVERER_ROLE: those who can validate a recovery request to return a token to the issuer. */ bytes32 public constant RECOVERER_ROLE = keccak256("RECOVERER_ROLE"); /** * @notice HYDRATE_OTHER_PARAM_INDEX_TOKEN_URI: the index of recoveryWindowSec in the otherParams array of the hydrateToken function. * NOTE: This index must be unique across all contracts that inherit from SmartAssetBase. */ uint256 public constant HYDRATE_OTHER_PARAM_INDEX_RECOVERY_WINDOW_SEC = 1; /** * @notice Mapping from token id to a boolean indicating whether the token is waiting for recovery or not. */ mapping(uint256 => bool) internal idToRecoveryRequest; /** * @notice Mapping from token id to the value of the recovery window in seconds. */ mapping(uint256 => uint256) internal idToRecoveryWindowSec; /** * @notice Mapping from token id to the timestamp of the first transfer. */ mapping(uint256 => uint256) internal idToFirstTransferTimestamp; /** * @notice This emits when a token recovery configuration is set. */ event TokenRecoveryConfigured(uint256 indexed tokenId, uint256 recoveryWindowSec); /** * @notice This emits when a issuer request a token recovery. */ event TokenRecoveryRequestUpdated(uint256 indexed tokenId, bool active); /** * @notice This emits when a token is recovered to the issuer. */ event TokenRecovered(uint256 indexed tokenId); constructor() { _grantRole(RECOVERER_ROLE, msg.sender); } /** * @notice Recover a token to the issuer without any recovery request. * @dev This function can be used to recover a token only if the recovery window is not over. * @param tokenId The token ID. */ function recoverToken(uint256 tokenId) external isIssuer(tokenId) { require( block.timestamp <= (idToFirstTransferTimestamp[tokenId] + idToRecoveryWindowSec[tokenId]), "SmartAssetRecoverable: The token recovery window is over" ); _transferTokenToIssuer(tokenId); } /** * @notice Validate a recovery request and transfer the token to the issuer. * @param tokenId The token ID. */ function validateRecoveryRequest(uint256 tokenId) external onlyRole(RECOVERER_ROLE) { require(isWaitingForRecovery(tokenId), "SmartAssetRecoverable: No active recovery request for this token"); _transferTokenToIssuer(tokenId); _updateRecoveryRequest(tokenId, false); } /** * @notice Try to retrieve the recovery window and the first transfer timestamp of a token. * @param tokenId The token ID. * @return recoveryWindowSec The recovery window in seconds. * @return firstTransferTimestamp The timestamp of the first transfer if any. */ function recoveryWindowOf(uint256 tokenId) external view returns (uint256 recoveryWindowSec, uint256 firstTransferTimestamp) { return (idToRecoveryWindowSec[tokenId], idToFirstTransferTimestamp[tokenId]); } /** * @notice Activate or deactivate a recovery request for a given token ID. * @param tokenId The token ID. * @param active A flag to indicate whether the recovery request is active or not. */ function updateRecoveryRequest(uint256 tokenId, bool active) public isIssuer(tokenId) { _updateRecoveryRequest(tokenId, active); } function _updateRecoveryRequest(uint256 tokenId, bool active) internal { idToRecoveryRequest[tokenId] = active; emit TokenRecoveryRequestUpdated(tokenId, active); } /** * @notice See {SmartAssetBase-hydrateToken} */ function hydrateToken(TokenHydratationParams memory tokenHydratationParams) public virtual override onlyRole(MINTER_ROLE) whenNotPaused { uint256 tokenId = tokenHydratationParams.tokenId; require( tokenHydratationParams.otherParams.length > HYDRATE_OTHER_PARAM_INDEX_RECOVERY_WINDOW_SEC, "SmartAssetRecoverable: HYDRATE_OTHER_PARAM_INDEX_RECOVERY_WINDOW_SEC is missing" ); bytes memory recoveryWindowSecBytes = tokenHydratationParams.otherParams[HYDRATE_OTHER_PARAM_INDEX_RECOVERY_WINDOW_SEC]; require(recoveryWindowSecBytes.length > 0, "SmartAssetRecoverable: A non-zero uint256 is expected for the recoveryWindowSec argument"); uint256 recoveryWindowSec = BytesLib.toUint256(recoveryWindowSecBytes, 0); idToRecoveryWindowSec[tokenId] = recoveryWindowSec; emit TokenRecoveryConfigured(tokenId, recoveryWindowSec); super.hydrateToken(tokenHydratationParams); } /** * @notice Check whether a token is waiting for recovery or not. * @param tokenId The token ID. * @return A boolean indicating whether the token is waiting for recovery or not. */ function isWaitingForRecovery(uint256 tokenId) public view returns (bool) { return idToRecoveryRequest[tokenId]; } /** * @notice Transfer the token to the issuer. * NOTE: This function does not perform any check. */ function _transferTokenToIssuer(uint256 tokenId) internal { super._approve(_msgSender(), tokenId); address owner = super.ownerOf(tokenId); address issuer = super.footprintOf(tokenId).issuer; super.safeTransferFrom(owner, issuer, tokenId); emit TokenRecovered(tokenId); } /** * @dev See {SmartAssetBase-_afterFirstTokenTransfer}. */ function _afterFirstTokenTransfer(uint256 tokenId) internal virtual override { idToFirstTransferTimestamp[tokenId] = block.timestamp; super._afterFirstTokenTransfer(tokenId); } /** * @dev See {ERC721-_burn}. */ function _burn(uint256 tokenId) internal virtual override { super._burn(tokenId); delete idToRecoveryRequest[tokenId]; delete idToRecoveryWindowSec[tokenId]; delete idToFirstTransferTimestamp[tokenId]; } /** * @dev See {IERC165-supportsInterface}. */ function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { return interfaceId == type(ISmartAssetRecoverable).interfaceId || super.supportsInterface(interfaceId); } }