ERC-7866 - Decentralised User Profiles

Created 2025-01-22
Status Draft
Category ERC
Type Standards Track
Authors
Requires

Abstract

This EIP proposes a standard for decentralised, interoperable user profiles known as Decentralised Profiles. Profiles are implemented as Soul Bound Tokens (SBTs) that are immutable, non-transferable, and tied to unique identifiers across multiple blockchain networks. The standard provides a unified structure for user metadata, including dApp-specific customisation, default profiles, and seamless cross-chain compatibility. Profiles can be leveraged for identity management, reputation systems, and personalised dApp experiences.

Motivation

Existing solutions for decentralised identity and user profiles lack cross-chain compatibility, dApp-specific customisation, and standardisation. A unified approach is essential to: 1. Facilitate interoperable profiles across all chains. 2. Leverage the immutability and non-transferability of SBTs for secure identities. 3. Enable dApp-specific customisations, such as unique avatars. 4. Provide a robust, standards-based alternative to centralised solutions like Gravatar. 5. Ensure user control and decentralisation with profiles stored on IPFS/Arweave. 6. A common standard for Decentralised Identity that can be used across all chains. 7. Act as a Digital Passport for users, enabling seamless decentralised verification and authentication.

Specification

Unique Profile Identifiers

Decentralised Profile

Each profile is identified by:

<username>@<network_slug>.soul

Example: john@eth.soul alice@polygon.soul

Decentralised Identifier (DID)

Each profile is tied to a DID:

did:<chain>:<address>

Example: did:ethereum:0x123... did:xion:xion1abc...

Metadata Structure

The profile metadata structure is designed to balance extensibility, usability, and compatibility with decentralized storage systems like IPFS. Metadata will adhere to the following schema:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "UserProfile",
  "type": "object",
  "properties": {
    "username": {
      "type": "string",
      "description": "The unique handle of the user."
    },
    "avatar": {
      "type": "string",
      "format": "uri",
      "description": "IPFS URI pointing to the user's main avatar image."
    },
    "bio": {
      "type": "string",
      "description": "Short description or biography of the user."
    },
    "website": {
      "type": "string",
      "format": "uri",
      "description": "Personal or professional website of the user."
    },
    "socials": {
      "type": "object",
      "description": "User's social links.",
      "properties": {
        "twitter": {
          "type": "string",
          "format": "uri",
          "description": "URL to the user's Twitter profile."
        },
        "github": {
          "type": "string",
          "format": "uri",
          "description": "URL to the user's GitHub profile."
        }
      },
      "required": ["twitter", "github"],
      "additionalProperties": false
    },
    "default_avatar_visibility": {
      "type": "string",
      "enum": ["public", "private"],
      "description": "Default visibility setting for the main avatar."
    },
    "dapp_avatars": {
      "type": "object",
      "description": "Mapping of DApp addresses to their custom avatars and visibility settings.",
      "patternProperties": {
        "^0x[a-fA-F0-9]{40}$": {
          "type": "object",
          "properties": {
            "avatar": {
              "type": "string",
              "format": "uri",
              "description": "IPFS URI for the DApp-specific avatar."
            },
            "visibility": {
              "type": "string",
              "enum": ["public", "private"],
              "description": "Visibility setting for this avatar."
            }
          },
          "required": ["avatar", "visibility"],
          "additionalProperties": false
        }
      },
      "additionalProperties": false
    }
  },
  "required": [
    "username",
    "avatar",
    "bio",
    "website",
    "socials",
    "default_avatar_visibility",
    "dapp_avatars"
  ],
  "additionalProperties": false
}

Here's an example using the structure above:

{
  "username": "batman",
  "avatar": "ipfs://QmExampleMainAvatarCID",
  "bio": "Blockchain enthusiast and builder.",
  "website": "https://anirudha.dev",
  "socials": {
    "twitter": "https://twitter.com/kranirudha",
    "github": "https://github.com/anistark"
  },
  "default_avatar_visibility": "public",
  "dapp_avatars": {
    "0xDAppAddress1abcdefabcdefabcdefabcdefabcdefabcd": {
      "avatar": "ipfs://QmExampleAvatar1CID",
      "visibility": "private"
    },
    "0xDAppAddress2abcdefabcdefabcdefabcdefabcdefabcd": {
      "avatar": "ipfs://QmExampleAvatar2CID",
      "visibility": "public"
    }
  }
}

Access Control

  1. Default Avatar Visibility: Users can set their default avatar visibility as public or private.
  2. dApp-Specific Avatar Visibility: Each dApp-specific avatar can also have its visibility set to public or private.

Visibility Logic: - If an avatar is public, it is retrievable by any external caller. - If an avatar is private, only the user can retrieve it. Other callers will get an encoded response alongwith error message.

dApp-Specific Avatar Customisation

Rationale

