Alert Source Discuss
🚧 Stagnant Standards Track: ERC

ERC-5050: 具有模块化环境的交互式 NFT

用于 NFT 上和 NFT 之间交互的操作消息和发现协议

Authors Alexi (@alexi)
Created 2021-04-18
Discussion Link https://ethereum-magicians.org/t/eip-5050-nft-interaction-standard/9922
Requires EIP-165, EIP-173, EIP-721, EIP-1155, EIP-1820, EIP-4906

摘要

本标准定义了一个广泛适用的操作消息协议,用于在代币之间传输用户发起的动作。模块化状态性通过可选的状态控制器合约(即环境)来实现,这些合约管理共享状态,并提供操作过程的仲裁和结算。

动机

诸如 EIP-721EIP-1155 等代币化项目标准充当以太坊计算环境的对象。越来越多的项目正在寻求将交互性和“数字物理学”构建到 NFT 中,尤其是在游戏和去中心化身份的背景下。一个标准的操作消息协议将允许以与操作对象相同的开放的、以太坊原生方式开发此物理层。

概述的消息协议定义了如何在代币和(可选的)共享状态环境之间启动和传输操作。 它与定义功能的通用接口配对,该接口允许链下服务聚合和查询受支持的合约以实现功能和互操作性; 创建一个可发现的、人类可读的交互式代币合约网络。实现此标准的合约不仅可以被此类服务自动发现,而且它们的交互策略也可以被发现。这允许客户端轻松发现兼容的发送者和接收者,以及允许的操作。

聚合器还可以解析操作事件日志,以获得有关新操作类型、趋势/流行/新交互式合约的分析,用户可能与之交互的代币和状态合约对,以及其他发现工具以促进交互。

收益

  1. 使交互式代币合约可被应用程序发现和使用
  2. 为游戏和其他应用程序创建一个去中心化的“数字物理”层
  3. 为开发者提供一个简单的解决方案,具有可行的有效性保证,以制作动态 NFT 和其他代币
  4. 允许使用通用操作桥来在链之间传输操作(支持将 L1 资产上的操作保存到 L2,L1 资产与 L2 资产交互,以及将 L2 操作在 L1 上“汇总”/最终确定)。

规范

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

实现此 EIP 标准的智能合约必须实现 EIP-165supportsInterface 函数,并且如果 IERC5050Sender 接口 ID 0xc8c6c9f3 和/或 IERC5050Receiver 接口 ID 0x1a3f02f4 通过 interfaceID 参数传递,则必须返回常量值 true(取决于合约实现的接口)。

pragma solidity ^0.8.0;

/// @param _address 交互对象的地址
/// @param tokenId 正在交互的代币(可选)
struct Object {
    address _address;
    uint256 _tokenId;
}

/// @param selector 操作字符串的 bytes4(keccack256()) 编码
/// @param user 发送者的地址
/// @param from 启动对象
/// @param to 接收对象
/// @param state 状态控制器合约
/// @param data 没有指定格式的额外数据
struct Action {
    bytes4 selector;
    address user;
    Object from;
    Object to;
    address state;
    bytes data;
}

/// @title EIP-5050 具有模块化环境的交互式 NFT
interface IERC5050Sender {
    /// @notice 向目标地址发送操作
    /// @dev 操作的 `fromContract` 自动设置为 `address(this)`,
    /// 并且 `from` 参数设置为 `msg.sender`。
    /// @param action 要发送的操作
    function sendAction(Action memory action) external payable;

    /// @notice 根据操作的哈希和 nonce 检查操作是否有效
    /// @dev 当操作通过所有三个可能的合约时
    /// (`fromContract`、`to` 和 `state`),`state` 合约使用基于 nonce 的操作哈希
    /// 通过启动 `fromContract` 来验证操作。
    /// 此哈希在启动操作处理之前,计算后保存到 `fromContract` 上的存储中。
    /// `state` 合约计算哈希并验证该哈希和 `fromContract` 的 nonce。
    /// @param _hash 要验证的哈希
    /// @param _nonce 要验证的 nonce
    function isValid(bytes32 _hash, uint256 _nonce) external returns (bool);

    /// @notice 检索可以发送的操作列表。
    /// @dev 供链下应用程序使用,以查询兼容的合约,
    /// 并以人类可读的形式宣传功能。
    function sendableActions() external view returns (string[] memory);

