EIP-7702

📜 EIP-7702: Soulbound Token Standard

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.

Last updated

Was this helpful?