Alert Source Discuss
🚧 Stagnant Standards Track: ERC

ERC-3440: ERC-721 版本标准

Authors Nathan Ginnever (@nginnever)
Created 2021-04-20
Discussion Link https://ethereum-magicians.org/t/eip-3340-nft-editions-standard-extension/6044
Requires EIP-712, EIP-721

简述

本标准通过允许在代表艺术品的 NFT 上签名来解决 ERC-721 规范 的扩展问题。 这通过为艺术家创建指定其作品的原始签名限量版画的功能来提供改进的来源。

摘要

ERC-3440 是一个 ERC-721 扩展,专门设计用于使 NFT 对于艺术作品更加强大。 这通过提供使用类似于内置的 原始 721 扩展 的专用枚举扩展来指定原始版和限量版画的能力来扩展原始 ERC-721 规范。 此扩展的关键改进是允许艺术家指定其版画的有限性质,并提供代表他们对给定 token Id 的唯一签名的签名数据,很像艺术家签署其作品的版画。

动机

目前,NFT 和数字艺术作品之间的链接仅在存储在 NFT 的共享 tokenURI 状态中的 token 元数据中强制执行。虽然区块链提供了一个不可变的记录历史,可以追溯到 NFT 的起源,但通常起源并不是艺术家像手写签名那样密切维护的关键。

版本是原始艺术品的印刷复制品。 ERC-721 并非专门设计用于艺术作品,例如数字艺术和音乐。 ERC-721 (NFT) 最初是为处理契约和其他合同而创建的。 最终,ERC-721 演变为游戏 token,服务器托管的元数据可能就足够了。 本提案的立场是,我们可以创建一个 NFT、数字艺术、所有者和艺术家之间更具体的联系。 通过为艺术制定一个简洁的标准,艺术家可以更容易地与以太坊区块链以及购买其 token 的粉丝保持联系。

NFT 的用例已经演变为数字艺术作品,并且需要以无需信任的方式指定原始 NFT 和带签名的印刷版本。 ERC-721 合同可能由艺术家部署,也可能不由艺术家部署,并且目前,了解某些东西被艺术家唯一触摸的唯一方法是在第三方应用程序上显示它,这些应用程序假设通过存在于区块链外部的服务器上的元数据建立连接。 本提案通过为艺术家签署其作品提供随时可用的功能来帮助消除这种距离,并为第三方应用程序提供了一个标准,以显示为购买它们的 NFT 的独特性。 限量版的指定与不可变的签名相结合,创建了一个无需信任的强制链接。 此签名附带视图函数,允许应用程序轻松显示这些签名和限量版画,以证明艺术家专门使用其密钥来指定总供应量并签署每个 NFT 的独特性证据。

规范

本文档中的关键词“必须”、“禁止”、“必需”、“应”、“不应”、“建议”、“不建议”、“可以”和“可选”应按照 RFC 2119 中的描述进行解释。

符合 ERC-721 的合同可以实现此 ERC 以用于版本,从而提供一种标准方法来指定艺术家的签名原始版本和限量版画。

ERC-3440 的实现必须指定哪个 token Id 是原始 NFT(默认为 Id 0),以及哪个 token Id 是唯一的副本。 原始印刷品应该是 token Id 数字 0,但可以分配给不同的 Id。 原始印刷品必须仅指定一次。 实现必须指定最大数量的已铸造版本,之后不得印刷/铸造新的 Id。

艺术家可以使用签名功能来签署原始版或限量版画,但这是可选的。 建议使用标准消息来签名,该消息只是 token Id 整数的哈希值。

签名消息必须使用 EIP-712 标准。

符合 ERC-3440 的合约应实现以下抽象合约(称为 ERC3440.sol):

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

/**
 * @dev 具有版本扩展的 ERC721 token。
 */