    /// @notice 更改或重申操作的批准地址
    /// @dev 零地址表示没有批准的地址。
    ///  除非 `msg.sender` 是 `_account`,或者是 `_account` 的授权
    ///  运营者,否则会抛出异常
    /// @param _account 要批准的帐户操作对的帐户
    /// @param _action 要批准的帐户操作对的操作
    /// @param _approved 新批准的帐户操作控制器
    function approveForAction(
        address _account,
        bytes4 _action,
        address _approved
    ) external returns (bool);

    /// @notice 启用或禁用第三方(“运营者”)代表 `msg.sender` 执行
    ///  所有操作的批准
    /// @dev 发出 ApprovalForAll 事件。合约必须允许
    ///  每个所有者有无限数量的运营者。
    /// @param _operator 要添加到授权运营者集合的地址
    /// @param _approved 如果运营者已批准则为 True,如果撤销批准则为 false
    function setApprovalForAllActions(address _operator, bool _approved)
        external;

    /// @notice 获取帐户操作对的批准地址
    /// @dev 如果 `_tokenId` 不是有效的 NFT,则会抛出异常。
    /// @param _account 要查找批准地址的帐户操作的帐户
    /// @param _action 要查找批准地址的帐户操作的操作
    /// @return 此帐户操作的批准地址,如果没有,则为零地址
    function getApprovedForAction(address _account, bytes4 _action)
        external
        view
        returns (address);

    /// @notice 查询地址是否是另一个地址的授权运营者
    /// @param _account 代表其执行操作的地址
    /// @param _operator 代表帐户执行操作的地址
    /// @return 如果 `_operator` 是 `_account` 的批准运营者,则为 True,否则为 false
    function isApprovedForAllActions(address _account, address _operator)
        external
        view
        returns (bool);

    /// @dev 当发送操作时,会发出此事件 (`sendAction()`)
    event SendAction(
        bytes4 indexed name,
        address _from,
        address indexed _fromContract,
        uint256 _tokenId,
        address indexed _to,
        uint256 _toTokenId,
        address _state,
        bytes _data
    );

    /// @dev 当帐户操作对的批准地址
    ///  被更改或重申时,会发出此事件。零地址表示没有
    ///  批准的地址。
    event ApprovalForAction(
        address indexed _account,
        bytes4 indexed _action,
        address indexed _approved
    );

    /// @dev 当为帐户启用或禁用运营者时,会发出此事件。
    ///  运营者可以代表帐户执行所有操作。
    event ApprovalForAllActions(
        address indexed _account,
        address indexed _operator,
        bool _approved
    );
}

interface IERC5050Receiver {
    /// @notice 处理操作
    /// @dev `to` 合约和 `state` 合约都通过
    /// `onActionReceived()` 调用。
    /// @param action 要处理的操作
    function onActionReceived(Action calldata action, uint256 _nonce)
        external
        payable;

    /// @notice 检索可以接收的操作列表。
    /// @dev 供链下应用程序使用,以查询兼容的合约,
    /// 并以人类可读的形式宣传功能。
    function receivableActions() external view returns (string[] memory);

    /// @dev 当收到有效操作时,会发出此事件。
    event ActionReceived(
        bytes4 indexed name,
        address _from,
        address indexed _fromContract,
        uint256 _tokenId,
        address indexed _to,
        uint256 _toTokenId,
        address _state,
        bytes _data
    );
}

操作命名

操作应该使用点分隔进行命名空间划分(例如,"spells.cast" 指定具有命名空间 "spells""cast" 操作),并使用箭头分隔进行序列指定(例如,"settle>build" 表示必须在 "build" 之前接收到 "settle")。

状态合约的工作原理

操作不需要使用状态合约。操作可以从一个代币合约 (Object) 传输到另一个代币合约,或者从用户传输到单个代币合约。在这些情况下,发送和接收合约各自控制自己的状态。

状态合约允许任意发送者和接收者共享用户指定的状态环境。每个 Object 都可以定义自己的操作处理,该处理可以包括在期间从状态合约读取数据,但操作必须由状态合约最终确定。这意味着状态合约充当最终依据。

预期工作流程是状态合约定义有状态的游戏环境,通常具有供其他合约使用的自定义 IState 接口。 Objects 向状态合约注册以初始化其状态。然后,用户使用特定的状态合约提交操作以使游戏发生某些事情。

