Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Propose Minimalistic Souldbound Extension for Non-Fungible Tokens #6454

Merged
merged 10 commits into from
Feb 7, 2023
107 changes: 107 additions & 0 deletions EIPS/eip-6454.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
eip: 6454
title: Minimalistic Soulbound interface for NFTs
description: An interface for Soulbound Non-Fungible Tokens extension allowing for tokens to be non-transferrable.
author: Bruno Škvorc (@Swader), Francesco Sullo (@sullof), Steven Pineda (@steven2308), Stevan Bogosavljevic (@stevyhacker), Jan Turk (@ThunderDeliverer)
discussions-to: https://ethereum-magicians.org/t/minimalistic-transferable-interface/12517
status: Draft
type: Standards Track
category: ERC
created: 2023-01-31
requires: 165, 721
---

## Abstract

The Minimalistic Soulbound interface for Non-Fungible Tokens standard extends [EIP-721](./eip-721.md) by preventing NFTs to be transferred.

This proposal introduces the ability to prevent a token to be transferred from their owner, making them bound to the externally owned account, smart contract or token that owns it.

## Motivation

With NFTs being a widespread form of tokens in the Ethereum ecosystem and being used for a variety of use cases, it is time to standardize additional utility for them. Having the ability to prevent the tokens to be transferred introduces new possibilities of NFT utility and evolution.

This proposal is designed in a way to be as minimal as possible in order to be compatible with any usecases that wish to utilize this proposal.

This EIP introduces new utilities for [EIP-721](./eip-721.md) based tokens in the following areas:

