Implementation Guide
Complete guide to implementing ERC-7866 Soul Bound Profiles in your dApp.
Overview
ERC-7866 defines a standard for decentralized user profiles with:
- Unique usernames tied to addresses
- Default and dApp-specific avatars
- Privacy control for avatar visibility
- Off-chain metadata with on-chain pointers
Core Contracts
IERC7866 Interface
The standard interface all implementations must support:
solidity
interface IERC7866 {
function createProfile(string memory username, string memory defaultAvatarURI) external;
function setDefaultAvatar(string memory avatarURI) external;
function setDappAvatar(string memory dappName, string memory avatarURI, bool isPublic) external;
function removeDappAvatar(string memory dappName) external;
function getProfile(address owner) external view returns (Profile memory);
function getDefaultAvatar(address owner) external view returns (string memory);
function getDappAvatar(address owner, string memory dappName) external view returns (DappAvatar memory);
function getProfileByUsername(string memory username) external view returns (address);
function hasProfile(address owner) external view returns (bool);
}SoulProfile Implementation
Reference implementation with full feature support:
solidity
import { IERC7866 } from "../interfaces/IERC7866.sol";
contract SoulProfile is IERC7866 {
mapping(address => Profile) private profiles;
mapping(address => mapping(string => DappAvatar)) private dappAvatars;
mapping(string => address) private usernameToAddress;
mapping(address => bool) private _hasProfile;
// ... events and functions
}SoulProfileResolver
Optional resolver for safe profile discovery:
solidity
contract SoulProfileResolver {
IERC7866 public soulProfile;
function resolveDappAvatar(address owner, string memory dappName)
external view returns (string memory);
function resolveDappAvatarPublic(address owner, string memory dappName)
external view returns (string memory);
function resolveUsername(address owner) external view returns (string memory);
}Deployment
Deploy SoulProfile
bash
# Compile
forge build
# Deploy to Ethereum
forge create contracts/core/SoulProfile.sol:SoulProfile \
--rpc-url $ETHEREUM_RPC \
--private-key $PRIVATE_KEYDeploy SoulProfileResolver (Optional)
bash
# Deploy resolver with SoulProfile address
forge create contracts/extensions/SoulProfileResolver.sol:SoulProfileResolver \
--constructor-args <SOULPROFILE_ADDRESS> \
--rpc-url $ETHEREUM_RPC \
--private-key $PRIVATE_KEYUsage Examples
Create Profile
solidity
// User creates a profile
soulProfile.createProfile(
"alice",
"ipfs://QmXxxx..." // Avatar metadata
);Validations
- Username must be unique
- Username must be 1-32 characters
- Profile must not already exist
Set Avatars
solidity
// Set default avatar
soulProfile.setDefaultAvatar("ipfs://QmYyyy...");
// Set dApp-specific avatar (public)
soulProfile.setDappAvatar(
"MyGameDApp",
"ipfs://QmZzzz...",
true // isPublic
);
// Set private dApp avatar
soulProfile.setDappAvatar(
"PrivateDApp",
"ipfs://QmPrivate...",
false // isPublic
);
// Remove dApp avatar
soulProfile.removeDappAvatar("MyGameDApp");Retrieve Profiles
solidity
// Get complete profile
IERC7866.Profile memory profile = soulProfile.getProfile(userAddress);
// Returns: { username, defaultAvatarURI, bio, website }
// Get default avatar
string memory avatar = soulProfile.getDefaultAvatar(userAddress);
// Get dApp-specific avatar (with privacy enforcement)
IERC7866.DappAvatar memory gameAvatar = soulProfile.getDappAvatar(
userAddress,
"MyGameDApp"
);
// Lookup address by username
address alice = soulProfile.getProfileByUsername("alice");
// Check if profile exists
bool exists = soulProfile.hasProfile(userAddress);Privacy Enforcement
Privacy is enforced at read time:
solidity
function getDappAvatar(address owner, string memory dappName)
external view
returns (DappAvatar memory)
{
DappAvatar memory avatar = dappAvatars[owner][dappName];
// Return empty URI if private and caller isn't owner
if (!avatar.isPublic && msg.sender != owner) {
return DappAvatar({dappName: dappName, avatarURI: "", isPublic: false});
}
return avatar;
}When building UI, always handle the case where avatarURI is empty (indicating private data).
Off-Chain Metadata
Store rich metadata on IPFS or Arweave:
json
{
"username": "alice",
"avatar": "https://gateway.pinata.cloud/ipfs/QmHash",
"bio": "Web3 developer",
"website": "https://alice.com",
"social": {
"twitter": "@alice",
"github": "alice-dev",
"discord": "alice#1234"
}
}Upload to IPFS:
bash
# Using IPFS CLI
ipfs add metadata.json
# Output: QmXxxx...
# Or using Pinata/Infura
curl -X POST https://api.pinata.cloud/pinning/pinFileToIPFS \
-F file=@metadata.json \
-H "pinata_api_key: $KEY" \
-H "pinata_secret_api_key: $SECRET"Use the returned hash as the avatar URI:
solidity
soulProfile.createProfile("alice", "ipfs://QmXxxx...");Integration Pattern
Integrate profiles in your dApp:
solidity
import { IERC7866 } from "../interfaces/IERC7866.sol";
contract MyGame {
IERC7866 public soulProfiles;
constructor(address _soulProfileAddress) {
soulProfiles = IERC7866(_soulProfileAddress);
}
function getPlayerName(address player) external view returns (string memory) {
if (!soulProfiles.hasProfile(player)) {
return "Anonymous";
}
IERC7866.Profile memory profile = soulProfiles.getProfile(player);
return profile.username;
}
function getPlayerAvatar(address player) external view returns (string memory) {
if (!soulProfiles.hasProfile(player)) {
return "";
}
// Try game-specific avatar
IERC7866.DappAvatar memory gameAvatar = soulProfiles.getDappAvatar(
player,
"MyGame"
);
if (bytes(gameAvatar.avatarURI).length > 0) {
return gameAvatar.avatarURI;
}
// Fall back to default
return soulProfiles.getDefaultAvatar(player);
}
}Event Listening
Listen to profile events for discovery:
typescript
const contract = new ethers.Contract(
SOULPROFILE_ADDRESS,
ABI,
provider
);
// Track new profiles
contract.on("ProfileCreated", (owner, username) => {
console.log(`New profile: ${username} by ${owner}`);
// Index for discovery
});
// Track avatar updates
contract.on("DefaultAvatarUpdated", (owner, avatarURI) => {
console.log(`${owner} updated avatar to ${avatarURI}`);
});
// Track dApp avatars
contract.on("DappAvatarSet", (owner, dappName, avatarURI, isPublic) => {
console.log(`${owner} set ${dappName} avatar (public: ${isPublic})`);
});Resolver Usage
Use the resolver for safer profile lookups:
solidity
SoulProfileResolver resolver = new SoulProfileResolver(address(soulProfile));
// Resolve with privacy checks already applied
string memory avatar = resolver.resolveDappAvatar(userAddress, "MyDApp");
// Resolve only public avatars
string memory publicAvatar = resolver.resolveDappAvatarPublic(userAddress, "MyDApp");
// Resolve username
string memory username = resolver.resolveUsername(userAddress);Best Practices
Username Management
- Use lowercase for consistency
- Avoid special characters
- Reserve important names through governance if needed
- Consider allowing name changes in v2
Avatar URIs
- Always validate URIs in frontend
- Use whitelist for allowed protocols (ipfs://, ar://, https://)
- Beware of javascript: and data: URIs
- Cache metadata locally with IPFS/Arweave gateway
Privacy
- Always check
avatarURIis not empty before using - Private avatars return empty string, not null
- Remember that avatar existence is revealed through events
- Plan privacy strategy before deployment
Gas Optimization
- Batch avatar updates in separate transactions
- Don't store large metadata on-chain
- Use IPFS/Arweave for all media
Testing
Test with Foundry:
solidity
function testCreateProfile() public {
vm.prank(alice);
soulProfile.createProfile("alice", "ipfs://avatar");
IERC7866.Profile memory profile = soulProfile.getProfile(alice);
assertEq(profile.username, "alice");
}
function testPrivateAvatar() public {
vm.prank(alice);
soulProfile.createProfile("alice", "ipfs://avatar");
vm.prank(alice);
soulProfile.setDappAvatar("Game", "ipfs://game", false); // private
// Owner can see
IERC7866.DappAvatar memory avatar1 = soulProfile.getDappAvatar(alice, "Game");
assertNotEq(bytes(avatar1.avatarURI).length, 0);
// Non-owner cannot see
vm.prank(bob);
IERC7866.DappAvatar memory avatar2 = soulProfile.getDappAvatar(alice, "Game");
assertEq(bytes(avatar2.avatarURI).length, 0);
}Common Patterns
Multi-dApp Avatar Strategy
solidity
// Public default for discoverability
setDefaultAvatar("ipfs://public...");
// Game-specific for context
setDappAvatar("GameA", "ipfs://character...", true);
setDappAvatar("GameB", "ipfs://different...", true);
// Private for sensitive data
setDappAvatar("PrivateApp", "ipfs://sensitive...", false);Profile Discovery
typescript
// Build username index from events
const profiles = new Map<string, string>();
contract.on("ProfileCreated", (owner, username) => {
profiles.set(username, owner);
});
// Later, lookup by username
const ownerAddress = profiles.get("alice");Cross-Game Avatar Sync
solidity
// Use same dApp name on multiple games
// All integrate the same avatar across games
// Or use a custom dApp name for the sync service
soulProfile.setDappAvatar(
"CrossGameAvatar",
"ipfs://universal-avatar...",
true
);