What are Soul bound tokens(SBT)?

What are Soul bound tokens(SBT)?

Soulbound tokens are a type of non-fungible token (NFT) that is permanently linked or "bound" to a specific user's account. In other words, once a soulbound token is created and assigned to a user, it cannot be transferred or sold to another user.

Soulbound tokens are typically used in gaming and virtual world environments to represent unique in-game items, such as weapons or armor, that are earned or awarded to a specific player. Because these items are bound to the player's account, they cannot be traded or sold, which helps to maintain the integrity of the game's economy and prevent cheating or fraud.
Another segment we can use SBT is the education field. where on completion of a course on Udemy or your degree certification. We can also build applications that create SBT associated with government-issued cards, such as a driving license or your Aadhar card.

I’ve just shown you the tip of the iceberg, the use cases are endless, If you want to understand more about here’s an excellent blog by the man himself Vitalik Buterin

In some cases, soulbound tokens may also be used to represent access to specific content or services, such as a subscription to a premium service or a ticket to a restricted event. In these cases, the token is bound to the user's account and cannot be transferred or resold to another user.

Soulbound tokens are created using smart contracts on a blockchain, which ensures that the tokens are secure, transparent, and tamper-proof. By binding the token to a specific user's account, soulbound tokens help to create a sense of ownership and exclusivity, which can be a powerful motivator for users in gaming and virtual world environments.

Understanding the crux of the blog

Today we’ll be writing and deploying an NFT smart contract which is an EIP4973 complaint i.e. Soulbound/Account bound token and we as the owner can mint and send the tokens to eligible people so that they can view it on Opensea, or any other marketplace.

For this, we’ll first create an ERC721 NFT smart contract and then modify it along the way to make it EIP4973 compliant

ERC721 Contract.

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

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

contract SoulboundGov is ERC721, ERC721URIStorage, Ownable {
    using Counters for Counters.Counter;

    Counters.Counter private _tokenIdCounter;

    //Events
    event Attest(address indexed to, uint256 indexed tokenId);
    event Revoke(address indexed to, uint256 indexed tokenId);

    constructor() ERC721("SoulBound-AadharCard", "GOV") {}

    function _baseURI() internal pure override returns (string memory) {
        return "ipfs://bafkreidwx6v4vb4tkhhcjdjnmpndxqqcxp6bnowtkzehmncpknx3x2rfhi";
    }
    function safeMint(address to) public onlyOwner {
        uint256 tokenId = _tokenIdCounter.current();
        _tokenIdCounter.increment();
        _safeMint(to, tokenId);
    }

    // The following functions are overrides required by Solidity. 
    // And its internal, so that means it can be called only within this contract
    function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
        super._burn(tokenId);
    }
    function burn(uint256 tokenId) external {
        require(msg.sender==ownerOf(tokenId),"You dont have ownership of this token");
        _burn(tokenId);
    } 
    function tokenURI(uint256)
        public
        pure
        override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return _baseURI();
    }
    //this is inbuilt, it acts basically like a hook that executes before the token is transfered;
    //The virtual keyword basically says that the child contract can override this contract using the override keyword
    function _beforeTokenTransfer(address from,address to,uint256 /*tokenId*/) override internal virtual {
        require(from==address(0)||to==address(0),"you cannot transfer this token as its soul bound");
    }
        function _afterTokenTransfer(
        address from,
        address to,
        uint256 firstTokenId
    ) internal override virtual {
        if(from == address(0)) {
            emit Attest(to, firstTokenId);
        } else if (to == address(0)) {
            emit Revoke(to, firstTokenId);
        }
    }
}

So I created a NFT and tried sending it to my other account and as expected, it failed.

Now although I've explained the above 2 main functions in the code, I'm gonna explain it again

    function _beforeTokenTransfer(address from,address to,uint256 /*tokenId*/) 
override
internal 
virtual {
        require(from==address(0)||to==address(0),"you     cannot transfer this token as its soul bound");

    }

So basically it says you can mint it or you can sent it back to the contract that minted it for you!. snding it anywhere else is not possible.
The above function is basically like a hook that runs before the NFT is sent. you dont have to worry about the inderlying details as its virtual, so you can override it.

    function _beforeTokenTransfer(address from,address to,uint256 /*tokenId*/) override internal virtual {
        require(from==address(0)||to==address(0),"you cannot transfer this token as its soul bound");
    }
        function _afterTokenTransfer(
        address from,
        address to,
        uint256 firstTokenId
    ) internal override virtual {
        if(from == address(0)) {
            emit Attest(to, firstTokenId);
        } else if (to == address(0)) {
            emit Revoke(to, firstTokenId);
        }
    }
}

The second function is similar to the first, except its run after the token is sent.
here its basically used to send a event.

Etherscan will given you what require failed.