-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Propose Minimalistic Souldbound Extension for Non-Fungible Tokens
Proposed an interface for Non-Fungible Tokens extension allowing for them to be non-transferrable.
- Loading branch information
1 parent
df83123
commit 3587cb9
Showing
6 changed files
with
263 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
); | ||
}); | ||
}); |