abstract contract ERC3440 is ERC721URIStorage {

    // eip-712
    struct EIP712Domain {
        string  name;
        string  version;
        uint256 chainId;
        address verifyingContract;
    }
    
    // 要签名的消息内容
    struct Signature {
        address verificationAddress; // 确保艺术家仅为每件作品签署 address(this)
        string artist;
        address wallet;
        string contents;
    }

    // 类型哈希
    bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256(
        "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
    );

    bytes32 constant SIGNATURE_TYPEHASH = keccak256(
        "Signature(address verifyAddress,string artist,address wallet, string contents)"
    );

    bytes32 public DOMAIN_SEPARATOR;
    
    // 用于签名的可选映射
    mapping (uint256 => bytes) private _signatures;
    
    // 用于显示艺术家的地址的视图
    address public artist;

    // 用于显示创建的印刷品总数的视图
    uint public editionSupply = 0;
    
    // 用于显示哪个 ID 是原始副本的视图
    uint public originalId = 0;
    
    // 签名 token 事件
    event Signed(address indexed from, uint256 indexed tokenId);

    /**
     * @dev 将 `artist` 设置为原始艺术家。
     * @param `address _artist` 签名艺术家的钱包(TODO 考虑多个签名者和合约签名者(非 EOA)
     */
    function _designateArtist(address _artist) internal virtual {
        require(artist == address(0), "ERC721Extensions: the artist has already been set");

        // 如果没有为艺术家指定特殊名称,则设置它。
        artist = _artist;
    }
    
    /**
     * @dev 将 `tokenId as the original print` 设置为 `tokenId` 的 tokenURI。
     * @param `uint256 tokenId` 原始印刷品的 nft id
     */
    function _designateOriginal(uint256 _tokenId) internal virtual {
        require(msg.sender == artist, "ERC721Extensions: only the artist may designate originals");
        require(_exists(_tokenId), "ERC721Extensions: Original query for nonexistent token");
        require(originalId == 0, "ERC721Extensions: Original print has already been designated as a different Id");

        // 如果没有为原始指定特殊名称,则设置它。
        originalId = _tokenId;
    }
    

    /**
     * @dev 将原始印刷品的印刷版本总数设置为 `tokenId` 的 tokenURI。
     * @param `uint256 _maxEditionSupply` 最大供应量
     */
    function _setLimitedEditions(uint256 _maxEditionSupply) internal virtual {
        require(msg.sender == artist, "ERC721Extensions: only the artist may designate max supply");
        require(editionSupply == 0, "ERC721Extensions: Max number of prints has already been created");

        // 如果没有最大印刷品供应量,则设置它。 将供应量保留为 0 表示没有原始印刷品
        editionSupply = _maxEditionSupply;
    }

    /**
     * @dev 创建代表印刷版本的 `tokenIds`。
     * @param `string memory _tokenURI` 附加到每个 nft 的元数据
     */
    function _createEditions(string memory _tokenURI) internal virtual {
        require(msg.sender == artist, "ERC721Extensions: only the artist may create prints");
        require(editionSupply > 0, "ERC721Extensions: the edition supply is not set to more than 0");
        for(uint i=0; i < editionSupply; i++) {
            _mint(msg.sender, i);
            _setTokenURI(i, _tokenURI);
        }
    }

    /**
     * @dev 内部哈希实用程序
     * @param `Signature memory _message` 要签名的签名消息结构
     * 此合约的地址在哈希中强制执行
     */
    function _hash(Signature memory _message) internal view returns (bytes32) {
        return keccak256(abi.encodePacked(
            "\x19\x01",
            DOMAIN_SEPARATOR,
            keccak256(abi.encode(
                SIGNATURE_TYPEHASH,
                address(this),
                _message.artist,
                _message.wallet,
                _message.contents
            ))
        ));
    }

    /**
     * @dev 签署代表印刷品的 `tokenId`。
     * @param `uint256 _tokenId` 要签名的 NFT 的 id
     * @param `Signature memory _message` 签名消息
     * @param `bytes memory _signature` 链下创建的签名字节
     *
     * 要求:
     *
     * - `tokenId` 必须存在。
     *
     * 发出 {Signed} 事件。
     */
    function _signEdition(uint256 _tokenId, Signature memory _message, bytes memory _signature) internal virtual {
        require(msg.sender == artist, "ERC721Extensions: only the artist may sign their work");
        require(_signatures[_tokenId].length == 0, "ERC721Extensions: this token is already signed");
        bytes32 digest = hash(_message);
        address recovered = ECDSA.recover(digest, _signature);
        require(recovered == artist, "ERC721Extensions: artist signature mismatch");
        _signatures[_tokenId] = _signature;
        emit Signed(artist, _tokenId);
    }

    
    /**
     * @dev 显示来自艺术家的签名。
     * @param `uint256 _tokenId` NFT id 以验证 isSigned
     * @returns `bytes` 获取存储在 token 上的签名
     */
    function getSignature(uint256 _tokenId) external view virtual returns (bytes memory) {
        require(_signatures[_tokenId].length != 0, "ERC721Extensions: no signature exists for this Id");
        return _signatures[_tokenId];
    }
    
    /**
     * @dev 如果消息由艺术家签名,则返回 `true`。
     * @param `Signature memory _message` 由艺术家签名并在其他地方发布的消息
     * @param `bytes memory _signature` 消息上的签名
     * @param `uint _tokenId` 要验证为已签名的 token 的 id
     * @returns `bool` 如果由艺术家签名则为 true
     * 艺术家可以带外广播签名,该签名将在 nft 上验证
     */
    function isSigned(Signature memory _message, bytes memory _signature, uint _tokenId) external view virtual returns (bool) {
        bytes32 messageHash = hash(_message);
        address _artist = ECDSA.recover(messageHash, _signature);
        return (_artist == artist && _equals(_signatures[_tokenId], _signature));
    }

    /**
    * @dev 实用函数,用于检查两个 `bytes memory` 变量是否相等。 这是使用哈希完成的,
    * 这比单独比较每个字节更节省 gas。
    * 相等意味着:
    *  - 'self.length == other.length'
    *  - 对于 '[0, self.length)' 中的 'n','self[n] == other[n]'
    */
    function _equals(bytes memory _self, bytes memory _other) internal pure returns (bool equal) {
        if (_self.length != _other.length) {
            return false;
        }
        uint addr;
        uint addr2;
        uint len = _self.length;
        assembly {
            addr := add(_self, /*BYTES_HEADER_SIZE*/32)
            addr2 := add(_other, /*BYTES_HEADER_SIZE*/32)
        }
        assembly {
            equal := eq(keccak256(addr, len), keccak256(addr2, len))
        }
    }
}