状态合约的模块化允许创建和由客户端换入或换出相同或相似的“游戏环境”的多个副本。 这种模块化有多种使用方式:

  • 聚合器服务可以分析操作事件,以确定给定发送者/接收者的可能状态合约
  • 发送者/接收者合约可以要求特定的状态合约
  • 发送者/接收者合约可以允许任何状态合约,但设置一个默认值。这对于根据状态更改其渲染方式的 NFT 来说很重要。此默认值也可以由代币持有者配置。
  • 状态合约可以是与其他链上的状态合约的桥梁,从而实现 L1 验证、L2 存储使用模式 (验证 layer-1 资产的操作,保存在存储更便宜的 l2 上)。

示例

状态合约 FightGame 定义一个格斗游戏环境。代币持有者调用 FightGame.register(contract, tokenId) 以随机初始化其统计数据(力量/生命值/等)。持有合约 Fighters 的已注册代币 A 的帐户调用 Fighters.sendAction(AttackAction),指定来自 Fighters 的代币 A 作为发送者,来自 Pacifists 合约的代币 B 作为接收者,以及 FightGame 作为状态合约。

该操作被传递给代币 B,该代币可以在将操作传递给 FightGame 状态合约之前以其想要的任何方式处理该操作。状态合约可以使用 Fighters 合约验证存储的操作哈希,以验证该操作在更新统计数据之前是否真实,从而对代币 B 造成伤害。

代币 A 和 B 可以基于 FightGame 状态合约中的统计数据更新其元数据,或者基于它们自己的响应于发送/接收操作而更新的存储数据。

扩展

互动性

某些合约可能具有促进互动的自定义用户界面。

pragma solidity ^0.8.0;

/// @title EIP-5050 具有模块化环境的交互式 NFT
interface IERC5050Interactive {
    function interfaceURI(bytes4 _action) external view returns (string);
}

操作代理

操作代理可用于支持与不可升级合约的向后兼容性,并可能用于跨链操作桥接。

它们可以使用 EIP-1820 的修改版本来实现,该版本允许 EIP-173 合约所有者调用 setManager()

可控性

此标准的用户可能希望允许受信任的合约控制操作过程,以提供安全保证并支持操作桥接。控制器逐步执行操作链,按顺序分别调用每个合约。

支持控制器的合约应忽略与操作验证相关的 require/revert 语句,并且不得将操作传递到链中的下一个合约。

pragma solidity ^0.8.0;

/// @title EIP-5050 操作控制器
interface IControllable {
    
    /// @notice 启用或禁用第三方(“控制器”)的批准,以强制
    ///  处理给定操作,而不执行 EIP-5050 有效性检查。
    /// @dev 发出 ControllerApproval 事件。合约必须允许
    ///  每个操作有无限数量的控制器。
    /// @param _controller 要添加到授权控制器集合的地址
    /// @param _action 要为其批准/不批准控制器的操作的选择器
    /// @param _approved 如果批准了控制器,则为 True,如果撤消批准,则为 false
    function setControllerApproval(address _controller, bytes4 _action, bool _approved)
        external;

    /// @notice 启用或禁用第三方(“控制器”)的批准,以强制
    ///  操作处理,而不执行 EIP-5050 有效性检查。 
    /// @dev 发出 ControllerApproval 事件。合约必须允许
    ///  每个操作有无限数量的控制器。
    /// @param _controller 要添加到授权控制器集合的地址
    /// @param _approved 如果批准了控制器,则为 True,如果撤消批准,则为 false
    function setControllerApprovalForAll(address _controller, bool _approved)
        external;

    /// @notice 查询地址是否是给定操作的授权控制器。
    /// @param _controller 可以强制操作处理的受信任第三方地址
    /// @param _action 要查询的操作选择器
    /// @return 如果 `_controller` 是 `_account` 的批准运营者,则为 True,否则为 false
    function isApprovedController(address _controller, bytes4 _action)
        external
        view
        returns (bool);
    
    /// @dev 当为给定
    ///  操作启用或禁用控制器时,会发出此事件。控制器可以强制在发出事件的合约上进行 `action` 处理,
    ///  绕过标准的 EIP-5050 有效性检查。
    event ControllerApproval(
        address indexed _controller,
        bytes4 indexed _action,
        bool _approved
    );
    
