diff --git a/src/names/INames.sol b/src/names/INames.sol new file mode 100644 index 0000000..ffab081 --- /dev/null +++ b/src/names/INames.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import { IERC721Enumerable } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol"; + +/// @title INames - ERC721 Name Registration Interface +/// @notice Interface for a decentralized name registration system using NFTs +/// @dev Extends IERC721Enumerable to provide name registration and management functionality +interface INames is IERC721Enumerable { + /// @notice Emitted when a new name is minted + /// @param user Address of the user minting the name + /// @param tokenId ID of the minted NFT + /// @param name The registered name + event NameMinted(address indexed user, uint256 indexed tokenId, string indexed name); + + /// @notice Emitted when the contract URI is updated + /// @param newURI New URI for contract metadata + event ContractURIUpdated(string newURI); + + /// @notice Emitted when the token URI is updated + /// @param newURI New URI for token metadata + event TokenURIUpdated(string newURI); + + /// @notice Error thrown when name exceeds maximum allowed length + error LongName(); + + /// @notice Error thrown when name is shorter than minimum required length + error ShortName(); + + /// @notice Error thrown when attempting to register an already taken name + /// @param name The name that was attempted to be registered + /// @param owner Current owner of the name + error NameAlreadyTaken(string name, address owner); + + /// @notice Error thrown when a user attempts to register multiple names + /// @param user Address of the user + /// @param name Current name of the user + error UserAlreadyHasName(address user, string name); + + /// @notice Mints a new name token + /// @dev Creates a new NFT representing the name ownership + /// @param name The name to register + /// @return mintedTokenId The ID of the newly minted NFT + /// @custom:throws LongName If name length exceeds maximum + /// @custom:throws ShortName If name length is below minimum + /// @custom:throws NameAlreadyTaken If name is already registered + /// @custom:throws UserAlreadyHasName If caller already owns a name + function mintName(string memory name) external returns (uint256 mintedTokenId); + + /// @notice Retrieves the owner address for a given name + /// @param name The name to query + /// @return user The address that owns the name (zero address if unregistered) + function nameToUser(string memory name) external view returns (address user); + + /// @notice Retrieves the registered name for a given user + /// @param user The address to query + /// @return name The user's registered name (empty string if none) + function userToName(address user) external view returns (string memory name); + + /// @notice Retrieves the name associated with a token ID + /// @param tokenId The ID of the name token + /// @return name The name associated with the token + function tokenIdToName(uint256 tokenId) external view returns (string memory name); + + /// @notice Checks if a name is available for registration + /// @param name The name to check + /// @return bool True if the name is available, false otherwise + function isAvailable(string memory name) external view returns (bool); + + /// @notice Checks if an address has a registered name + /// @param user The address to check + /// @return bool True if the address has a name, false otherwise + function hasName(address user) external view returns (bool); + + /// @notice Returns the contract-level metadata URI + /// @return URI string for contract metadata + function contractURI() external view returns (string memory); + + /// @notice The minimum allowed length for names + /// @return uint256 Minimum name length (3) + function MINIMAL_NAME_LENGTH() external view returns (uint256); + + /// @notice The maximum allowed length for names + /// @return uint256 Maximum name length (30) + function MAX_NAME_LENGTH() external view returns (uint256); + + /// @notice Updates the contract-level metadata URI + /// @param _newURI New URI string for contract metadata + function updateContractURI(string memory _newURI) external; + + /// @notice Updates the token-level metadata URI + /// @param _newURI New URI string for token metadata + function updateTokenURI(string memory _newURI) external; +} diff --git a/src/names/Names.sol b/src/names/Names.sol new file mode 100644 index 0000000..d388c79 --- /dev/null +++ b/src/names/Names.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import { INames } from "./INames.sol"; +import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import { ERC721Enumerable } from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; +import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +/// @title Names - Decentralized Name Registration System +/// @author [Your Name/Organization] +/// @notice This contract implements a decentralized name registration system where users can mint unique names as NFTs +/// @dev Extends ERC721Enumerable for enumerable NFT functionality and implements custom INames interface +/// @custom:security-contact security@yourproject.com +contract Names is ERC721Enumerable, INames, Ownable { + /// @notice URI for contract metadata + /// @dev Used for OpenSea and other marketplaces to display collection information + /// @inheritdoc INames + string public contractURI; + + /// @notice Base URI for token metadata + /// @dev Used as the base for all token URIs + string private _tokenURI; + + /// @notice Counter for generating unique token IDs + /// @dev Increments by 1 for each new mint + uint256 private _tokenIds; + + /// @notice Minimum allowed length for a name + /// @dev Prevents extremely short names + /// @inheritdoc INames + uint256 public constant MINIMAL_NAME_LENGTH = 3; + + /// @notice Maximum allowed length for a name + /// @dev Prevents excessive gas costs and maintains reasonable name lengths + /// @inheritdoc INames + uint256 public constant MAX_NAME_LENGTH = 30; + + // State mappings + /// @notice Maps name strings to their owner addresses + /// @dev Primary lookup for name ownership + /// @inheritdoc INames + mapping(string name => address user) public nameToUser; + /// @inheritdoc INames + mapping(address user => string name) public userToName; + /// @inheritdoc INames + mapping(uint256 tokenId => string name) public tokenIdToName; + + /// @notice Initializes the Names contract with basic metadata + /// @dev Sets initial URIs and configures base contract parameters + constructor() Ownable(msg.sender) ERC721("Plasa Names", "NAME") ERC721Enumerable() { + contractURI = "some-contract-uri"; + _tokenURI = "some-token-uri"; + } + + /// @notice Updates the contract-level metadata URI + /// @dev Only callable by contract owner + /// @param _newURI New URI for contract metadata + /// @inheritdoc INames + function updateContractURI(string memory _newURI) public onlyOwner { + contractURI = _newURI; + emit ContractURIUpdated(_newURI); + } + + /// @inheritdoc INames + function updateTokenURI(string memory _newURI) public onlyOwner { + _tokenURI = _newURI; + emit TokenURIUpdated(_newURI); + } + + /// @inheritdoc IERC721Metadata + function tokenURI(uint256 /* tokenId */) public view override returns (string memory) { + return _tokenURI; + } + + /// @notice Internal function to mint a new name token + /// @dev Handles validation and state updates for name minting + /// @param _user Address to mint the name for + /// @param _name Name to be minted + /// @return mintedTokenId The ID of the newly minted token + /// @custom:security Validates name availability and length constraints + function _mintName(address _user, string memory _name) internal returns (uint256 mintedTokenId) { + if (hasName(_user)) { + revert UserAlreadyHasName(_user, userToName[_user]); + } + + if (!isAvailable(_name)) { + revert NameAlreadyTaken(_name, nameToUser[_name]); + } + + uint256 nameLength = bytes(_name).length; + + if (nameLength < MINIMAL_NAME_LENGTH) { + revert ShortName(); + } + + if (nameLength > MAX_NAME_LENGTH) { + revert LongName(); + } + + mintedTokenId = _tokenIds++; + _mint(_user, mintedTokenId); + + nameToUser[_name] = _user; + userToName[_user] = _name; + tokenIdToName[mintedTokenId] = _name; + + emit NameMinted(_user, mintedTokenId, _name); + } + + /// @inheritdoc INames + function mintName(string memory name) public returns (uint256 mintedTokenId) { + return _mintName(msg.sender, name); + } + + /// @inheritdoc INames + function isAvailable(string memory name) public view returns (bool) { + return nameToUser[name] == address(0); + } + + /// @inheritdoc INames + function hasName(address user) public view returns (bool) { + return balanceOf(user) != 0; + } +}