合理依据

NFT 的主要作用是在数字艺术中展示独特性。 出处是艺术作品的理想特征,此标准将通过提供更好的验证唯一性的方法来帮助改进 NFT。 艺术家采取这一额外步骤来明确签署 token,从而在艺术家和他们在区块链上的作品之间建立更好的联系。 艺术家现在可以保留他们的私钥并在未来签署消息,表明唯一的 NFT 上存在相同的签名。

向后兼容性

此提案结合了已经可用的 721 扩展,并且与 ERC-721 标准向后兼容。

测试用例

可以在此处找到包含测试的示例实现。

参考实现

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./ERC3440.sol";

/**
 * @dev 具有版本扩展的 ERC721 token。
 */
contract ArtToken is ERC3440 {

    /**
     * @dev 将 `address artist` 设置为将 NFT 部署到帐户的原始艺术家。
     */
     constructor (
        string memory _name, 
        string memory _symbol,
        uint _numberOfEditions,
        string memory tokenURI,
        uint _originalId
    ) ERC721(_name, _symbol) {
        _designateArtist(msg.sender);
        _setLimitedEditions(_numberOfEditions);
        _createEditions(tokenURI);
        _designateOriginal(_originalId);

        DOMAIN_SEPARATOR = keccak256(abi.encode(
            EIP712DOMAIN_TYPEHASH,
            keccak256(bytes("Artist's Editions")),
            keccak256(bytes("1")),
            1,
            address(this)
        ));
    }
    
    /**
     * @dev 签署代表印刷品的 `tokenId`。
     */
    function sign(uint256 _tokenId, Signature memory _message, bytes memory _signature) public {
        _signEdition(_tokenId, _message, _signature);
    }
}

安全考虑事项

此扩展使艺术家能够指定原始版本,设置版本的最大供应量以及打印版本,并使用 tokenURI 扩展来提供到艺术作品的链接。 为了最大程度地降低艺术家在出售原始作品后更改此值的风险,此函数只能发生一次。 确保这些功能只能发生一次,从而提供与唯一性和可验证性的一致性。 因此,参考实现处理构造函数函数中的这些功能。 一个版本只能签名一次,并且应注意在发布 token 之前正确签署该版本。

版权

通过 CC0 放弃版权及相关权利。

Citation

Please cite this document as:

Nathan Ginnever (@nginnever), "ERC-3440: ERC-721 版本标准 [DRAFT]," Ethereum Improvement Proposals, no. 3440, April 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-3440.