diff --git a/EIPS/eip-7231.md b/EIPS/eip-7231.md new file mode 100644 index 00000000000000..a35ca8e5e7aaa2 --- /dev/null +++ b/EIPS/eip-7231.md @@ -0,0 +1,175 @@ +--- +eip: 7231 +title: Identity aggregated NFT +description: The aggregation of web2 & web3 identities to NFTs, authorized by individuals, gives attributes of ownerships, relationships, experiences. +author: Chloe Gu , Navid X. (@xuxinlai2002), Victor Yu , Archer H. +discussions-to: https://ethereum-magicians.org/t/erc7231-identity-aggregated-nft/15062 +status: Draft +type: Standards Track +category: ERC +created: 2023-06-25 +requires: 165, 721, 1271 +--- + +## Abstract + +This standard extends [ERC-721](./eip-721.md) by binding individuals' Web2 and Web3 identities to non-fungible tokens (NFTs) and soulbound tokens (SBTs). By binding multiple identities, aggregated and composible identity infomation can be verified, resulting in more beneficial onchain scenarios for individuals, such as self-authentication, social overlapping, commercial value generation from user targetting, etc. By adding a custom schema in the metadata, and updating and verifying the schema hash in the contract, the binding of NFT and identity information is completed. + +## Motivation + +One of the most interesting aspects of Web3 is the ability to bring an individual's own identity to different applications. Even more powerful is the fact that individuals truly own their accounts without relying on centralized gatekeepers, disclosing to different apps components necessary for authentication and approved by individuals. +Exisiting solutions such as ENS, although open, decentralized, and more convenient for Ethereum-based applications, suffer from a lack of data standardization and authentication of identity due to inherent anominity. Other solutions such as SBTs rely on centralized attestors, can not prevent data tampering, and do not inscribe data into the ledger itself in a privacy enabling way. +The proposed pushes the boundaries of solving identity problems with Identity Aggregated NFT, i.e., the individual-authenticated aggregation of web2 and web3 identities to NFTs (SBTs included). + +## Specification + +The keywords “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. + +### Every compliant contract must implement the Interface + +```solidity +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.15; + +interface IERC7231 { + + /** + * @notice emit the use binding informain + * @param id nft id + * @param identitiesRoot new identity root + */ + event SetIdentitiesRoot( + uint256 id, + bytes32 identitiesRoot + ); + + /** + * @notice + * @dev set the user ID binding information of NFT with identitiesRoot + * @param id nft id + * @param identitiesRoot multi UserID Root data hash + * MUST allow external calls + */ + function setIdentitiesRoot( + uint256 id, + bytes32 identitiesRoot + ) external; + + /** + * @notice + * @dev get the multi-userID root by NFTID + * @param id nft id + * MUST return the bytes32 multiUserIDsRoot + * MUST NOT modify the state + * MUST allow external calls + */ + function getIdentitiesRoot( + uint256 id + ) external returns(bytes32); + + /** + * @notice + * @dev verify the userIDs binding + * @param id nft id + * @param userIDs userIDs for check + * @param identitiesRoot msg hash to veriry + * @param signature ECDSA signature + * MUST If the verification is passed, return true, otherwise return false + * MUST NOT modify the state + * MUST allow external calls + */ + function verifyIdentitiesBinding( + uint256 id,address nftOwnerAddress,string[] memory userIDs,bytes32 identitiesRoot, bytes calldata signature + ) external returns (bool); +} +``` + +This is the “Metadata JSON Schema” referenced above. + +```json +{ + "title": "Asset Metadata", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Identifies the asset to which this NFT represents" + }, + "description": { + "type": "string", + "description": "Describes the asset to which this NFT represents" + }, + "image": { + "type": "string", + "description": "A URI pointing to a resource with mime type image" + }, + "MultiIdentities": [ + { + "userID": { + "type": "string", + "description": "User ID of Web2 and web3(DID)" + }, + "verifierUri": { + "type": "string", + "description": "Verifier Uri of the userID" + }, + "memo": { + "type": "string", + "description": "Memo of the userID" + }, + "properties": { + "type": "object", + "description": "properties of the user ID information" + } + } + ] + } +} +``` + +## Rationale + +Designing the proposal, we considered the following problems that are solved by this standard: +![EIP Flow Diagram](../assets/eip-7231/img/Identity-aggregated-NFT-flow.png) + +1. Resolve the issue of multiple ID bindings for web2 and web3. +By incorporating the MultiIdentities schema into the metadata file, an authorized bond is established between user identity information and NFTs. This schema encompasses a userID field that can be sourced from a variety of web2 platforms or a decentralized identity (DID) created on blockchain. By binding the NFT ID with the UserIDInfo array, it becomes possible to aggregate multiple identities seamlessly. +1. Users have full ownership and control of their data +Once the user has set the metadata, they can utilize the setIdentitiesRoot function to establish a secure binding between hashed userIDs objects and NFT ID. As only the user holds the authority to carry out this binding, it can be assured that the data belongs solely to the user. +1. Verify the binding relationship between data on-chain and off-chain data through signature based on [ERC-1271](./eip-1271.md) +Through the signature method based on the [ERC-1271](./eip-1271.md) protocol, the verifyIdentiesBinding function of this EIP realizes the binding of the userID and NFT owner address between on-chain and off-chain. + 1. NFT ownership validation + 2. UserID format validation + 3. IdentitiesRoot Consistency verification + 4. Signature validation from nft owner + +As for how to verify the authenticity of the individuals' identities, wallets, accounts, there are various methods, such as zk-based DID authentication onchain, and offchain authentication algorithms, such as auth2, openID2, etc. + +## Backwards Compatibility + +As mentioned in the specifications section, this standard can be fully [ERC-721](./eip-721.md) compatible by adding an extension function set. +In addition, new functions introduced in this standard have many similarities with the existing functions in [ERC-721](./eip-721.md). This allows developers to easily adopt the standard quickly. + +## Test Cases + +Tests are included in [`erc7231.ts`](../assets/eip-7231/test/erc7231.ts). + +To run them in terminal, you can use the following commands: + +``` +cd ../assets/eip-7231 +npm install +npx hardhat test +``` + +## Reference Implementation + +`ERC7231.sol` Implementation: [`ERC7231.sol`](../assets/eip-7231/contracts/ERC7231.sol) + +## Security Considerations + +This EIP standard can comprehensively empower individuals to have ownership and control of their identities, wallets, and relevant data by themselves adding or removing the NFTs and identity bound information. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/assets/eip-7231/contracts/ERC7231.sol b/assets/eip-7231/contracts/ERC7231.sol new file mode 100644 index 00000000000000..85be7e549fac0f --- /dev/null +++ b/assets/eip-7231/contracts/ERC7231.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.17; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; +import { StrSlice, toSlice } from "@dk1a/solidity-stringutils/src/StrSlice.sol"; + + +import "./interfaces/IERC7231.sol"; + + +contract ERC7231 is IERC7231,ERC721 { + + using { toSlice } for string; + mapping (uint256 => bytes32) _idMultiIdentitiesRootBinding; + mapping(uint256 => mapping(bytes32 => bool)) internal _idSignatureSetting; + + constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol) {} + + /** + * @dev Checks if the sender owns NFT with ID tokenId + * @param id NFT ID of the signing NFT + */ + modifier onlyTokenOwner(uint256 id) { + //nft owner check + require(ownerOf(id) == msg.sender,"nft owner is not correct"); + _; + } + + + /** + * @notice + * @dev set the user ID binding information of NFT with multiIdentitiesRoot + * @param id nft id + * @param multiIdentitiesRoot multi UserID Root data hash + */ + function setIdentitiesRoot( + uint256 id, + bytes32 multiIdentitiesRoot + ) external { + + _idMultiIdentitiesRootBinding[id] = multiIdentitiesRoot; + emit SetIdentitiesRoot(id,multiIdentitiesRoot); + } + + /** + * @notice + * @dev Update the user ID binding information of NFT + * @param id nft id + */ + function getIdentitiesRoot( + uint256 id + ) external view returns(bytes32){ + + return _idMultiIdentitiesRootBinding[id]; + } + + /** + * @notice + * @dev verify the userIDs binding + * @param id nft id + * @param multiIdentitiesRoot msg hash to veriry + * @param userIDs userIDs for check + * @param signature ECDSA signature + */ + function verifyIdentitiesBinding( + uint256 id,address nftOwnerAddress,string[] memory userIDs,bytes32 multiIdentitiesRoot, bytes calldata signature + ) external view returns (bool){ + + //nft ownership validation + require(ownerOf(id) == nftOwnerAddress,"nft owner is not correct"); + + //multi-identities root consistency validation + require(_idMultiIdentitiesRootBinding[id] == multiIdentitiesRoot,""); + + //user id format validtion + uint256 userIDLen = userIDs.length; + require(userIDLen > 0,"userID cannot be empty"); + + for(uint i = 0 ;i < userIDLen ;i ++){ + _verifyUserID(userIDs[i]); + } + + //signature validation from nft owner + bool isVerified = SignatureChecker.isValidSignatureNow(msg.sender,multiIdentitiesRoot,signature); + return isVerified; + + } + + /** + * @notice + * @dev verify the userIDs binding + * @param userID nft id + */ + function _verifyUserID(string memory userID) internal view{ + + require(bytes(userID).length > 0,"userID can not be empty"); + + //first part(encryption algorithm or did) check + string memory strSplit = ":"; + bool found; + StrSlice left; + StrSlice right = userID.toSlice(); + (found, left, right) = right.splitOnce(strSplit.toSlice()); + require(found,"the first part delimiter does not exist"); + require(bytes(left.toString()).length > 0,"the first part does not exist"); + + //second part(Organization Information) check + (found, left, right) = right.splitOnce(strSplit.toSlice()); + require(found,"the second part delimiter does not exist"); + require(bytes(left.toString()).length > 0,"the second part does not exist"); + + //id hash check + require(bytes(right.toString()).length == 64,"id hash length is not correct"); + + } + + /** + * @dev ERC-165 support + */ + function supportsInterface( + bytes4 interfaceId + ) public view virtual override returns (bool) { + return + interfaceId == type(IERC7231).interfaceId || + super.supportsInterface(interfaceId); + } + +} + diff --git a/assets/eip-7231/contracts/interfaces/IERC7231.sol b/assets/eip-7231/contracts/interfaces/IERC7231.sol new file mode 100644 index 00000000000000..63dee26bb9a583 --- /dev/null +++ b/assets/eip-7231/contracts/interfaces/IERC7231.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.15; + +interface IERC7231 { + + /** + * @notice emit the use binding informain + * @param id nft id + * @param identitiesRoot new identity root + */ + event SetIdentitiesRoot( + uint256 id, + bytes32 identitiesRoot + ); + + /** + * @notice + * @dev set the user ID binding information of NFT with identitiesRoot + * @param id nft id + * @param identitiesRoot multi UserID Root data hash + * MUST allow external calls + */ + function setIdentitiesRoot( + uint256 id, + bytes32 identitiesRoot + ) external; + + /** + * @notice + * @dev get the multi-userID root by NFTID + * @param id nft id + * MUST return the bytes32 multiUserIDsRoot + * MUST NOT modify the state + * MUST allow external calls + */ + function getIdentitiesRoot( + uint256 id + ) external returns(bytes32); + + /** + * @notice + * @dev verify the userIDs binding + * @param id nft id + * @param userIDs userIDs for check + * @param identitiesRoot msg hash to veriry + * @param signature ECDSA signature + * MUST If the verification is passed, return true, otherwise return false + * MUST NOT modify the state + * MUST allow external calls + */ + function verifyIdentitiesBinding( + uint256 id,address nftOwnerAddress,string[] memory userIDs,bytes32 identitiesRoot, bytes calldata signature + ) external returns (bool); + +} \ No newline at end of file diff --git a/assets/eip-7231/contracts/mocks/ERC7231Mock.sol b/assets/eip-7231/contracts/mocks/ERC7231Mock.sol new file mode 100644 index 00000000000000..b17f155bb2b0a6 --- /dev/null +++ b/assets/eip-7231/contracts/mocks/ERC7231Mock.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.17; + +import "../ERC7231.sol"; + +contract ERC7231Mock is ERC7231 { + + constructor( + string memory name, + string memory symbol + ) ERC7231(name, symbol) {} + + function mint(address to, uint256 tokenId) external { + _mint(to, tokenId); + } + + function transfer(address to, uint256 tokenId) external { + _transfer(msg.sender, to, tokenId); + } + + function burn(uint256 tokenId) external { + _burn(tokenId); + } +} diff --git a/assets/eip-7231/img/Identity-aggregated-NFT-flow.png b/assets/eip-7231/img/Identity-aggregated-NFT-flow.png new file mode 100644 index 00000000000000..28b05bf1a50b82 Binary files /dev/null and b/assets/eip-7231/img/Identity-aggregated-NFT-flow.png differ diff --git a/assets/eip-7231/test/erc7231.ts b/assets/eip-7231/test/erc7231.ts new file mode 100644 index 00000000000000..83bcbed7245df9 --- /dev/null +++ b/assets/eip-7231/test/erc7231.ts @@ -0,0 +1,130 @@ +import { ethers } from "hardhat"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { ERC7231Mock } from "../typechain-types"; + +import { expect } from "chai"; +import web3 from "web3"; + + +describe("ERC7231", async () => { + + let owner : SignerWithAddress; + let others: SignerWithAddress[]; + + let ERC7231Mock: ERC7231Mock; + + const name = "carvTest"; + const symbol = "CVTS"; + const tokenId = 1; + + const MultiUserIDs = [ + { + "userID":"openID2:steam:a000000000000000000000000000000000000000000000000000000000000001", + "verifierUri1":"https://carv.io/verify/steam/a000000000000000000000000000000000000000000000000000000000000001", + "memo":"memo1" + }, + { + "userID":"did:polgyonId:b000000000000000000000000000000000000000000000000000000000000002", + "verifierUri1":"https://carv.io/verify/steam/b000000000000000000000000000000000000000000000000000000000000002", + "memo":"memo1" + } + ] + + beforeEach(async () => { + + [owner, ...others] = await ethers.getSigners(); + + const ERC7231Factory = await ethers.getContractFactory("ERC7231Mock"); + ERC7231Mock = await ERC7231Factory.deploy(name, symbol,); + await ERC7231Mock.deployed(); + + // await ERC7231Mock. + await ERC7231Mock.connect(owner).mint(owner.address,tokenId); + + }); + + describe("Init of Erc721 ", async function () { + + it("Name", async function () { + expect(await ERC7231Mock.name()).to.equal(name); + }); + + it("Symbol", async function () { + expect(await ERC7231Mock.symbol()).to.equal(symbol); + }); + + }); + + describe("set MultiUserIDs Root", async function () { + + it("Normal case", async function () { + + let multiUserIDsHash = "0xa5b9d60f32436310afebcfda832817a68921beb782fabf7915cc0460b443116a" + await expect( + ERC7231Mock.connect(owner).setIdentitiesRoot( + tokenId, + multiUserIDsHash + ) + ).to.emit(ERC7231Mock,"SetIdentitiesRoot").withArgs( + tokenId, + multiUserIDsHash + ); + + let multiUserIDsRoot = await ERC7231Mock.getIdentitiesRoot( + tokenId + ); + + expect(multiUserIDsHash).to.eql(multiUserIDsRoot); + + }); + + }); + + + describe("verify UserIDs Binding", async function () { + + it("Normal case", async function () { + + const dataHash = ethers.utils.keccak256( + ethers.utils.toUtf8Bytes(JSON.stringify(MultiUserIDs)) + ); + const dataHashBin = ethers.utils.arrayify(dataHash); + const ethHash = ethers.utils.hashMessage(dataHashBin); + + // const wallet = new ethers.Wallet(process.env.PK); + const signature = await owner.signMessage(dataHashBin); + + await ERC7231Mock.connect(owner).setIdentitiesRoot( + tokenId,ethHash + ) + + let userIDS = new Array(); + MultiUserIDs.forEach( + (MultiUserIDObj) => { + userIDS.push(MultiUserIDObj.userID) + } + ) + + let result = await ERC7231Mock.verifyIdentitiesBinding( + tokenId, + owner.address, + userIDS, + ethHash, + signature + ) + expect(result).to.eql(true); + + }); + + + }); + + + + + + + + + +});