The design of the Decentralised Profile Standard was guided by the need for a unified, interoperable user profile system that can operate seamlessly across all blockchain networks. Current solutions, such as ENS profiles or Gravatar, either lack cross-chain functionality, are centralised, or do not allow users to customise profiles for specific dApps. This standard addresses these shortcomings while ensuring simplicity, security, and scalability.

Design Decisions

  1. Decentralised Identifiers (DIDs)
  2. Using the did:<chain>:<address> format provides a globally unique identifier for profiles. This aligns with the decentralised identity movement and ensures compatibility with broader DID frameworks.

  3. Decentralised Profile

  4. The profile format (username@networkslug.soul) makes profiles human-readable and chain-specific while maintaining a universal structure. The .soul suffix clearly identifies profiles compliant with this standard.

  5. dApp-Specific Avatars

  6. Allowing users to assign dApp-specific avatars caters to personalisation and enhances the user experience. It supports scenarios where users may want different representations or metadata for different applications.

  7. Soul Bound Tokens (SBTs)

  8. Leveraging SBTs ensures that profiles are non-transferable, reinforcing the concept of identity ownership. SBTs prevent profiles from being sold or hijacked, making them ideal for reputation-based systems.

  9. Registry and Resolver Architecture

  10. This architecture was chosen for its extensibility and proven track record, as seen in ENS. It separates the management of profile identifiers (Registry) from the resolution of metadata (Resolver), making upgrades and integrations straightforward.

  11. Compatibility with Existing Standards

  12. The profile standard integrates with ERC-165 for interface detection and can complement ENS or other naming systems, fostering interoperability rather than competition.

  13. Default and dApp-Specific Metadata

  14. The inclusion of both default and dApp-specific metadata ensures flexibility. If dApp-specific metadata is not set, the default profile seamlessly applies, reducing friction for developers and users.

  15. On-Chain Data Minimisation

  16. Metadata is stored off-chain (e.g., IPFS or Arweave) to minimise gas costs and support scalable operations. Only URIs and pointers are stored on-chain.

  17. Access Control

  18. Users have complete control over avatar visibility, catering to privacy preferences.

  19. Specific dApp-based customizations ensure fine-grained control.

  20. dApp Identification

  21. Requiring dApps to be identified by an address ensures traceability and security.

  22. Extensibility

  23. Adding metadata fields or new visibility levels does not disrupt the standard.

  24. Profiles can remain lightweight while supporting future scalability.

  25. Security

  26. Access control minimizes the risk of sensitive data exposure.

  27. Metadata stored off-chain ensures minimal gas usage and flexibility.

Alternative Designs Considered

  1. Pure On-Chain Metadata
  2. Storing all metadata on-chain was considered but discarded due to high gas costs, limited storage capacity, and challenges in supporting complex or large datasets such as high-resolution avatars.

  3. Direct Integration with ENS

  4. While integrating with ENS was explored, it was deemed limiting for cross-chain functionality, as ENS is predominantly tied to Ethereum. Decentralised profile takes inspiration from ENS while ensuring a truly multichain approach.

  5. Fully Centralised System

  6. A centralised system would simplify implementation but contradict the core principles of decentralisation and user sovereignty.

  7. Non-SBT-Based Implementation

  8. Using standard ERC721 tokens for profiles was considered but rejected since transferability is unsuitable for identity management. SBTs enforce the immutability of identity ownership.

Comparison with Related Work

Scalability and Extensibility

The Registry and Resolver architecture, combined with off-chain metadata storage, ensures that profile can scale with the growth of the blockchain ecosystem. New chains, metadata types, and customisations can be added without disrupting the core functionality or introducing breaking changes.

The design balances simplicity, extensibility, and user control, making it well-suited for adoption across a wide range of dApps, wallets, and blockchains.

Contract Interface

The standard includes the following interface:

ISoulProfile

// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.19;

interface ISoulProfile {
    struct Metadata {
        string username;
        string avatar;
        string bio;
        string website;
        mapping(string => string) socials;
        string defaultAvatarVisibility;
        mapping(address => DappAvatar) dappAvatars;
    }

    struct DappAvatar {
        string avatarURI;
        string visibility; // "public" or "private"
    }

    event ProfileCreated(address indexed user, string did, string username);
    event AvatarUpdated(address indexed user, string avatarURI, string visibility);
    event DappAvatarUpdated(
        address indexed user,
        address indexed dApp,
        string avatarURI,
        string visibility
    );

    function createProfile(string calldata username) external;
    function setDefaultAvatar(string calldata avatarURI, string calldata visibility) external;
    function setDappAvatar(
        address dApp,
        string calldata avatarURI,
        string calldata visibility
    ) external;
    function getDefaultAvatar(address user) external view returns (string memory, string memory);
    function getDappAvatar(address user, address dApp) external view returns (string memory, string memory);
}

Reference Implementation

SoulProfile

// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "./ISoulProfile.sol";

