Claude Skills Guide

Claude Code for Soulbound Token Workflow

Soulbound tokens (SBTs) represent a powerful primitive in Web3 development—non-transferable tokens that bind an asset to a specific wallet address. Unlike traditional NFTs, SBTs cannot be sold or transferred, making them ideal for credentials, memberships, achievements, and identity verification. This guide shows you how to use Claude Code to streamline your soulbound token development workflow.

Understanding Soulbound Tokens

Soulbound tokens derive their name from the concept in gaming—items that are permanently bound to a character and cannot be traded. In blockchain terms, this translates to tokens with transfer restrictions enforced at the smart contract level.

Key Use Cases

Setting Up Your Development Environment

Before writing soulbound token contracts, ensure your environment is properly configured with Claude Code and the necessary tooling.

Prerequisites

# Install Foundry (smart contract development framework)
curl -L https://foundry.paradigm.xyz | bash
foundryup

# Initialize a new project
forge init soulbound-token-project
cd soulbound-token-project

Claude Code Project Structure

Create a Claude Code skill to manage your soulbound token workflow:

# .claude/soulbound-workflow.md
# Soulbound Token Development Workflow

You help developers create, test, and deploy soulbound tokens (SBTs).
Your expertise includes:
- ERC-721 and ERC-5192 token standards
- Soulbound token implementations
- Gas optimization techniques
- Security best practices

When creating SBT contracts:
1. Use OpenZeppelin contracts as base
2. Implement proper access controls
3. Add minting and burning functions
4. Include metadata support
5. Write comprehensive tests

Writing Your First Soulbound Token Contract

The most common approach to creating soulbound tokens is implementing ERC-5192, which defines a minimal interface for soul-bound tokens. Let’s create a complete implementation.

Basic Soulbound Token Contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

/**
 * @title SoulboundToken
 * @dev Implementation of ERC-5192 Soulbound Token standard
 */
contract SoulboundToken is ERC721, ERC721URIStorage, Ownable, ReentrancyGuard {
    
    // Token ID counter
    uint256 private _tokenIdCounter;
    
    // Mapping from token ID to lock status (true = locked/bound)
    mapping(uint256 => bool) private _lockedTokens;
    
    // Mapping from user address to token ID (one token per address)
    mapping(address => uint256) private _userToken;
    
    // Event emitted when a token is minted and bound to an address
    event TokenBound(address indexed to, uint256 indexed tokenId);
    
    // Event emitted when a token is burned/unbound
    event TokenUnbound(address indexed from, uint256 indexed tokenId);
    
    // Event for ERC-5192 compliance
    event Locked(uint256 tokenId);
    event Unlocked(uint256 tokenId);
    
    constructor() ERC721("SoulboundCredential", "SBC") Ownable(msg.sender) {}
    
    /**
     * @dev Mint a new soulbound token to the specified address
     * @param to Address to receive the soulbound token
     * @param uri Metadata URI for the token
     */
    function mint(address to, string memory uri) public onlyOwner nonReentrant {
        require(to != address(0), "Cannot mint to zero address");
        require(_userToken[to] == 0, "Address already has a soulbound token");
        
        uint256 tokenId = _tokenIdCounter++;
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, uri);
        _lockedTokens[tokenId] = true;
        _userToken[to] = tokenId;
        
        emit TokenBound(to, tokenId);
        emit Locked(tokenId);
    }
    
    /**
     * @dev Burn a soulbound token (only callable by owner)
     * @param tokenId ID of the token to burn
     */
    function burn(uint256 tokenId) public onlyOwner {
        require(_lockedTokens[tokenId], "Token is not locked");
        address owner = ownerOf(tokenId);
        
        delete _lockedTokens[tokenId];
        delete _userToken[owner];
        
        _burn(tokenId);
        
        emit TokenUnbound(owner, tokenId);
        emit Unlocked(tokenId);
    }
    
    /**
     * @dev Override transfer function to prevent transfers
     * This enforces the soulbound nature of the token
     */
    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public pure override {
        revert("Soulbound: transfer not allowed");
    }
    
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        bytes memory data
    ) public pure override {
        revert("Soulbound: transfer not allowed");
    }
    
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public pure override {
        revert("Soulbound: transfer not allowed");
    }
    
    /**
     * @dev Check if a token is locked (soulbound)
     * @param tokenId ID of the token to check
     * @return True if the token is locked
     */
    function locked(uint256 tokenId) public view returns (bool) {
        require(_exists(tokenId), "Token does not exist");
        return _lockedTokens[tokenId];
    }
    
    // Required overrides for ERC-721URIStorage
    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }
    
    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}

