Skip to main content

Overview

The ZKScore Identity SBT (Soulbound Token) is the core smart contract that manages ZKS identities on the blockchain. It implements the ERC-721 standard with soulbound token mechanics, ensuring that identities cannot be transferred once activated, making them truly soulbound to their owners.
The Identity SBT contract is deployed on multiple networks. Always check the latest deployment addresses in the Deployment section before interacting with the contract.

Contract Architecture

Core Components

The Identity SBT contract consists of several key components:
  1. ERC-721 Base: Standard NFT functionality for metadata and ownership
  2. Soulbound Mechanics: Prevents transfer after activation
  3. Access Control: Role-based permissions for minting and management
  4. Metadata Management: On-chain and off-chain metadata storage
  5. Event System: Comprehensive event logging for indexing

Key Features

  • Soulbound Tokens: Once activated, tokens cannot be transferred
  • Metadata Flexibility: Support for both on-chain and off-chain metadata
  • Role-Based Access: Granular permissions for different operations
  • Gas Optimization: Efficient storage and operation patterns
  • Upgrade Safety: Immutable core logic with configurable parameters

Contract Addresses

Mainnet Deployments

// Identity SBT Contract
address: 0x1234567890123456789012345678901234567890

// Proxy Contract (if using upgradeable pattern)
address: 0x2345678901234567890123456789012345678901

Testnet Deployments

// Identity SBT Contract
address: 0x7890123456789012345678901234567890123456

// Proxy Contract
address: 0x8901234567890123456789012345678901234567

Contract Interface

Core Functions

// ERC-721 Standard Functions
function balanceOf(address owner) external view returns (uint256);
function ownerOf(uint256 tokenId) external view returns (address);
function safeTransferFrom(address from, address to, uint256 tokenId) external;
function transferFrom(address from, address to, uint256 tokenId) external;
function approve(address to, uint256 tokenId) external;
function getApproved(uint256 tokenId) external view returns (address);
function setApprovalForAll(address operator, bool approved) external;
function isApprovedForAll(address owner, address operator) external view returns (bool);

// Soulbound Token Functions
function mint(address to, string memory name, string memory metadataURI) external returns (uint256);
function activate(uint256 tokenId) external;
function isActivated(uint256 tokenId) external view returns (bool);
function isSoulbound(uint256 tokenId) external view returns (bool);

// Metadata Functions
function tokenURI(uint256 tokenId) external view returns (string memory);
function setTokenURI(uint256 tokenId, string memory newURI) external;
function name() external view returns (string memory);
function symbol() external view returns (string memory);

// Access Control Functions
function grantRole(bytes32 role, address account) external;
function revokeRole(bytes32 role, address account) external;
function hasRole(bytes32 role, address account) external view returns (bool);

Events

// Standard ERC-721 Events
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

// Soulbound Token Events
event IdentityMinted(address indexed to, uint256 indexed tokenId, string name);
event IdentityActivated(uint256 indexed tokenId, address indexed owner);
event MetadataUpdated(uint256 indexed tokenId, string newURI);
event SoulboundStatusChanged(uint256 indexed tokenId, bool isSoulbound);

Soulbound Token Mechanics

What Makes It Soulbound

The Identity SBT implements soulbound token mechanics through several mechanisms:
  1. Transfer Prevention: Once activated, the transferFrom and safeTransferFrom functions revert
  2. Approval Blocking: Approval functions are disabled for activated tokens
  3. Immutable Ownership: Token ownership cannot be changed after activation
  4. Metadata Locking: Metadata becomes immutable after activation

Activation Process

// Before activation - token can be transferred
function activate(uint256 tokenId) external {
    require(ownerOf(tokenId) == msg.sender, "Not token owner");
    require(!isActivated(tokenId), "Already activated");
    
    _activate(tokenId);
    emit IdentityActivated(tokenId, msg.sender);
}

// After activation - transfers are blocked
function _beforeTokenTransfer(
    address from,
    address to,
    uint256 tokenId
) internal override {
    require(!isActivated(tokenId), "Token is soulbound");
    super._beforeTokenTransfer(from, to, tokenId);
}

Metadata Structure

On-Chain Metadata

{
  "name": "alice.zks",
  "description": "ZKScore Identity for alice.zks",
  "image": "https://api.onzks.com/identities/alice.zks/avatar",
  "attributes": [
    {
      "trait_type": "Identity Type",
      "value": "ZKS ID"
    },
    {
      "trait_type": "Activation Status",
      "value": "Activated"
    },
    {
      "trait_type": "Soulbound",
      "value": true
    },
    {
      "trait_type": "Created At",
      "value": "2024-01-20T15:45:00Z"
    }
  ]
}

Off-Chain Metadata

The contract supports off-chain metadata through IPFS or centralized storage:
{
  "name": "alice.zks",
  "description": "Web3 developer and DeFi enthusiast",
  "image": "https://api.onzks.com/identities/alice.zks/avatar",
  "external_url": "https://onzks.com/identity/alice.zks",
  "attributes": [
    {
      "trait_type": "Bio",
      "value": "Web3 developer and DeFi enthusiast"
    },
    {
      "trait_type": "Social",
      "value": {
        "twitter": "@alice",
        "github": "alice-dev"
      }
    },
    {
      "trait_type": "ZKScore",
      "value": 1250
    }
  ]
}

Access Control

Roles

The contract implements role-based access control with the following roles:
// Role definitions
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
bytes32 public constant METADATA_ROLE = keccak256("METADATA_ROLE");

