Skip to content

Commit

Permalink
Propose Minimalistic Souldbound Extension for Non-Fungible Tokens
Browse files Browse the repository at this point in the history
Proposed an interface for Non-Fungible Tokens extension allowing for
them to be non-transferrable.
  • Loading branch information
ThunderDeliverer committed Feb 7, 2023
1 parent df83123 commit 3587cb9
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 0 deletions.
87 changes: 87 additions & 0 deletions EIPS/eip-x.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
---
eip: x
title: Minimalistic Souldbound 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: x
status: Draft
type: Standards Track
category: ERC
created: 2023-01-31
requires: 165, 721
---

## Abstract

The Minimalistic Souldbound 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:

- [Foo](#foo)

### Foo

The ability to prevent the tokens to be transferred

## 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-x Minimalistic Souldbound interface for NFTs
/// @dev See https://eips.ethereum.org/EIPS/eip-x
/// @dev Note: the ERC-165 identifier for this interface is 0x0.
pragma solidity ^0.8.16;
interface IERCx is IERC165 {
/**
* @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);
}
```

## Rationale

Designing the proposal, we considered the following questions:

## Backwards Compatibility

The Emotable token standard is fully compatible with [EIP-721](./epi-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-x/test/soulbound.ts).

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

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

## Reference Implementation

See [`Soulbound.sol`](../assets/eip-x/contracts/Soulbound.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-x/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-x/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-x/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-x/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-x/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"
);
});
});

0 comments on commit 3587cb9

Please sign in to comment.