// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.0; import "@openzeppelin/contracts/utils/Strings.sol"; import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import "./IERC4671.sol"; import "./IERC4671Metadata.sol"; import "./IERC4671Enumerable.sol"; abstract contract ERC4671 is IERC4671, IERC4671Metadata, IERC4671Enumerable, ERC165 { // Token data struct Token { address issuer; address owner; bool valid; } // Mapping from tokenId to token mapping(uint256 => Token) private _tokens; // Mapping from owner to token ids mapping(address => uint256[]) private _indexedTokenIds; // Mapping from token id to index mapping(address => mapping(uint256 => uint256)) private _tokenIdIndex; // Mapping from owner to number of valid tokens mapping(address => uint256) private _numberOfValidTokens; // Token name string private _name; // Token symbol string private _symbol; // Total number of tokens emitted uint256 private _emittedCount; // Total number of token holders uint256 private _holdersCount; // Contract creator address private _creator; constructor (string memory name_, string memory symbol_) { _name = name_; _symbol = symbol_; _creator = msg.sender; } /// @notice Count all tokens assigned to an owner /// @param owner Address for whom to query the balance /// @return Number of tokens owned by `owner` function balanceOf(address owner) public view virtual override returns (uint256) { return _indexedTokenIds[owner].length; } /// @notice Get owner of a token /// @param tokenId Identifier of the token /// @return Address of the owner of `tokenId` function ownerOf(uint256 tokenId) public view virtual override returns (address) { return _getTokenOrRevert(tokenId).owner; } /// @notice Check if a token hasn't been revoked /// @param tokenId Identifier of the token /// @return True if the token is valid, false otherwise function isValid(uint256 tokenId) public view virtual override returns (bool) { return _getTokenOrRevert(tokenId).valid; } /// @notice Check if an address owns a valid token in the contract /// @param owner Address for whom to check the ownership /// @return True if `owner` has a valid token, false otherwise function hasValid(address owner) public view virtual override returns (bool) { return _numberOfValidTokens[owner] > 0; } /// @return Descriptive name of the tokens in this contract function name() public view virtual override returns (string memory) { return _name; } /// @return An abbreviated name of the tokens in this contract function symbol() public view virtual override returns (string memory) { return _symbol; } /// @notice URI to query to get the token's metadata /// @param tokenId Identifier of the token /// @return URI for the token function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { _getTokenOrRevert(tokenId); bytes memory baseURI = bytes(_baseURI()); if (baseURI.length > 0) { return string(abi.encodePacked( baseURI, Strings.toHexString(tokenId, 32) )); } return ""; } /// @return emittedCount Number of tokens emitted function emittedCount() public view override returns (uint256) { return _emittedCount; } /// @return holdersCount Number of token holders function holdersCount() public view override returns (uint256) { return _holdersCount; } /// @notice Get the tokenId of a token using its position in the owner's list /// @param owner Address for whom to get the token /// @param index Index of the token /// @return tokenId of the token function tokenOfOwnerByIndex(address owner, uint256 index) public view virtual override returns (uint256) { uint256[] storage ids = _indexedTokenIds[owner]; require(index < ids.length, "Token does not exist"); return ids[index]; } /// @notice Get a tokenId by it's index, where 0 <= index < total() /// @param index Index of the token /// @return tokenId of the token function tokenByIndex(uint256 index) public view virtual override returns (uint256) { return index; } function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { return interfaceId == type(IERC4671).interfaceId || interfaceId == type(IERC4671Metadata).interfaceId || interfaceId == type(IERC4671Enumerable).interfaceId || super.supportsInterface(interfaceId); } /// @notice Prefix for all calls to tokenURI /// @return Common base URI for all token function _baseURI() internal pure virtual returns (string memory) { return ""; } /// @notice Mark the token as revoked /// @param tokenId Identifier of the token function _revoke(uint256 tokenId) internal virtual { Token storage token = _getTokenOrRevert(tokenId); require(token.valid, "Token is already invalid"); token.valid = false; assert(_numberOfValidTokens[token.owner] > 0); _numberOfValidTokens[token.owner] -= 1; emit Revoked(token.owner, tokenId); } /// @notice Mint a new token /// @param owner Address for whom to assign the token /// @return tokenId Identifier of the minted token function _mint(address owner) internal virtual returns (uint256 tokenId) { tokenId = _emittedCount; _mintUnsafe(owner, tokenId, true); emit Minted(owner, tokenId); _emittedCount += 1; } /// @notice Mint a given tokenId /// @param owner Address for whom to assign the token /// @param tokenId Token identifier to assign to the owner /// @param valid Boolean to assert of the validity of the token function _mintUnsafe(address owner, uint256 tokenId, bool valid) internal { require(_tokens[tokenId].owner == address(0), "Cannot mint an assigned token"); if (_indexedTokenIds[owner].length == 0) { _holdersCount += 1; } _tokens[tokenId] = Token(msg.sender, owner, valid); _tokenIdIndex[owner][tokenId] = _indexedTokenIds[owner].length; _indexedTokenIds[owner].push(tokenId); if (valid) { _numberOfValidTokens[owner] += 1; } } /// @return True if the caller is the contract's creator, false otherwise function _isCreator() internal view virtual returns (bool) { return msg.sender == _creator; } /// @notice Retrieve a token or revert if it does not exist /// @param tokenId Identifier of the token /// @return The Token struct function _getTokenOrRevert(uint256 tokenId) internal view virtual returns (Token storage) { Token storage token = _tokens[tokenId]; require(token.owner != address(0), "Token does not exist"); return token; } /// @notice Remove a token /// @param tokenId Token identifier to remove function _removeToken(uint256 tokenId) internal virtual { Token storage token = _getTokenOrRevert(tokenId); _removeFromUnorderedArray(_indexedTokenIds[token.owner], _tokenIdIndex[token.owner][tokenId]); if (_indexedTokenIds[token.owner].length == 0) { assert(_holdersCount > 0); _holdersCount -= 1; } if (token.valid) { assert(_numberOfValidTokens[token.owner] > 0); _numberOfValidTokens[token.owner] -= 1; } delete _tokens[tokenId]; } /// @notice Removes an entry in an array by its index /// @param array Array for which to remove the entry /// @param index Index of the entry to remove function _removeFromUnorderedArray(uint256[] storage array, uint256 index) internal { require(index < array.length, "Trying to delete out of bound index"); if (index != array.length - 1) { array[index] = array[array.length - 1]; } array.pop(); } }