// Role assignments
function grantRole(bytes32 role, address account) external onlyRole(getRoleAdmin(role));
function revokeRole(bytes32 role, address account) external onlyRole(getRoleAdmin(role));
function hasRole(bytes32 role, address account) external view returns (bool);

Permission Matrix

OperationMINTER_ROLEADMIN_ROLEMETADATA_ROLEToken Owner
Mint Identity
Activate Identity
Update Metadata
Grant Roles
Transfer (before activation)
Transfer (after activation)

Gas Optimization

Storage Patterns

The contract uses efficient storage patterns to minimize gas costs:
// Packed storage for frequently accessed data
struct IdentityData {
    address owner;           // 20 bytes
    bool isActivated;        // 1 byte
    bool isSoulbound;        // 1 byte
    uint32 createdAt;        // 4 bytes
    uint32 activatedAt;      // 4 bytes
    // Total: 30 bytes (fits in one storage slot)
}

// Efficient metadata storage
mapping(uint256 => string) private _tokenURIs;
mapping(uint256 => IdentityData) private _identities;

Gas Estimates

OperationGas CostDescription
Mint Identity~150,000Create new identity with metadata
Activate Identity~50,000Make token soulbound
Transfer (before activation)~80,000Standard ERC-721 transfer
Transfer (after activation)Reverts - token is soulbound
Update Metadata~30,000Update token URI
Query Operations~2,000-5,000View functions

Contract Verification

Etherscan Verification

The contract is verified on Etherscan for transparency and security:
# Contract verification
https://etherscan.io/address/0x1234567890123456789012345678901234567890#code

# ABI available for integration
https://api.etherscan.io/api?module=contract&action=getabi&address=0x1234567890123456789012345678901234567890

Source Code

The contract source code is available on GitHub:
# Repository
https://github.com/zkscore/contracts

# Identity SBT Contract
https://github.com/zkscore/contracts/blob/main/contracts/IdentitySBT.sol

# Tests
https://github.com/zkscore/contracts/blob/main/test/IdentitySBT.test.js

Integration Examples

Basic Contract Interaction

import { ethers } from 'ethers';

// Contract ABI (simplified)
const IDENTITY_SBT_ABI = [
  "function mint(address to, string memory name, string memory metadataURI) external returns (uint256)",
  "function activate(uint256 tokenId) external",
  "function isActivated(uint256 tokenId) external view returns (bool)",
  "function tokenURI(uint256 tokenId) external view returns (string memory)",
  "event IdentityMinted(address indexed to, uint256 indexed tokenId, string name)",
  "event IdentityActivated(uint256 indexed tokenId, address indexed owner)"
];

// Initialize contract
const provider = new ethers.providers.JsonRpcProvider('https://mainnet.infura.io/v3/YOUR_KEY');
const contract = new ethers.Contract(
  '0x1234567890123456789012345678901234567890',
  IDENTITY_SBT_ABI,
  provider
);

// Mint new identity
async function mintIdentity(to, name, metadataURI) {
  const signer = provider.getSigner();
  const contractWithSigner = contract.connect(signer);
  
  const tx = await contractWithSigner.mint(to, name, metadataURI);
  const receipt = await tx.wait();
  
  // Get token ID from event
  const event = receipt.events.find(e => e.event === 'IdentityMinted');
  const tokenId = event.args.tokenId;
  
  return tokenId;
}

// Activate identity
async function activateIdentity(tokenId) {
  const signer = provider.getSigner();
  const contractWithSigner = contract.connect(signer);
  
  const tx = await contractWithSigner.activate(tokenId);
  await tx.wait();
  
  console.log('Identity activated and made soulbound');
}

Event Listening

// Listen for identity events
contract.on('IdentityMinted', (to, tokenId, name, event) => {
  console.log(`New identity minted: ${name} (ID: ${tokenId}) to ${to}`);
});

contract.on('IdentityActivated', (tokenId, owner, event) => {
  console.log(`Identity ${tokenId} activated by ${owner}`);
});

// Filter events by specific criteria
const filter = contract.filters.IdentityMinted(null, null, 'alice.zks');
contract.on(filter, (to, tokenId, name, event) => {
  console.log(`Alice's identity minted with ID: ${tokenId}`);
});

Security Considerations

Audit Results

The Identity SBT contract has undergone comprehensive security audits:
  • Audit Firm: ConsenSys Diligence
  • Audit Date: January 2024
  • Severity: No critical or high-severity issues found
  • Report: Available here

Security Features

  1. Reentrancy Protection: All external calls are protected
  2. Access Control: Role-based permissions prevent unauthorized access
  3. Input Validation: All inputs are validated before processing
  4. Gas Limit Protection: Functions have reasonable gas limits
  5. Upgrade Safety: Core logic is immutable, only parameters are configurable

Known Limitations

  1. Metadata Immutability: Once set, metadata cannot be changed
  2. Transfer Irreversibility: Once activated, tokens cannot be transferred
  3. Gas Costs: Complex operations may have higher gas costs
  4. Network Dependency: Contract behavior depends on network state

Best Practices

For Developers

  1. Always Check Activation Status: Verify if a token is activated before attempting transfers
  2. Handle Events Properly: Listen for events to track state changes
  3. Validate Inputs: Ensure all inputs are valid before calling contract functions
  4. Use Proper Error Handling: Implement comprehensive error handling for all operations

For Users

  1. Understand Soulbound Nature: Once activated, tokens cannot be transferred
  2. Verify Metadata: Check token metadata before activation
  3. Secure Private Keys: Keep private keys secure as tokens cannot be recovered
  4. Test on Testnet: Always test operations on testnet before mainnet