Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update ERC-7529: Better strategy for eTLD+1 storage and querying #331

Merged
merged 23 commits into from Mar 20, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
90 changes: 43 additions & 47 deletions ERCS/erc-7529.md
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down