// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.18; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "../../Utilities/MutableERC2771ContextUpgradeable.sol"; import "../../Interfaces/IOwnershipRegistry.sol"; import "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165CheckerUpgradeable.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "../../Utilities/Strings.sol"; /** * @title OwnershipRegistry. * @author Arianee - Dynamic NFTs for real-world use cases and consumer engagement (www.arianee.org). * @notice The OwnershipRegistry contract is part of the Arianee protocol. Its provides a permissionless registry of token ownership per identity to enable cross-chain interoperability. * @dev See: https://docs.arianee.org */ contract OwnershipRegistry is OwnableUpgradeable, MutableERC2771ContextUpgradeable, UUPSUpgradeable, IOwnershipRegistry { using strings for *; /** * @notice Arianee Top-Level Identity (TLI). */ string public constant ARIANEE_TLI = ".arianee.eth"; /** * @notice Arianee account address. */ address public arianeeAddress; /** * @notice Mapping from token to ENS identity. */ mapping(address => string) tokenToEns; /** * @notice Mapping from ENS identity to tokens. */ mapping(string => address[]) ensToTokens; /** * @notice Mapping from issuer to ENS identity. */ mapping(address => string) issuerToEns; /** * @notice Mapping from ENS identity to issuer. */ mapping(string => address[]) ensToIssuer; /** * @notice Mapping from token to token stamp. */ mapping(address => bytes[]) tokenToStamp; /** * @notice This emits when a token is registered. */ event TokenRegistered(address indexed contractAddress, string indexed ensIndex, string ens); /** * @notice This emits when an issuer is registered. */ event IssuerRegistered(address indexed issuerAddress, string indexed ensIndex, string ens); /** * @notice This emits when a token is stamped by Arianee. * NOTE: This event is used internally by the "Arianee SAS" to save additional data on-chain for a specific token (e.g. gasless transfer, indexation, etc.) * You can safely ignore this event if you are developing a third-party application. */ event TokenStamped(address indexed contractAddress, bytes[] params); /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } /** * @notice Initializer of the contract, used instead of constructor to let the proxy pattern works. * @param arianeeAddress_ Arianee account address. */ function initialize(address arianeeAddress_, address trustedForwarder_) public initializer { __Ownable_init_unchained(); __MutableERC2771ContextUpgradeable_init_unchained(trustedForwarder_); __UUPSUpgradeable_init(); arianeeAddress = arianeeAddress_; } /** * @notice Check if the caller is the owner of the target token contract. * @param tokenAddress The address of the token contract. */ modifier onlyTokenContractOwner(address tokenAddress) { require( _isTokenContractOwner(tokenAddress, _msgSender()), "OwnershipRegistry: The caller is not the owner of the target token contract" ); _; } /** * @notice Check if the caller is Arianee. */ modifier onlyArianee() { require(_msgSender() == arianeeAddress, "OwnershipRegistry: The caller is not Arianee"); _; } /** * @notice Register a token contract. * NOTE: This function can only be called by the owner of the target token contract. * @param contractAddress address of the token contract. * @param ens ENS identity to link to this token contract. */ function registerToken( address contractAddress, string memory ens ) external onlyTokenContractOwner(contractAddress) { _registerToken(contractAddress, ens); } /** * @notice Force to register a token contract. * NOTE: This function can only be called by Arianee and is used if the token to register is not matching onlyTokenContractOwner requirement * @param contractAddress address of the ERC721 contract to register * @param ens ENS identity to link to this token contract. */ function forceRegisterToken(address contractAddress, string memory ens) public onlyArianee { require(ERC165CheckerUpgradeable.supportsInterface(contractAddress, type(Ownable).interfaceId) == false, "OwnershipRegistry: The token must be registered by its owner"); _registerToken(contractAddress, ens); } /** * @notice Register an issuer. * @param ens ENS identity to link to this issuer. */ function registerIssuer(string memory ens) public { require(ens.toSlice().endsWith(ARIANEE_TLI.toSlice()), "OwnershipRegistry: The ENS identity must be an Arianee subdomain"); if (bytes(issuerToEns[_msgSender()]).length > 0) { string memory oldEns = issuerToEns[_msgSender()]; for (uint256 i = 0; i < ensToIssuer[oldEns].length; i++) { if (ensToIssuer[oldEns][i] == _msgSender()) { ensToIssuer[oldEns][i] = ensToIssuer[oldEns][ensToIssuer[oldEns].length - 1]; ensToIssuer[oldEns].pop(); } } issuerToEns[_msgSender()] = ""; } issuerToEns[_msgSender()] = ens; ensToIssuer[ens].push(_msgSender()); emit IssuerRegistered(_msgSender(), ens, ens); } /** * @notice Stamp a token contract. * NOTE: This function can only be called by Ariane and is used to save additional data on-chain for a specific token (e.g. gasless transfer, indexation, etc.) * @param contractAddress address of the token contract. * @param stamp array of bytes representing the stamp (following an internal specific format). */ function stampToken(address contractAddress, bytes[] memory stamp) public onlyArianee { tokenToStamp[contractAddress] = stamp; emit TokenStamped(contractAddress, stamp); } /** * @notice Try to retrieve registered tokens from a given ENS identity. * @param ens ENS identity. */ function getTokensPerIdentity(string memory ens) public view returns (address[] memory) { return ensToTokens[ens]; } /** * @notice Try to retrieve a registered token at a given index from a given ENS identity. * @param ens ENS identity. * @param index index of the token to retrieve from the ENS identity. */ function getTokenPerIdentityAtIndex(string memory ens, uint256 index) public view returns (address) { return ensToTokens[ens][index]; } /** * @notice Try to retrieve the ENS identity linked to a given token. * @param contractAddress address of the token contract. */ function getIdentityPerToken(address contractAddress) public view returns (string memory) { return tokenToEns[contractAddress]; } /** * @notice Try to retrieve the ENS identity linked to a given issuer. * @param issuer address of the issuer. */ function getIdentityPerIssuer(address issuer) public view returns (string memory) { return issuerToEns[issuer]; } /** * @notice Try to retrieve registered issuers from a given ENS identity. * @param ens ENS identity. */ function getIssuersPerIdentity(string memory ens) public view returns (address[] memory) { return ensToIssuer[ens]; } /** * @notice Try to retrieve a registered issuer at a given index from a given ENS identity. * @param ens ENS identity. * @param index index of the issuer to retrieve from the ENS identity. */ function getIssuerPerIdentityAtIndex(string memory ens, uint256 index) public view returns (address) { return ensToIssuer[ens][index]; } /** * @notice Try to retrieve a token stamp. * @param contractAddress address of the token contract. */ function getStamp(address contractAddress) public view returns (bytes[] memory) { return tokenToStamp[contractAddress]; } /** * @notice Register a token contract to an ENS identity. * @param contractAddress address of the token contract. * @param ens ENS identity to link to this token. */ function _registerToken(address contractAddress, string memory ens) internal { require(ens.toSlice().endsWith(ARIANEE_TLI.toSlice()), "OwnershipRegistry: The ENS identity must be an Arianee subdomain"); if (bytes(tokenToEns[contractAddress]).length > 0) { string memory oldEns = tokenToEns[contractAddress]; for (uint256 i = 0; i < ensToTokens[oldEns].length; i++) { if (ensToTokens[oldEns][i] == contractAddress) { ensToTokens[oldEns][i] = ensToTokens[oldEns][ensToTokens[oldEns].length - 1]; ensToTokens[oldEns].pop(); } } tokenToEns[contractAddress] = ""; } tokenToEns[contractAddress] = ens; ensToTokens[ens].push(contractAddress); emit TokenRegistered(contractAddress, ens, ens); } /** * @dev See {UUPSUpgradeable-_authorizeUpgrade}. */ function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} /** * @dev Check if the `sender` is the owner of the target token contract. * @param tokenAddress The address of the token contract. * @param sender The sender address. */ function _isTokenContractOwner(address tokenAddress, address sender) internal view returns (bool) { return sender == Ownable(tokenAddress).owner(); } /** * @dev See {MutableERC2771ContextUpgradeable-_msgSender}. */ function _msgSender() internal view override(MutableERC2771ContextUpgradeable, ContextUpgradeable) returns (address sender) { return MutableERC2771ContextUpgradeable._msgSender(); } /** * @dev See {MutableERC2771ContextUpgradeable-_msgData}. */ function _msgData() internal view override(MutableERC2771ContextUpgradeable, ContextUpgradeable) returns (bytes calldata ret) { return MutableERC2771ContextUpgradeable._msgData(); } }