A Prototype for a Fungible, Soulbound and Revocable Token

FungibelSBT icon

Soulbound Tokens (SBTs) were recently proposed by Ethereum’s founder Vitalik Buterin for blockchain implementations of assets which are strictly tied to an identity (or soul) such as degrees, lending or reputation. In contrast to more commonly used types of tokens SBTs can only be issued to an account once and can not be transferred further to any other address. This entails that the token can not be sold or exchanged for other currencies.

Among possible use cases might be things such as loyalty points, renting contracts, awards, credit reports, attendence certifications or reputation scores. While many of these can be imagined as unique non-fungible artifacts (NFTs) others – like reputation score or loyalty points – are rather fungible tokens in the sense of the ERC20 standard.

In this post we will examine a possible implementation of a soulbound fungible token standard. Furthermore, I propose an extension of the token which makes it depositable as a collateral. For example in the case of reputation so that the activities which increase reputation can also potentially risk the loss of reputation. Or so that loyalty points can be burned in exchange for rewards.

What are the properties we want the Fungible Soulbound Token to have?

1. Nontransferability – The token can not be transferred to another account or exchanged.

2. Revocability – The token can be burned by the entity which issued it.

3. Fungibility – The individual unitary valued tokens are indistinguishable from each other.

How would a possible implementation look like?

Since the standard template of fungible tokens is the ERC20 contract we orient ourselves on the reference implementation of the IERC20 interface in the Openzeppelin contract library. The interface defines the blueprint for a token with the getter functions balanceOf() and totalSupply() which we will also implement in the FungibleSBT contract. Different from the ERC20 contract we keep track of two separate balances – the balance of unassigned tokens which were not yet issued but only distributed or minted and the balance of issued tokens. This mechanism allows us to mint the tokens to an account first without directly issuing them and rendering them unspendable. The first and only transfer called issuance is thereby decoupled from the minting process which makes it possible to control the supply of tokens independently of the issue mechanism. We might define a constant fixed supply distributed to a predefined list of recipients such as institutions, DAOs or ordinary account. Alternatively, the token can also be distributed as a mining reward.

Basic functionalities of the fungible soul bound token

We define these two additional getter functions for the fungible soulbound token.

Solidity
/**
* @notice Get the balance of minted but not issued token.
* @param account The address to query the balance
* @return The balance of not yet assigned tokens of the address
*/
function unassignedBalanceOf(address account) external view returns (uint256);

/**
* @notice Get the amount of tokens issued by spender to owner.
* @param from The address of the owner
* @param to The address of the spender
* @return The total issuance of spender to owner
*/
function getIssuance(address from, address to) external view returns (uint256);

The getIssuance() function records all the issuing transactions so they can be verified and revoked easily. Next, we need functions to actually issue and revoke tokens.

Solidity
/**
* @notice Issue an amount of tokens to an address.
* @dev MUST revert if the `to` address is the zero address.
* @param to The address to issue the token to
* @param amount The amount of tokens
*/
function issue(
    address to,
    uint256 amount
) external returns (bool);

/**
* @notice Revoke/burn tokens.
* @param account The account
* @param amount The amount of tokens
*/
function revoke(
    address account,
    uint256 amount
) external returns (bool);

These are the external functions defined in the contract interface. To implement the actual contract we need a number of helper functions such as _mint(), _burn() or _transfer() which the contract uses internally. These can be adapted with few changes from a reference implementation of the ERC20 token contract.

Check out the SoulboundSBT.sol contract on the Github project page to find an implementation of the internal and interface functions. This contract should present a sufficient implementation of the idea of a fungible SBT. In some cases we might want more functionality to interact with the soulbound token. For example it might be possible that we want to extend the authority to revoke tokens to other entities. This might be useful to prevent rapid inflation of the token and ensure the authenticity of an entity. An entity that issues soulbound tokens can define a procedure for the issuance which requires the applicant to give some tokens as a collateral. If the application is approved by the issuer the collateral can be returned and otherwise it will be burned resulting in a loss of capital for the applicant and a certain risk coupled to token gains.

This image has an empty alt attribute; its file name is FungibleSBTDepositable-1024x542.png

To achieve this we use the following functions.

Solidity
/**
* @dev Sets `amount` as allowance of `revoker` to burn caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* @param revoker address of the account burning the tokens.
* @param amount allowance of tokens which may be burned by the revoker.
*/
function grantCollateral(address revoker, uint256 amount) external returns (bool);

/**
* @notice provides burn authorization of a specific amount of tokens to an address
* @dev unassigned tokenIds are invalid, and queries do throw
* @param holder address of the account holding the tokens to be burned
* @param revoker address of the account burning the tokens.
*/
function collateralDeposit(address holder, address revoker) external view returns (uint256);

/**
* @notice Revoke/burn tokens.
* @param account The account
* @param amount The amount of tokens
*/
function burnDeposit(
    address account,
    uint256 amount
) external returns (bool);

/**
* @notice Return the revocation authorisations.
* @param account The account
* @param amount The amount of tokens
*/
function returnDeposit(
    address account,
    uint256 amount
) external returns (bool);

The implementation of these functions can be found in the github project as well. The idea is quite similar to the allowance mechanism in the ERC20 contract. There is a mapping of two addresses to an integer which represents the number of tokens one account is allowed to burn from the other.

What is it good for?

I think such a depositable soulbound token would be a good base for a reputation score. It can not be transferred or purchased, does not have to be issued and stored by a central entity and can not be gamed easily if collateral fees are required to gain more tokens. The collateral can be nicely coupled to a mechanism which rewards community acceptance of certain actions or opinions. This could turn it into a powerful incentive system for things such as scientific peer review or product reviews.