- [Verifiable attribution](#verifiable-attribution)
- [Immutable properties](#immutable-properties)

### Verifiable attribution

Personal achievements can be represented by non-fungible tokens. These tokens can be used to represent a wide range of accomplishments, including scientific advancements, philanthropic endeavors, athletic achievements, and more. However, if these achievement-indicating NFTs can be easily transferred, their authenticity and trustworthiness can be called into question. By binding the NFT to a specific account, it can be ensured that the account owning the NFT is the one that actually achieved the corresponding accomplishment. This creates a secure and verifiable record of personal achievements that can be easily accessed and recognized by others in the network. The ability to verify attribution helps to establish the credibility and value of the achievement-indicating NFT, making it a valuable asset that can be used as a recognition of the holder's accomplishments.

### Immutable properties

NFT properties are a critical aspect of non-fungible tokens, serving to differentiate them from one another and establish their scarcity. Centralized control of NFT properties by the issuer, however, can undermine the uniqueness of these properties.

By tying NFTs to specific properties, the original owner is ensured that the NFT will always retain these properties and its uniqueness.

In a blockchain game that employs soulbound NFTs to represent skills or abilities, each skill would be a unique and permanent asset tied to a specific player or token. This would ensure that players retain ownership of the skills they have earned and prevent them from being traded or sold to other players. This can increase the perceived value of these skills, enhancing the player experience by allowing for greater customization and personalization of characters.

## Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

```solidity
/// @title EIP-6454 Minimalistic Soulbound interface for NFTs
/// @dev See https://eips.ethereum.org/EIPS/eip-6454
/// @dev Note: the ERC-165 identifier for this interface is 0x911ec470.

pragma solidity ^0.8.16;

interface IERC6454 is IERC165 {
/**
* @notice Used to check whether the given token is soulbound or not.
* @dev If this function returns `true`, the transfer of the token MUST revert execution
* @param tokenId ID of the token being checked
* @return Boolean value indicating whether the given token is soulbound
*/
function isSoulbound(uint256 tokenId) external view returns (bool);
}
```

## Rationale

Designing the proposal, we considered the following questions:

1. **Should we propose another Soulbound NFT proposal given the existence of existing ones, some even final, and how does this proposal compare to them?**\
This proposal aims to provide the minimum necessary specification for the implementation of soulbound NFTs, we feel none of the existing proposals have presented the minimal required interface. Unlike other proposals that address the same issue, this proposal requires fewer methods in its specification, providing a more streamlined solution.
2. **Why is there no event marking the token as Soulbound in this interface?**\
The token can become soulbound either at its creation, after being marked as soulbound, or after a certain condition is met. This means that some cases of tokens becoming soulbound cannot emit an event, such as if the token becoming soulbound is determined by a block number. Requiring an event to be emitted upon the token becoming soulbound is not feasible in such cases.
3. **Should the soulbound state management function be included in this proposal?**\
A function that marks a token as soulbound or releases the binding is referred to as the soulbound management function. To maintain the objective of designing an agnostic soulbound proposal, we have decided not to specify the soulbound management function. This allows for a variety of custom implementations that require the tokens to be non-transferable.
4. **Why should this be an EIP if it only contains one method?**\
One could argue that since the core of this proposal is to only prevent EIP-721 tokens to be transferred, this could be done by overriding the transfer function. While this is true, the only way to assure that the token is soulbound before the smart contract execution, is for it to have the soulbound interface.\
This also allows for smart contract to validate that the token is soulbound and not attempt transferring it as this would result in failed transactions and wasted gas.

## Backwards Compatibility

The Minimalistic Soulbound token standard is fully compatible with [EIP-721](./eip-721.md) and with the robust tooling available for implementations of EIP-721 as well as with the existing EIP-721 infrastructure.

## Test Cases

Tests are included in [`soulbound.ts`](../assets/eip-6454/test/soulbound.ts).

To run them in terminal, you can use the following commands:

```
cd ../assets/eip-6454
npm install
npx hardhat test
```

## Reference Implementation

See [`ERC721SoulboundMock.sol`](../assets/eip-6454/contracts/mocks/ERC721SoulboundMock.sol).

## Security Considerations

The same security considerations as with [EIP-721](./eip-721.md) apply: hidden logic may be present in any of the functions, including burn, add asset, accept asset, and more.

Caution is advised when dealing with non-audited contracts.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
13 changes: 13 additions & 0 deletions assets/eip-6454/contracts/ISoulbound.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: CC0-1.0

pragma solidity ^0.8.16;

interface ISoulbound {

/**
* @notice Used to check whether the given token is soulbound or not.
* @param tokenId ID of the token being checked
* @return Boolean value indicating whether the given token is soulbound
*/
function isSoulbound(uint256 tokenId) external view returns (bool);
}
61 changes: 61 additions & 0 deletions assets/eip-6454/contracts/mocks/ERC721SoulboundMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: CC0-1.0

pragma solidity ^0.8.16;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "../ISoulbound.sol";

error CannotTransferSoulbound();

/**
* @title ERC721SoulboundMock
* Used for tests
*/
contract ERC721SoulboundMock is ISoulbound, ERC721 {
constructor(
string memory name,
string memory symbol
) ERC721(name, symbol) {}

function mint(address to, uint256 amount) public {
_mint(to, amount);
}

function burn(uint256 tokenId) public {
_burn(tokenId);
}

function isSoulbound(uint256 tokenId) public view returns (bool) {
return true;
}

function _beforeTokenTransfer(
address from,
address to,
uint256 firstTokenId,
uint256 batchSize
) internal virtual override {
super._beforeTokenTransfer(from, to, firstTokenId, batchSize);

// exclude minting and burning
if ( from != address(0) && to != address(0)) {
uint256 lastTokenId = firstTokenId + batchSize;
for (uint256 i = firstTokenId; i < lastTokenId; i++) {
uint256 tokenId = firstTokenId + i;
if (isSoulbound(tokenId)) {
revert CannotTransferSoulbound();
}
unchecked {
i++;
}
}
}
}

function supportsInterface(
bytes4 interfaceId
) public view virtual override(ERC721) returns (bool) {
return interfaceId == type(ISoulbound).interfaceId
|| super.supportsInterface(interfaceId);
}
}
17 changes: 17 additions & 0 deletions assets/eip-6454/hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-chai-matchers";
import "@typechain/hardhat";

const config: HardhatUserConfig = {
solidity: {
version: "0.8.16",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
};

export default config;
28 changes: 28 additions & 0 deletions assets/eip-6454/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "soulbound-tokens",
"scripts": {
"test": "yarn typechain && hardhat test",
"typechain": "hardhat typechain",
"prettier": "prettier --write ."
},
"engines": {
"node": ">=16.0.0"
},
"dependencies": {
"@openzeppelin/contracts": "^4.6.0"
},
"devDependencies": {
"@nomicfoundation/hardhat-chai-matchers": "^1.0.1",
"@nomicfoundation/hardhat-network-helpers": "^1.0.3",
"@nomiclabs/hardhat-ethers": "^2.2.1",
"@typechain/ethers-v5": "^10.1.0",
"@typechain/hardhat": "^6.1.2",
"@types/chai": "^4.3.1",
"chai": "^4.3.6",
"ethers": "^5.6.9",
"hardhat": "^2.12.2",
"ts-node": "^10.8.2",
"typechain": "^8.1.0",
"typescript": "^4.7.4"
}
}
57 changes: 57 additions & 0 deletions assets/eip-6454/test/soulbound.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { ethers } from "hardhat";
import { expect } from "chai";
import { BigNumber } from "ethers";
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { ERC721SoulboundMock } from "../typechain-types";

async function soulboundTokenFixture(): Promise<ERC721SoulboundMock> {
const factory = await ethers.getContractFactory("ERC721SoulboundMock");
const token = await factory.deploy("Chunky", "CHNK");
await token.deployed();

return token;
}

describe("Soulbound", async function () {
let soulbound: ERC721SoulboundMock;
let owner: SignerWithAddress;
let otherOwner: SignerWithAddress;
const tokenId = 1;

beforeEach(async function () {
const signers = await ethers.getSigners();
owner = signers[0];
otherOwner = signers[1];
soulbound = await loadFixture(soulboundTokenFixture);

await soulbound.mint(owner.address, 1);
});

it("can support IRMRKSoulbound", async function () {
expect(await soulbound.supportsInterface("0x911ec470")).to.equal(true);
});

it("does not support other interfaces", async function () {
expect(await soulbound.supportsInterface("0xffffffff")).to.equal(false);
});

it("cannot transfer", async function () {
expect(
soulbound
.connect(owner)
["safeTransferFrom(address,address,uint256)"](
owner.address,
otherOwner.address,
tokenId
)
).to.be.revertedWithCustomError(soulbound, "CannotTransferSoulbound");
});

it("can burn", async function () {
await soulbound.connect(owner).burn(tokenId);
await expect(soulbound.ownerOf(tokenId)).to.be.revertedWith(
"ERC721: invalid token ID"
);
});
});