contract SoulProfile is Ownable, ISoulProfile {
    mapping(address => Metadata) private profiles;

    modifier onlyProfileOwner(address user) {
        require(msg.sender == user, "Not authorized");
        _;
    }

    function createProfile(string calldata username) external override {
        require(bytes(username).length > 0, "Username cannot be empty");
        require(profiles[msg.sender].username == "", "Profile already exists");

        profiles[msg.sender].username = username;
        profiles[msg.sender].defaultAvatarVisibility = "public"; // Default to public

        emit ProfileCreated(msg.sender, generateDID(msg.sender), username);
    }

    function setDefaultAvatar(string calldata avatarURI, string calldata visibility) external override {
        require(bytes(visibility).length > 0, "Visibility must be set");
        profiles[msg.sender].avatar = avatarURI;
        profiles[msg.sender].defaultAvatarVisibility = visibility;

        emit AvatarUpdated(msg.sender, avatarURI, visibility);
    }

    function setDappAvatar(
        address dApp,
        string calldata avatarURI,
        string calldata visibility
    ) external override {
        require(dApp != address(0), "Invalid dApp address");
        require(bytes(visibility).length > 0, "Visibility must be set");

        profiles[msg.sender].dappAvatars[dApp] = DappAvatar(avatarURI, visibility);

        emit DappAvatarUpdated(msg.sender, dApp, avatarURI, visibility);
    }

    function getDefaultAvatar(address user)
        external
        view
        override
        returns (string memory avatarURI, string memory visibility)
    {
        Metadata storage profile = profiles[user];
        return (profile.avatar, profile.defaultAvatarVisibility);
    }

    function getDappAvatar(address user, address dApp)
        external
        view
        override
        returns (string memory avatarURI, string memory visibility)
    {
        Metadata storage profile = profiles[user];
        DappAvatar storage dappAvatar = profile.dappAvatars[dApp];
        return (dappAvatar.avatarURI, dappAvatar.visibility);
    }

    function generateDID(address user) internal pure returns (string memory) {
        return string(abi.encodePacked("did:ethereum:", Strings.toHexString(user)));
    }
}

Of course, this can be extended to prepare a full registry and resolver according to ENS or similar standards. Refer to Rationale above for more information about the same.

Backwards Compatibility

The Decentralised Profile system is designed to ensure smooth integration with existing decentralized applications (dApps) and platforms, while offering an upgrade path for future enhancements. Below are key considerations for backwards compatibility:

Security Considerations

Profile system should prioritise robust security to protect user data and prevent unauthorized access.

  1. Access Control: Users can set visibility for their avatars (public or private). This is enforced at both the resolver and registry levels to ensure unauthorized entities cannot access private avatars. Functions that modify state (e.g., setDefaultAvatar, setDappAvatar) are protected with access controls, ensuring only the profile owner can make changes.

  2. Data Privacy: Sensitive metadata is encrypted before storage on decentralized storage systems like IPFS or Arweave. Visibility flags ensure users can control who can view specific profile elements.

  3. Reentrancy Protection: Functions modifying state implement the checks-effects-interactions pattern or leverage ReentrancyGuard to prevent reentrancy attacks. For example, setDappAvatar ensures that all validations are performed before updating the state.

  4. Input Validation: All user inputs (e.g., username, visibility, dApp) are validated to ensure they meet specified criteria. Invalid or malicious inputs are rejected to prevent injection attacks or other exploits. Visibility is restricted to predefined values ("public" or "private").

  5. Immutable DIDs: Decentralized identifiers (DIDs) for users are immutable once created. This prevents spoofing or unauthorized changes to user identities.

  6. Fallback Mechanisms: The system provides fallback mechanisms for fetching avatars. If a dApp-specific avatar is not found, the default avatar is returned, ensuring smooth operation without errors.

  7. Upgradeable Contracts: Proxy contracts are used to allow for upgrades while preserving state. Upgrades are performed securely via a multi-signature governance process to minimize risks.

  8. Rate Limiting: Rate-limiting mechanisms can be implemented to prevent spam or abuse of profile creation and update functions.

  9. Audits and Best Practices: The contracts are designed following best practices and are to be audited regularly by independent security firms. Dependencies (e.g., OpenZeppelin contracts) are reviewed and kept up to date to mitigate vulnerabilities.

  10. dApp Address Verification: All dApps interacting with the system must be identified by a valid address. This ensures that unauthorized or spoofed entities cannot manipulate profiles or fetch restricted data.

  11. Phishing Mitigation: User-facing dApps are encouraged to clearly display information about interactions on-chain. Users should be warned about potential phishing attacks and advised to interact only with verified dApps.

  12. Gas Optimization: Operations are optimized to prevent gas exhaustion during execution, which could lead to incomplete transactions. This ensures that even on congested networks, the system remains functional.

  13. Secure Fallback Functions: Fallback functions are implemented securely to prevent accidental Ether transfers or denial-of-service attacks.

Copyright

Copyright and related rights waived via CC0.