diff --git a/ERCS/erc-7529.md b/ERCS/erc-7529.md index 0eac94ebe7..4fc7c4a371 100644 --- a/ERCS/erc-7529.md +++ b/ERCS/erc-7529.md @@ -29,7 +29,7 @@ A user visits merchant.com who accepts payments via paymentprocessor.com. The bu **Example 2**: -A user visits nftmarketplace.io to buy a limited release NFT from theirfavoritebrand.com. The marketplace app can leverage this ERC to allow the user to search by domain name and also indicate to the user that an NFT of interest is indeed an authentic asset associated with theirfavoritebrand.com. +A user visits nftmarketplace.io to buy a limited release NFT from theirfavoritebrand.com. The marketplace webapp can leverage this ERC to allow the user to search by domain name and also indicate to the user that an NFT of interest is indeed an authentic asset associated with theirfavoritebrand.com. ## Specification @@ -71,25 +71,46 @@ await fetch("https://example-doh-provider.com/dns-query?name=ERC-7529.1._domainc Any smart contract MAY implement this ERC to provide a verification mechanism of smart contract addresses listed in a compatible TXT record. -A smart contract need only store one new member variable, `domains`, which is an array of all unique eTLD+1 domains associated with the business or organization which deployed (or is closely associated with) the contract. This member variable can be written to with the external functions `addDomain` and `removeDomain`. +A smart contract need only store one new member variable, `domains`, which is a mapping from the keccak256 hash of all eTLD+1 domain strings associated with the business or organization which deployed (or is closely associated with) the contract to a boolean. This member variable can be written to with the external functions `addDomain` and `removeDomain`. The `domains` member variable can be queried by the `getDomain` function which takes a string representing an eTLD+1 and returns true +if the contract has been associated with the domain and false otherwise. + +Lastly, the contract MAY emit events when eTLD+1 domains are added (`AddDomain`) or removed (`RemoveDomain`) from the `domains` map. This can be useful for +determining all domains associated with a contract when they are not known ahead of time by the client. ```solidity { - public string[] domains; // a string list of eTLD+1 domains associated with this contract + /// @notice Optional event emitted when a domain is added + /// @param domain eTLD+1 associated with the contract + event AddDomain(string domain); + + /// @notice Optional event emitted when a domain is removed + /// @param domain eTLD+1 that is no longer associated with the contract + event RemoveDomain(string domain); + + /// @dev a mapping from the keccak256 hash of eTLD+1 domains associated with this contract to a boolean + mapping(bytes32 => bool) domains; + + /// @notice a getter function that takes an eTLD+1 domain string and returns true if associated with the contract + /// @param domain a string representing an eTLD+1 domain + function getDomain(string calldata domain) external view returns (bool); - function addDomain(string memory domain) external; // an authenticated method to add an eTLD+1 + /// @notice an authenticated method to add an eTLD+1 domain + /// @param domain a string representing an eTLD+1 domain associated with the contract + function addDomain(string calldata domain) external; - function removeDomain(string memory domain) external; // an authenticated method to remove an eTLD+1 + /// @notice an authenticated method to remove an eTLD+1 domain + /// @param domain a string representing an eTLD+1 domain that is no longer associated with the contract + function removeDomain(string calldata domain) external; } ``` ### Client-side Verification -The user client MUST verify that the eTLD+1 of the TXT record matches an entry in the `domains` list of the smart contract. +When a client detects a compatible TXT record listed on an eTLD+1, it SHOULD loop through each listed contract address and, via an appropriate RPC provider, assert +that each of the smart contracts returns `true` when the eTLD+1 string is passed to the `getDomain` function. -When a client detects a compatible TXT record listed on an eTLD+1, it MUST loop through each listed contract address and, via an appropriate RPC provider, collect the `domains` array from each contract in the list. The client should detect an eTLD+1 entry in the contract's `domains` array that exactly matches (DNS domains are not case-sensitive) the eTLD+1 of the TXT record. - -Alternatively, if a client is inspecting a contract that implements this ERC, the client SHOULD collect the `domains` array from the contract and then attempt to fetch TXT records from all listed eTLD+1 domains to ascertain its association or authenticity. The client MUST confirm that the contract's address is contained in a TXT record's `VALUE` field of the eTLD+1 pointed to by the contract's `domains` array. +Alternatively, if a client is inspecting a contract that implements this ERC, the client SHOULD inspect the `AddDomain` and `RemoveDomain` events to calculate if +one or more eTLD+1 domains are actively associated with the contract. The user client SHOULD attempt to fetch TXT records from all associated eTLD+1 domains to verify its association or authenticity. The client MUST confirm that each contract address is contained in a TXT record's `VALUE` field of the eTLD+1 pointed to by the contract's `domains` mapping. ## Rationale @@ -103,57 +124,32 @@ No backward compatibility issues found. ## Reference Implementation -The implementation of `addDomain` and `removeDomain` is a trivial exercise, but candidate implementations are given here for completeness (note that these functions are unlikely to be called often, so gas optimizations are possible): +The implementation of `getDomain`, `addDomain` and `removeDomain` is a trivial exercise, but candidate implementations are given here for completeness: ```solidity +function getDomain( + string calldata domain + ) external view returns (bool) { + return domains[keccak256(abi.encodePacked(domain))]; + } + function addDomain( string memory domain ) external onlyRole(DEFAULT_ADMIN_ROLE) { - string[] memory domainsArr = domains; - - // check if domain already exists in the array - for (uint256 i; i < domains.length; ) { - if ( - keccak256(abi.encodePacked((domainsArr[i]))) == - keccak256(abi.encodePacked((domain))) - ) { - revert("Domain already added"); - } - unchecked { - ++i; - } - } - domains.push(domain); + domains[keccak256(abi.encodePacked(domain))] = true; + emit AddDomain(domain); } function removeDomain( string memory domain ) external onlyRole(DEFAULT_ADMIN_ROLE) { - string[] memory domainsArr = domains; - // A check that is incremented if a requested domain exists - uint8 flag; - for (uint256 i; i < domains.length; ) { - if ( - keccak256(abi.encodePacked((domainsArr[i]))) == - keccak256(abi.encodePacked((domain))) - ) { - // replace the index to delete with the last element - domains[i] = domains[domains.length - 1]; - // delete the last element of the array - domains.pop(); - // update to flag to indicate a match was found - flag++; - break; - } - unchecked { - ++i; - } - } - require(flag > 0, "Domain is not in the list"); + require(domains[keccak256(abi.encodePacked(domain))] == true, "ERC7529: eTLD+1 currently not associated with this contract"); + domains[keccak256(abi.encodePacked(domain))] = false; + emit RemoveDomain(domain); } ``` -**NOTE**: It is important that appropriate account authentication be applied to `addDomain` and `removeDomain` so that only authorized users may update the `domains` list. In the given reference implementation the `onlyRole` modifier is used to restrict call privileges to accounts with the `DEFAULT_ADMIN_ROLE` which can be added to any contract with the OpenZeppelin access control library. +**NOTE**: Appropriate account authentication MUST be applied to `addDomain` and `removeDomain` so that only authorized users may update the `domains` mapping. In the given reference implementation the `onlyRole` modifier is used to restrict call privileges to accounts with the `DEFAULT_ADMIN_ROLE` which can be added to any contract with the OpenZeppelin access control abstract class. ## Security Considerations