Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-7738: 免许可脚本注册表

用于获取合约可执行脚本的免许可注册表

Authors Victor Zhang (@zhangzhongnan928), James Brown (@JamesSmartCell)
Created 2024-07-01
Discussion Link https://ethereum-magicians.org/t/erc-7738-permissionless-script-registry/20503
Requires EIP-173

摘要

此 EIP 提供了一种创建标准注册表的方法,用于查找与 token 关联的可执行脚本。

动机

ERC-5169 为合约提供了一种客户端脚本查找方法。这要求合约在构建时实现 ERC-5169 接口(或允许升级路径)。

本提案概述了一种可以提供原型和认证脚本的合约。该合约将是一个多链单例实例,将部署在受支持链上的相同地址。

概述

注册表合约将为给定的合约地址提供一组 URI 链接。这些 URI 链接指向可以由钱包、查看器或迷你 dApp 获取的脚本程序。

可以使用注册表合约中的 setter 免许可地设置这些指针。

规范

本文档中的关键词“MUST”、“MUST NOT”、“REQUIRED”、“SHALL”、“SHALL NOT”、“SHOULD”、“SHOULD NOT”、“RECOMMENDED”、“MAY”和“OPTIONAL”应按照 RFC 2119 中的描述进行解释。

合约必须实现 IERC7738 接口。 合约必须在脚本更新时发出 ScriptUpdate 事件。 合约应以这样的顺序返回 scriptURI,即使得合约中 ERC-173 owner() 的脚本条目最先返回(在简单实现的情况下,钱包会选择第一个返回的 scriptURI)。 如果 scriptURI 条目数量很大,合约应提供一种分页浏览条目的方法。

interface IERC7738 {
    /// @dev 此事件在 scriptURI 更新时发出,
    /// 因此实现此接口的钱包可以更新缓存的脚本
    event ScriptUpdate(address indexed contractAddress, string[] newScriptURI);

    /// @notice 获取合约的 scriptURI
    /// @return scriptURI
    function scriptURI(address contractAddress) external view returns (string[] memory);

    /// @notice 更新 scriptURI
    /// 发出事件 ScriptUpdate(address indexed contractAddress, scriptURI memory newScriptURI);
    function setScriptURI(address contractAddress, string[] memory scriptURIList) external;
}

本文档中的关键词 “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, 和 “OPTIONAL” 应该按照 RFC 2119 和 RFC 8174 中的描述进行解释。

理由

此方法允许在没有 ERC-5169 接口的情况下编写的合约将脚本与自身关联,并避免了对集中式在线服务器的需求,以及后续对安全性的需求,并要求组织成为数据库的守门人。

测试用例

测试用例包含在 NFTRegistryTest.test.ts 中。合约、部署脚本和注册表脚本可以在测试脚本旁边找到。

克隆 repo 并运行:

cd ../assets/eip-7738
npm install --save-dev hardhat
npm install
npx hardhat test

参考实现

脚本注册表的实时实现在多个主网、L2 和测试网链上的 0x0077380bCDb2717C9640e892B9d5Ee02Bb5e0682。要部署脚本以供使用,您可以直接调用 setScriptURI 函数:

function setScriptURI(address contractAddress, string[] memory newScriptURIs)

或使用捆绑的 ethers 脚本,确保填写目标合约地址和 scriptURI:

创建注册表条目

简化实现

import "@openzeppelin/contracts/access/Ownable.sol";

contract DecentralisedRegistry is IERC7738 {
    struct ScriptEntry {
        mapping(address => string[]) scriptURIs;
        address[] addrList;
    }

    mapping(address => ScriptEntry) private _scriptURIs;

    function setScriptURI(
        address contractAddress,
        string[] memory scriptURIList
    ) public {
        require (scriptURIList.length > 0, "> 0 entries required in scriptURIList");
        bool isOwnerOrExistingEntry = Ownable(contractAddress).owner() == msg.sender 
            || _scriptURIs[contractAddress].scriptURIs[msg.sender].length > 0;
        _scriptURIs[contractAddress].scriptURIs[msg.sender] = scriptURIList;
        if (!isOwnerOrExistingEntry) {
            _scriptURIs[contractAddress].addrList.push(msg.sender);
        }
        
        emit ScriptUpdate(contractAddress, msg.sender, scriptURIList);
    }

    // Return the list of scriptURI for this contract.
    // Order the return list so `Owner()` assigned scripts are first in the list
    // 返回此合约的 scriptURI 列表。
    // 对返回列表进行排序,以便 `Owner()` 分配的脚本在列表中排在第一位
    function scriptURI(
        address contractAddress
    ) public view returns (string[] memory) {
        //build scriptURI return list, owner first
        //构建 scriptURI 返回列表,所有者优先
        address contractOwner = Ownable(contractAddress).owner();
        address[] memory addrList = _scriptURIs[contractAddress].addrList;
        uint256 i;

        //now calculate list length
        //现在计算列表长度
        uint256 listLen = _scriptURIs[contractAddress].scriptURIs[contractOwner].length;
        for (i = 0; i < addrList.length; i++) {
            listLen += _scriptURIs[contractAddress].scriptURIs[addrList[i]].length;
        }

        string[] memory ownerScripts = new string[](listLen);

        // Add owner scripts
        // 添加所有者脚本
        uint256 scriptIndex = _addScriptURIs(contractOwner, contractAddress, ownerScripts, 0);

        // Add remainder scripts
        // 添加剩余脚本
        for (uint256 i = 0; i < addrList.length; i++) {
            scriptIndex = _addScriptURIs(addrList[i], contractAddress, ownerScripts, scriptIndex);
        }

        return ownerScripts;
    }

    function _addScriptURIs(
        address user,
        address contractAddress,
        string[] memory ownerScripts,
        uint256 scriptIndex
    ) internal view returns (uint256) {
        for (uint256 j = 0; j < _scriptURIs[contractAddress].scriptURIs[user].length; j++) {
            string memory thisScriptURI = _scriptURIs[contractAddress].scriptURIs[user][j];
            if (bytes(thisScriptURI).length > 0) {
                ownerScripts[scriptIndex++] = thisScriptURI;
            }
        }
        return scriptIndex;
    }
}

安全注意事项

提供的脚本可以通过多种方式进行身份验证:

  1. setter 指定的目标合约实现了 ERC-173 Ownable 接口。一旦获取到脚本,就可以验证签名是否与 Owner() 匹配。在 TokenScript 的情况下,可以使用 TokenScript SDK、TokenScript 在线验证服务,或者通过从 XML 中提取签名,获取脚本的 keccak256 并 ecrecover 签名密钥地址,来由 dapp 或钱包进行检查。
  2. 如果合约未实现 Ownable,则可以采取进一步的步骤: a. 托管应用/钱包可以使用第三方 API 或区块浏览器确定部署密钥。然后,实现钱包、dapp 或查看器将检查签名是否与此部署密钥匹配。 b. 可以通过嵌入式密钥链由托管应用预先验证签名密钥。 c. governance token 可以允许脚本委员会对设置和验证密钥的请求进行身份验证。

如果未满足这些标准:

  • 对于主网实现,实现钱包应谨慎使用该脚本 - 这将由应用程序和/或用户自行决定。
  • 对于测试网,在钱包提供商的酌情决定下,允许脚本运行是可以接受的。

版权

CC0 下放弃版权和相关权利。

Citation

Please cite this document as:

Victor Zhang (@zhangzhongnan928), James Brown (@JamesSmartCell), "ERC-7738: 免许可脚本注册表 [DRAFT]," Ethereum Improvement Proposals, no. 7738, July 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7738.