    /// @dev 当为所有操作启用或禁用控制器时,会发出此事件。
    ///  禁用控制器的所有操作批准不会覆盖显式操作
    ///  操作批准。针对所有操作批准的控制器可以强制对发出事件的合约进行任何操作的操作处理。
    event ControllerApprovalForAll(
        address indexed _controller,
        bool _approved
    );
}

元数据更新

交互式 NFT 可能会响应某些操作而更新其元数据,并且开发者可能想要实现 EIP-4906 事件发射器。

原理

该交互式代币标准的关键特性是,它 1) 创建了一种定义、宣传和进行对象交互的常用方法,2) 启用可选的、通过有用的有效性保证以最小 gas 开销进行代理的状态性,3) 易于开发者实现,以及 4) 易于最终用户使用。

操作名称和选择器

操作使用人类可读的字符串进行宣传,并使用函数选择器 (bytes4(keccack256(action_key))) 进行处理。人类可读的字符串允许最终用户轻松解释功能,而函数选择器允许对任意长的操作键进行高效的比较操作。此方案还允许简单的命名空间划分和序列指定。

链下服务可以在与实现此 EIP 的合约交互或解析 SendActionActionReceived 事件日志时,轻松地将字符串转换为 bytes4 选择器编码。

验证

通过操作数据哈希验证启动合约,令几乎所有接受调查的人满意,并且是探索的最节省 gas 的验证解决方案。我们认识到,除了使用容易受到网络钓鱼攻击的 tx.origin 之外,此解决方案不允许接收和状态合约验证启动 user 帐户。

我们考虑使用签名消息来验证用户启动,但是此方法有两个主要缺点:

  1. 用户体验 用户需要执行两个步骤来提交每个操作(签署消息,并发送交易)
  2. Gas 执行签名验证的计算成本很高

最重要的是,接受调查的开发者之间的共识是,严格的用户验证是不必要的,因为关注点仅仅是恶意启动合约会通过网络钓鱼诱导用户使用恶意合约的资产来提交操作。此协议将启动合约的代币视为主要驱动者,而不是用户。 任何人都可以向比尔·盖茨发推文。任何代币都可以向另一个代币发送操作。哪些操作被接受以及如何处理这些操作由合约决定。高价值操作可以通过状态合约通过信誉进行控制,或者使用允许/禁止列表进行访问控制。 Controllable 合约也可以通过受信任的控制器用作操作链的替代方法。

考虑的替代方案:作为签名消息传输的操作,保存在启动合约上可重用存储槽中的操作

状态合约

将状态逻辑移入专用的、参数化的合约中,使状态成为操作原语,并防止状态管理隐藏在合约中。具体来说,它允许用户决定在哪个“环境”中提交操作,并允许启动和接收合约共享状态数据,而无需它们进行通信。

状态合约接口的具体细节超出了本标准的范围,旨在专门用于独特的交互式环境。

Gas 和复杂性(关于操作链)

每个合约中的操作处理可能非常复杂,并且无法消除某些合约交互会耗尽 gas 的可能性。但是,开发者应尽一切努力减少其操作处理方法中的 gas 使用量,并避免使用 for 循环。

考虑的替代方案:从一个合约推拉到下一个合约的多请求操作链。

向后兼容性

除非使用代理注册表扩展,否则不可升级的、已部署的代币合约将与此标准不兼容。

参考实现

../assets/eip-5050 中包含一个参考实现,其中包含一个简单的无状态示例 ExampleToken2Token.sol 和一个有状态示例 ExampleStateContract.sol

安全注意事项

此协议的核心安全考虑因素是操作验证。操作从一个合约传递到另一个合约,这意味着接收合约无法以原生方式验证启动合约的调用者是否与 action.from 地址匹配。此协议最重要的贡献之一是它提供了一种替代使用签名消息的方法,签名消息要求用户为提交的每个操作执行两个操作。

验证中所述,这是可行的,因为启动合约/代币被视为主要驱动者,而不是用户。

版权

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

Citation

Please cite this document as:

Alexi (@alexi), "ERC-5050: 具有模块化环境的交互式 NFT [DRAFT]," Ethereum Improvement Proposals, no. 5050, April 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5050.