Privacy Model
How ERC-7866 handles data visibility and privacy.
Visibility Levels
ERC-7866 supports two avatar visibility levels:
Public Avatars
profile.setDappAvatar("PublicDApp", "ipfs://QmHash", true); // isPublic = trueCharacteristics
- Readable by anyone
- No restrictions on access
- Return full avatar data to any caller
- Events are public
Use Cases
- Marketing avatars
- Professional branding
- Game characters (in public games)
- Community-facing identity
Private Avatars
profile.setDappAvatar("PrivateDApp", "ipfs://QmHash", false); // isPublic = falseCharacteristics
- Readable only by owner
- Other callers get empty URI
- No indication of existence to others
- Still emits events (events are always public)
Use Cases
- Personal/sensitive data
- Account-specific metadata
- Private app data
- Hidden game progress
Privacy Enforcement
Privacy is enforced at the function call level, not storage level:
function getDappAvatar(address owner, string memory dappName)
external view
returns (DappAvatar memory)
{
DappAvatar memory avatar = dappAvatars[owner][dappName];
// Privacy check at execution time
if (!avatar.isPublic && msg.sender != owner) {
return DappAvatar({dappName: dappName, avatarURI: "", isPublic: false});
}
return avatar;
}How It Works
- Function checks if avatar is private:
!avatar.isPublic - Function checks if caller is owner:
msg.sender != owner - If both true, return empty URI instead of actual data
- Otherwise, return full avatar data
Default Avatar Privacy
Default avatars are always public:
function getDefaultAvatar(address owner)
external view
returns (string memory)
{
require(_hasProfile[owner], "Profile does not exist");
return profiles[owner].defaultAvatarURI; // No privacy check
}Rationale
- Default avatar is meant to be a public identity
- Provides a baseline, discoverable avatar
- dApp-specific avatars can override with privacy
Profile Existence
Checking if a profile exists is always public:
function hasProfile(address owner) external view returns (bool) {
return _hasProfile[owner];
}Note: The fact that an address has a profile is public knowledge. Only the avatar URIs (for private avatars) are hidden.
Event Visibility
All events are public and on-chain:
event DappAvatarSet(
address indexed owner,
string dappName,
string avatarURI,
bool isPublic
);Important: Even if you set a private avatar, the event logs:
- That an avatar was set
- For which dApp
- The owner address
- Whether it's public or private
Resolver Pattern
The SoulProfileResolver contract provides privacy-aware lookups:
// Returns avatar only if public
function resolveDappAvatarPublic(address owner, string memory dappName)
external view
returns (string memory)
{
DappAvatar memory avatar = soulProfile.getDappAvatar(owner, dappName);
if (!avatar.isPublic) {
return "";
}
return avatar.avatarURI;
}
// Returns avatar if public OR caller is owner
function resolveDappAvatar(address owner, string memory dappName)
external view
returns (string memory)
{
DappAvatar memory avatar = soulProfile.getDappAvatar(owner, dappName);
return avatar.avatarURI; // Already has privacy check
}Privacy Patterns
Public Identity
// Set public default avatar
profile.setDefaultAvatar("ipfs://public-avatar...");
// Set public dApp avatars
profile.setDappAvatar("GameA", "ipfs://game-avatar...", true);
profile.setDappAvatar("GameB", "ipfs://other-avatar...", true);Result: Your identity is discoverable everywhere
Tiered Privacy
// Public default avatar (everyone sees this)
profile.setDefaultAvatar("ipfs://professional-avatar...");
// Public game avatar (players see this)
profile.setDappAvatar("GameA", "ipfs://game-character...", true);
// Private app data (only you see this)
profile.setDappAvatar("PrivateApp", "ipfs://personal-data...", false);Result: Different visibility for different contexts
Fully Private
// No default avatar set (no private option - defaults are public if set)
// Only private dApp avatars
profile.setDappAvatar("Secret1", "ipfs://...", false);
profile.setDappAvatar("Secret2", "ipfs://...", false);Result: Minimal public footprint
Frontend Privacy Handling
When building UIs:
// Always respect privacy
const getAvatarForDisplay = async (userAddress: string, dappName: string) => {
try {
const avatar = await contract.getDappAvatar(userAddress, dappName);
// Check if URI is empty (means private + you're not the owner)
if (!avatar.avatarURI) {
return defaultAvatar;
}
return avatar.avatarURI;
} catch (error) {
return defaultAvatar;
}
};
// Only request public avatars for discovery
const discoverProfiles = async (userAddresses: string[]) => {
const profiles = await Promise.all(
userAddresses.map(addr =>
contract.getDefaultAvatar(addr).catch(() => "")
)
);
return profiles.filter(p => p !== "");
};Security Considerations
Private Avatars Don't Hide Ownership
- Smart contracts can still call
getDappAvatar()and see the raw data - Only non-owner external calls get empty URIs
- On-chain, privacy is about access control, not encryption
Events Reveal Activity
- Setting a private avatar still emits events
- Observers know you have a private avatar even if they can't read it
- Consider this before setting sensitive data
Visibility is Immutable Per Avatar
- Once you set an avatar as private, it stays private
- You must remove and recreate to change visibility
- Plan your privacy strategy upfront