EIP-7702 introduces a standard for Soulbound Tokens (SBTs) in the Ethereum ecosystem. This page will provide a comprehensive overview of its architecture, core concepts, user flow, and implementation details.
🏗️ Architecture
The EIP-7702 standard defines a set of interfaces and functionalities for Soulbound Tokens. Here's a high-level overview of the architecture:
🧠 Core Concepts
1. Non-Transferability 🔒
SBTs are designed to be non-transferable, meaning once minted to an address, they cannot be moved to another address.
2. Revocability 🔄
The issuer of an SBT has the ability to revoke (burn) the token if necessary.
3. Metadata 📊
Each SBT can carry metadata, providing additional information about the token and its properties.
4. Enumeration 🔢
The standard includes enumeration functions to easily query and iterate over tokens.
🚶 User Flow
Here's a typical user flow for interacting with Soulbound Tokens:
💻 Implementation Details
Core Interface (IERC7702)
interface IERC7702 {
event Minted(address indexed to, uint256 indexed tokenId);
event Burned(address indexed from, uint256 indexed tokenId);
function mint(address to) external returns (uint256 tokenId);
function burn(uint256 tokenId) external;
function ownerOf(uint256 tokenId) external view returns (address owner);
}
Metadata Interface (IERC7702Metadata)
interface IERC7702Metadata is IERC7702 {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function tokenURI(uint256 tokenId) external view returns (string memory);
}
Enumerable Interface (IERC7702Enumerable)
interface IERC7702Enumerable is IERC7702 {
function totalSupply() external view returns (uint256);
function tokenByIndex(uint256 index) external view returns (uint256);
function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256);
}
🚀 Example Implementation
Here's a basic implementation of the ERC7702 standard:
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
contract ERC7702 is IERC7702, IERC7702Metadata, IERC7702Enumerable {
using Counters for Counters.Counter;
using Strings for uint256;
string private _name;
string private _symbol;
Counters.Counter private _tokenIds;
mapping(uint256 => address) private _owners;
mapping(address => uint256[]) private _ownedTokens;
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
function mint(address to) external override returns (uint256) {
_tokenIds.increment();
uint256 newTokenId = _tokenIds.current();
_mint(to, newTokenId);
return newTokenId;
}
function burn(uint256 tokenId) external override {
require(msg.sender == ownerOf(tokenId), "ERC7702: caller is not the owner");
_burn(tokenId);
}
function ownerOf(uint256 tokenId) public view override returns (address) {
address owner = _owners[tokenId];
require(owner != address(0), "ERC7702: invalid token ID");
return owner;
}
function name() public view override returns (string memory) {
return _name;
}
function symbol() public view override returns (string memory) {
return _symbol;
}
function tokenURI(uint256 tokenId) public view override returns (string memory) {
require(_exists(tokenId), "ERC7702: URI query for nonexistent token");
return string(abi.encodePacked("<https://example.com/token/>", tokenId.toString()));
}
function totalSupply() public view override returns (uint256) {
return _tokenIds.current();
}
function tokenByIndex(uint256 index) public view override returns (uint256) {
require(index < totalSupply(), "ERC7702: global index out of bounds");
return index + 1;
}
function tokenOfOwnerByIndex(address owner, uint256 index) public view override returns (uint256) {
require(index < balanceOf(owner), "ERC7702: owner index out of bounds");
return _ownedTokens[owner][index];
}
function balanceOf(address owner) public view returns (uint256) {
return _ownedTokens[owner].length;
}
function _mint(address to, uint256 tokenId) internal {
require(to != address(0), "ERC7702: mint to the zero address");
require(!_exists(tokenId), "ERC7702: token already minted");
_owners[tokenId] = to;
_ownedTokens[to].push(tokenId);
emit Minted(to, tokenId);
}
function _burn(uint256 tokenId) internal {
address owner = ownerOf(tokenId);
delete _owners[tokenId];
_removeTokenFromOwnerEnumeration(owner, tokenId);
emit Burned(owner, tokenId);
}
function _exists(uint256 tokenId) internal view returns (bool) {
return _owners[tokenId] != address(0);
}
function _removeTokenFromOwnerEnumeration(address owner, uint256 tokenId) private {
uint256 lastTokenIndex = _ownedTokens[owner].length - 1;
uint256 tokenIndex;
for (uint256 i = 0; i <= lastTokenIndex; i++) {
if (_ownedTokens[owner][i] == tokenId) {
tokenIndex = i;
break;
}
}
if (tokenIndex != lastTokenIndex) {
_ownedTokens[owner][tokenIndex] = _ownedTokens[owner][lastTokenIndex];
}
_ownedTokens[owner].pop();
}
}
This implementation provides a solid foundation for creating Soulbound Tokens following the EIP-7702 standard. Developers can extend and customize this base implementation to suit their specific use cases and requirements.