Testing Your Soulbound Token

Claude Code can help you write comprehensive tests to ensure your soulbound token behaves correctly.

Writing Tests with Foundry

// test/SoulboundToken.t.sol
pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import "../src/SoulboundToken.sol";

contract SoulboundTokenTest is Test {
    SoulboundToken public sbt;
    address public owner;
    address public user1;
    address public user2;
    
    function setUp() public {
        sbt = new SoulboundToken();
        owner = address(this);
        user1 = makeAddr("user1");
        user2 = makeAddr("user2");
    }
    
    function testMintSoulboundToken() public {
        sbt.mint(user1, "https://example.com/credential/1");
        
        assertEq(sbt.ownerOf(1), user1);
        assertEq(sbt.tokenURI(1), "https://example.com/credential/1");
        assertTrue(sbt.locked(1));
    }
    
    function testTransferNotAllowed() public {
        sbt.mint(user1, "https://example.com/credential/1");
        
        vm.prank(user1);
        vm.expectRevert("Soulbound: transfer not allowed");
        sbt.transferFrom(user1, user2, 1);
    }
    
    function testCannotMintTwiceToSameAddress() public {
        sbt.mint(user1, "https://example.com/credential/1");
        
        vm.expectRevert("Address already has a soulbound token");
        sbt.mint(user1, "https://example.com/credential/2");
    }
    
    function testBurnToken() public {
        sbt.mint(user1, "https://example.com/credential/1");
        
        sbt.burn(1);
        
        vm.expectRevert("Token does not exist");
        sbt.ownerOf(1);
    }
}

Run tests with:

forge test

Deploying to Testnet

Use Claude Code to generate deployment scripts and verify contracts on block explorers.

Deployment Script

// scripts/deploy.js
const hre = require("hardhat");

async function main() {
    console.log("Deploying SoulboundToken...");
    
    const SoulboundToken = await hre.ethers.getContractFactory("SoulboundToken");
    const sbt = await SoulboundToken.deploy();
    
    await sbt.waitForDeployment();
    const address = await sbt.getAddress();
    
    console.log(`SoulboundToken deployed to: ${address}`);
    
    // Verify on Etherscan (Sepolia testnet)
    if (hre.network.name === "sepolia") {
        console.log("Verifying contract on Etherscan...");
        try {
            await hre.run("verify:verify", {
                address: address,
                constructorArguments: [],
            });
            console.log("Contract verified!");
        } catch (error) {
            console.log("Verification failed:", error.message);
        }
    }
}

main()
    .then(() => process.exit(0))
    .catch((error) => {
        console.error(error);
        process.exit(1);
    });

Deploy using:

# Deploy to Sepolia testnet
npx hardhat run scripts/deploy.js --network sepolia

Best Practices for Soulbound Token Development

Security Considerations

  1. Access Control: Always use Ownable or role-based access control for minting
  2. Reentrancy Protection: Use ReentrancyGuard for state-modifying functions
  3. Input Validation: Validate all addresses and parameters
  4. Front-Running Protection: Consider adding commit-reveal schemes for sensitive operations

Gas Optimization Tips

UX Improvements

Conclusion

Soulbound tokens represent an emerging standard in Web3 with applications spanning credentials, identity, and reputation systems. By using Claude Code’s development workflow capabilities, you can efficiently implement, test, and deploy secure soulbound token contracts. Start with the basic implementation provided in this guide, then extend it based on your specific requirements—whether that’s multi-token support, governance integration, or advanced metadata handling.

Remember to always audit your smart contracts and consider professional security reviews before deploying to mainnet. Claude Code can help you identify potential vulnerabilities, but final security verification requires expert review.

Built by theluckystrike — More at zovo.one