Before we start, let’s address the obvious problem here – yes, I know I was supposed to complete the “End-to-End Password-less Authentication in Go” series. And I will. Just catching a break here with this one. Will get started again on that series once more in the coming weeks.
Earlier this year, Vitalik Buterin co-authored this paper which made headlines and was touted as something “revolutionary” – the Soulbound Tokens. It might have been, though, had it been released a couple of years back. I fall into that category of people who’d like to argue otherwise. It’s just a fancy Decentralized Identifier. And until we have a solid implementation (something like say, an Openzeppelin GA release) we cannot really say otherwise.
Soulbound Tokens
In layman’s terms, a Soulbound token is a digital token recorded on the blockchain that can only be minted once per address and will contain information about characteristics that might help identify that address.
The paper which coined the term can be read at this link. I would suggest that every reader of this article try to read that at least once. This article will try to create a contract that mints such tokens.
Criticism
This section discusses my views on Soulbound tokens. So, if you are here just for the code and an explanation of the code, please feel free to skip to the next section.
Okay, so if you are reading this line then you have decided to hear me out. That’s awesome. Thanks for sticking around to hear me rant about unpopular opinions.
Firstly, we already have Self-sovereign Identities in Blockchain. Considerable research has already been done in that domain already. We also have a standard for Decentralized Identifiers. Projects like Hyperledger Indy, Affinidi and Accredify etc. already deal with verifiable credentials. The first one is already on the blockchain while the others have it on their roadmaps. As such, Soulbound tokens just become “a not-so-special” nomenclature – not even an umbrella term.
Secondly, the paper discusses mapping tokens to addresses. It creates an analogy between a “wallet address” and a “soul”. I respectfully disagree. A Wallet address cannot be considered equivalent to a “soul”. A wallet address is more like an identity card one would wear and shed off. A proper analogy, in that case, can exist between a “soul” and a “wallet seed phrase”. And because there is no way to identify if two wallet addresses are derived from the same seed phrase, the term itself becomes a misnomer.
Let’s say for the sake of the argument that there was such a way to find out if two wallet addresses belong to the same seed phrase without revealing the actual seed phrase (I feel like I am giving out a million-dollar idea here) by using something like Zero Knowledge Proofs, then that would violate the actual purpose of public, trustless blockchains – essentially going against the very intention of Satoshi Nakamoto. It would put an end to anonymity – something that forms an essential part of most popular blockchains.
We need to carefully consider the cascading consequences of what the aforementioned situation entails. These are my views on the subject. Notwithstanding these arguments, there is considerable buzz around Soulbound tokens and so in the next section, we get to the code.
If you have a different opinion, I would really like to hear that so please drop a comment below and let's have a healthy discussion, shall we?
Code
Below is the code for what an SBT created by modifying an ERC 721 token might look like. Again, keep in mind, that this is not a concrete implementation. For that, you need to wait around for the Openzeppelin release.
In the code above, we define a contract – OurSBT. It inherits from ERC721Enumerable
. For those unaware of what that is – it is an extension on top of the standard ERC721. It offers a way to count the tokens minted by our contract. We also inherit from Ownable for access control purposes.
Line 15 is where part of the fun begins. In the safeMint()
function, we check if the address that the token is being minted to has already minted a token from our contract or not. Keep in mind that thanks to the onlyOwner
modifier only the contract owner can mint. This is in line with a situation where the token is minted by an authority to signify authentication and authorization. Like a college giving identity to a student or teacher, a guard in front of a bar putting stamps on the hands of guests and or a company giving authorization to an employee for a project and stuff like that.
If you want an implementation where anyone can mint for themselves, a slight modification would do and the safeMint()
function would look similar to the one below.
function safeMint() public payable {
require(balanceOf(_msgSender()) == 0, "Already Minted to Address!");
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(to, tokenId);
}
Keep in mind that it is always recommended to use _msgSender()
instead of the traditional msg.sender
as it helps make your contract robust and future-proof. All Openzeppelin contracts support this.
Next down, the _beforeTokenTransfer()
hook is where the magic happens. This hook is executed before any token is transferred from one address to another. Keep in mind that minting is transferring from 0x00
address to the address being minted to while burning is the reverse of that. So here we need to check if the transfer action pertains to minting.
This is also where two diverging schools of thought emerge. One of them argues that an SBT should not be burnable. After all, a “soul” cannot be burned – it would be tantamount to death. While another argues that the burning of SBTs should be allowed because of that very reason.
I actually would prefer to side with the former. But if you believe that SBTs should be burnable, then adding a simple check in the _beforeTokenTransfer()
hook like the one below would suffice.
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId
) internal override {
super._beforeTokenTransfer(from, to, tokenId);
require(from == address(0) || to == address(0), "Not Allowed");
}
The above should suffice as we will now be checking either of the two addresses for the 0x00
address and that should tell us if the action is minting or burning. Otherwise, the action is a general transfer between wallet addresses and it would revert. Also, you would need to either inherit from ERC721Burnable
(in which case you would need to specifically state override for safeMint
) or create a burn()
method for yourself like the one given below.
Keep in mind that the above modification to OurSBT also exposes it to an attack where an address can repeatedly mint and burn. For this, if you are implementing burnable Soulbounds, the contract should have a mapping to map every address to boolean. This mapping would track if a wallet address has minted or not.
supportsInterface()
is a utility function that you don’t need to concern yourself with much. We won’t be deploying this contract in this article as I have showed already how to do that time and again and you can refer to my previous articles for the steps.
And that’s about it. We have made a Soulbound token and are ready to roll.
Conclusion
In this article, we went through a 1000 feet overview of what Soulbound tokens are, I ranted about my biases of the said convention and then we coded a Soulbound token from ERC721. This article is not meant to be a primer on SBTs. So, if you think I might have missed anything important here, please feel free to start a discussion below. It is always encouraging when such interactions take place.
I will be getting back to completing my series “End-to-End Password-less Authentication in Go”. So hope to see you all there! Until then, keep building awesome things on Web3 and WAGMI.
Top comments (0)