Implementing Soulbound NFTs with BrightID

Overview
(Inspired by Vitalik’s blog post: Soulbound)
There is a category of NFTs that represent a personal achievement, e.g. Proof Of Attendance Protocol (POAP) or badge for winning a hackathon. A similar category are NFTs that represent governance/voting rights.

Since NFTs today can be freely transferred their meaning/value is not guaranteed.
E.g. I could buy a Gitcoin Hackathon Winner NFT from someone and claim that I have won when applying for a job. Or a malicious user could obtain DAO voting rights by buying/stealing other persons voting NFTs.

This is not in the interest of both issuer and owner.

Naive solution
As an NFT issuer I could design the NFT smart contract so that NFTs can never be transferred after minting. But this would be a problem for cases of compromised wallet or people in general changing or restructuring their addresses/wallets.

BrightID solution
BrightID can help fix this problem in two ways:

  1. Verification: Check the transfer history of a NFT to detect change of owning BrightID
  2. Restriction: Only allow transfer of a NFT if the target address is owned by the same BrightID as the current owner

Option 1 - Verification:
General idea is that any website can check and display the trail of the NFT owning addresses. Current NFT owner can point to that verification trail to prove he is the original owner.

The website would need to

  • Get history of owner addresses
  • For each address check if is linked with a BrightID
  • Warn user if
    • there was a change of BrightID
    • current owner is not linked with a BrightID
    • current brightID is not verified

The NFT owner would need to

  • link current and all previous owning addresses with his BrightID

Option 2 - Transfer restriction
General idea is that the NFT contract enforces binding of the NFT to a BrightID.
The NFT smart contract would need to

  • only allow minting if the target address is linked with a BrightID
  • only allow transferring if the target address is linked with the same BrightID as the current owner
  • optional: require a certain verification for minting

The NFT owner would need to

  • link the NFT owner address with his BrightID before minting or transferring

Thoughts/open questions

  • Is the whole approach feasible with our technology? Can we do this on-chain in a reliable way? I’m still lacking complete understanding of how our app linking process works together with smart contracts.
  • Option 1 seems to offer the easier integration path. It can start just as a signalling tool without actually enforcing anything and evolve to be more strict by time.
  • While option 2 is more restrictive for the user it has the advantage that actual DApps (e.g. a voting DAO) do not need to implement any check on their own. They can rely on the NFT contract to make sure that the NFT never changes ownership to another person.
5 Likes

Both approaches are workable, though Option 1 does put a lot more pressure & work onto a centralized webserver, which is not ideal.
But I did took some time to write a simple proof of concept of Option 2: ERC721Soulbound, check it out if you’re interested.

4 Likes

Both approaches are workable, though Option 1 does put a lot more pressure & work onto a centralized webserver, which is not ideal.

There would not necessarily be one centralized server. Any website/dapp could implement this on their own. E.g. OpenSea, Aragon/Gardens DAO etc - Whoever sees value in showing if a certain NFT is soulbound can integrate this check.
Depending on usecase it could just be a checkmark or traffic light:

  • Green: Current owner address is bound to a BrightID (and all previous owner either unbound or bound to the same brightId)
  • Yellow/grey: Neither current nor any previous owner is bound to a brightID
  • Red: Current and previous owner address are bound to different BrightID

What would be needed is some blockchain index service (thegraph) to obtain the list of previous owners. Manually assembling the list of previous owners by looking at Transfer Events of the NFT contract will take forever.

1 Like

But I did took some time to write a simple proof of concept of Option 2: ERC721Soulbound 1, check it out if you’re interested.

Awesome, very interesting!
Can you provide some more background on what is happening?
I still need to wrap my head around how on-chain verification works… :slight_smile:

In the register method, the node provides all addresses the user has linked his BrightID to. In line 85 you are overwriting all entries of the verifications array with the same timestamp and addrs. This does not look right?

In the _beforeTokenTransfer hook we need to check if the current owner address (from) is linked to the same BrightID as the next owner (to). This is the point where I am not sure this is possible. But maybe I don’t understand the code you created :slight_smile:
I think we only know that an address is verified and that maybe also previous addresses have been verified. But we do not know the link to a BrightID. How can we make sure that 2 verfied addresses are linked with the same BrightID?

2 Likes

Good points, I think ultimately this depends on the type of the application. A more presence-centric NFT would prefer restriction as opposed to verification (since users are still able to view “rogue” NFTs on another site).

2 Likes

I wrote an article going into this in detail: Implementing Soulbound NFTs with BrightID — MisterPlus
Answers to some of your questions:

In line 85 you are overwriting all entries of the verifications array with the same timestamp and addrs.

This is necessary for when a user linked a second address to the same BrightID, since you cannot unlink an address from a BrightID, the new signed data would include both of the addresses. The contract would need to get all addresses associated with the same BrightID to do further checks.

e.g.
A user linked two addresses to the same BrightID, address 0xaa… and 0xbb…
The contract would need to save two mappings,

0xaa => [0xaa..., 0xbb...]
0xbb => [0xaa..., 0xbb...]

So when checking in _beforeTokenTransfer, the contract can just use the from and to field to directly get all addresses associated with the same BrightID.
Now let’s say an attacker would want to steal such an soulbound NFT, the only way they can achieve this is linking a wallet that they control to another person’s BrightID, which they can’t because they don’t control the BrightID.

How can we make sure that 2 verfied addresses are linked with the same BrightID?

This question is essentially the same question, but just to make it clear: Whenever some one calls register they’ll have to submit the signed data along with all addresses linked to that BrightID, otherwise the signature wouldn’t match up. The node signs all data including all linked addresses.

bytes32 message = keccak256(abi.encodePacked(_context, contextIds, timestamp));
address signer = ecrecover(message, v, r, s);

This line of code is responsible for checking the signature, as you can see in abi.encodePacked(_context, contextIds, timestamp), the entire array of contextIds (addresses) needs to be submitted and be correct for the transaction to not revert.

The core reason we have to set it up like this is because there’s no signed data containing information about the actual unique BrightID behind any address. I know this might seem not quite scalable, but for the average users, they won’t need to link the same BrightID to multiple addresses, even if they do it would only be single digits. Besides there might be a more gas efficient way to implement this, we just haven’t thought of it yet.

3 Likes

Ah, understand it now, thank you for clarifying!
That blog post is :+1:

The solution is simple then: Only allow token transfers between the same BrightID, and overwrite the usual approval mechanic used by regular ERC721 standards to allow any address to transfer a soulbound token, as long as the spender, the owner and the recipient are all associated with the same BrightID.

This is a great idea!

4 Likes

I think the Soulbound use case is important enough to keep API v5 around indefinitely (renaming it BrightID-Soulbound). I wrote a new post on the topic.

1 Like