Alert Source Discuss
Standards Track: ERC

ERC-3475: 抽象存储债券

用于创建具有抽象链上元数据存储的代币化债务的接口

Authors Yu Liu (@yuliu-debond), Varun Deshpande (@dr-chain), Cedric Ngakam (@drikssy), Dhruv Malik (@dhruvmalik007), Samuel Gwlanold Edoumou (@Edoumou), Toufic Batrice (@toufic0710)
Created 2021-04-05
Requires EIP-20, EIP-721, EIP-1155

摘要

  • 此 EIP 允许创建具有抽象链上元数据存储的代币化债务。使用现有的代币标准无法实现发行具有多个赎回数据的债券。

  • 此 EIP 使得每个债券类别 ID 能够代表一种新的可配置代币类型,并且每个类别都对应着债券的 nonce 来表示发行条件或者 uint256 中的任何其他形式的数据。债券类别的每一个 nonce 都可以有其元数据、供应量和其他赎回条件。

  • 通过此 EIP 创建的债券也可以批量发行/赎回条件,以提高 gas 成本和用户体验方面的效率。最后,从此标准创建的债券可以被划分并在二级市场中交易。

动机

当前的 LP(流动性提供者)代币是简单的 EIP-20 代币,没有复杂的数据结构。为了允许将更复杂的奖励和赎回逻辑存储在链上,我们需要一个新的代币标准,该标准:

  • 支持多个代币 ID
  • 可以存储链上元数据
  • 不需要固定的存储模式
  • 具有 gas 效率。

还有一些好处:

  • 此 EIP 允许使用相同的接口创建任何义务。
  • 它将使任何第三方钱包应用程序或交易所能够读取这些代币的余额和赎回条件。
  • 这些债券也可以批量作为可交易的工具。然后,这些工具可以被分割并在二级市场中交易。

规范

定义

银行:在获得 необходимых 流动性后发行、赎回或销毁债券的实体。通常,是具有对池的管理员访问权限的单个实体。

函数

pragma solidity ^0.8.0;

/**
* transferFrom
* @param _from 参数是债券持有人的地址,其余额即将减少。
* @param _to 参数是债券接收者的地址,其余额即将增加。
* @param _transactions 是 `Transaction[] calldata` (类型为 ['classId', 'nonceId', '_amountBonds']) 结构,该结构在下面的理论依据部分中定义。
* @dev transferFrom 必须具有 `isApprovedFor(_from, _to, _transactions[i].classId)` 批准,才能为给定的 classId 将 `_from` 地址转移到 `_to` 地址(即,对于对应于所有 nonce 的 Transaction 元组)。
例如:
* function transferFrom(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef, 0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B, [IERC3475.Transaction(1,14,500)]);
* 从 `_from` 地址转移到 `_to` 地址,类型为 class`1` 和 nonce `42` 的 `500000000` 个债券。
*/

function transferFrom(address _from, address _to, Transaction[] calldata _transactions) external;

/**
* transferAllowanceFrom
* @dev 允许仅转移使用 allowance() 分配给 _to 地址的那些债券类型和 nonce。
* @param _from 是持有人的地址,其余额即将减少。
* @param _to 是接收者的地址,其余额即将增加。
* @param _transactions 是在“理论依据”部分中定义的 `Transaction[] calldata` 结构。
* @dev transferAllowanceFrom 必须具有 `allowance(_from, msg.sender, _transactions[i].classId, _transactions[i].nonceId)`(其中 `i` 循环 [ 0 ...Transaction.length - 1] )
例如:
* function transferAllowanceFrom(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef, 0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B, [IERC3475.Transaction(1,14,500)]);
* 从 `_from` 地址转移到 `_to` 地址,类型为 class`1` 和 nonce `42` 的 `500000000` 个债券。
*/

function transferAllowanceFrom(address _from,address _to, Transaction[] calldata _transactions) public ;

/**
* issue
* @dev 允许将任何数量的债券类型(由 Transaction 元组中的值定义为参数)发行到地址。
* @dev 它必须由单个实体发行(例如,一个基于角色的可拥有合约,该合约与由 `_to` 地址存入的抵押品的流动性池集成)。
* @param `_to` 参数是将要发行债券的地址。
* @param `_transactions` 是 `Transaction[] calldata`(即已发行债券类别、债券 nonce 和要发行的债券数量的数组)。
* @dev transferAllowanceFrom 必须具有 `allowance(_from, msg.sender, _transactions[i].classId, _transactions[i].nonceId)`(其中 `i` 循环 [0 ...Transaction.length - 1] )
例如:
example: issue(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef,[IERC3475.Transaction(1,14,500)]);
向地址 `0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef`发行类别为 `0`、nonce 为 `5` 的 `1000` 个债券。
*/
function issue(address _to, Transaction[] calldata _transaction) external;

/**
* redeem
* @dev 允许从地址赎回债券。
* @dev 调用此函数需要限制为债券发行人合约。
* @param `_from` 是将要赎回债券的地址。
* @param `_transactions` 是 `Transaction[] calldata` 结构(即,具有要赎回的债券的(类别、nonce 和数量)对的元组的数组)。在理论依据部分中进一步定义。
* @dev 给定类别和 nonce 类别的 redeem 函数必须在满足某些到期条件(可以是结束时间、总有效流动性等)后才能完成。
* @dev 此外,它应该仅由银行或二级市场做市商合约调用。
例如:
* redeem(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef, [IERC3475.Transaction(1,14,500)]);
表示“从钱包地址 (0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef) 赎回 500000000 个类别 1 和 nonce 42 的债券。
*/

function redeem(address _from, Transaction[] calldata _transactions) external;

/**
* burn
* @dev 允许无效债券(或将给定的债券转移到 address(0))。
* @dev 给定类别和 nonce 的 burn 函数必须仅由控制器合约调用。
* @param _from 是即将燃烧债券的持有人的地址。
* @param `_transactions` 是 `Transaction[] calldata` 结构(即,具有要销毁的债券的(类别、nonce 和数量)对的元组的数组)。在理论依据中进一步定义。
* @dev 给定类别和 nonce 类别的 burn 函数必须仅在满足某些到期条件(可以是结束时间、总有效流动性等)后才能完成。
* @dev 此外,它应该仅由银行或二级市场做市商合约调用。
* 例如:
* burn(0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B,[IERC3475.Transaction(1,14,500)]);
* 表示销毁地址 0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B 拥有的类别 1 nonce 42 的 500000000 个债券。
*/
function burn(address _from, Transaction[] calldata _transactions) external;

/**
* approve
* @dev 允许 `_spender` 从 msg.sender 中提取 `_amount` 和类型(classId 和 nonceId)的债券。
* @dev 如果再次调用此函数,它将覆盖当前金额。
* @dev `approve()` 只能由银行或帐户所有者调用。
* @param `_spender` 参数是被批准转移债券的用户的地址。
* @param `_transactions` 是 `Transaction[] calldata` 结构(即,具有要批准由 _spender 消费的债券的(类别、nonce 和数量)对的元组的数组)。在理论依据部分中进一步定义。
* 例如:
* approve(0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B,[IERC3475.Transaction(1,14,500)]);
* 表示地址 0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B 的所有者被批准管理类别 1 和 Nonce 14 的 500 个债券。
*/

function approve(address _spender, Transaction[] calldata _transactions) external;

/**
* SetApprovalFor
* @dev 启用或禁用第三方(“运营商”)管理调用者的债券给定类别中的所有债券的批准。
* @dev 如果再次调用此函数,它将覆盖当前金额。
* @dev `approve()` 只能由银行或帐户所有者调用。
* @param `_operator` 是要添加到授权运营商集中的地址。
* @param `classId` 是债券的类别 ID。
* @param `_approved` 如果运营商已获得批准(基于提供的条件),则为 true,false 表示批准被撤销。
* @dev 合约必须定义关于设置批准条件的内部函数,并且应该只能由银行或所有者调用。
* 例如:setApprovalFor(0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B,0,true);
* 表示地址 0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B 被授权从类别 0(跨所有 nonce)转移债券。
*/

function setApprovalFor(address _operator, bool _approved) external returns(bool approved);

/**
* totalSupply
* @dev 在这里,总供应量包括已燃烧和已赎回的供应量。
* @param classId 是债券的相应类别 ID。
* @param nonceId 是给定债券类别的 nonce ID。
* @return 债券的供应量
* 例如:
* totalSupply(0, 1);
* 它查找 classid 0 和债券 nonce 1 的债券的总供应量。
*/
function totalSupply(uint256 classId, uint256 nonceId) external view returns (uint256);

/**
* redeemedSupply
* @dev 返回由 (classId,nonceId) 标识的债券的已赎回供应量。
* @param classId 是债券的相应类别 ID。
* @param nonceId 是给定债券类别的 nonce ID。
* @return 已赎回的债券的供应量。
*/
function redeemedSupply(uint256 classId, uint256 nonceId) external view returns (uint256);

/**
* activeSupply
* @dev 返回由 (classId,NonceId) 定义的债券的有效供应量。
* @param classId 是债券的相应 classId。
* @param nonceId 是给定债券类别的 nonce ID。
* @return 未赎回的有效供应量。
*/
function activeSupply(uint256 classId, uint256 nonceId) external view returns (uint256);

/**
* burnedSupply
* @dev 返回由 (classId,NonceId) 定义的债券的已燃烧供应量。
* @param classId 是债券的相应 classId。
* @param nonceId 是给定债券类别的 nonce ID。
* @return 获取给定 classId 和 nonceId 的已燃烧债券的供应量。
*/
function burnedSupply(uint256 classId, uint256 nonceId) external view returns (uint256);

/**
* balanceOf
* @dev 返回地址 `_account` 拥有的给定 classId 和债券 nonce 的债券(非引用)余额。
* @param classId 是债券的相应 classId。
* @param nonceId 是给定债券类别的 nonce ID。
* @param _account 要确定其余额的所有者的地址。
* @dev 这也包括已赎回的债券。
*/
function balanceOf(address _account, uint256 classId, uint256 nonceId) external view returns (uint256);

/**
* classMetadata
* @dev 返回类的 JSON 元数据。
* @dev 元数据应遵循稍后在 metadata.md 中解释的一组结构
* @param metadataId 是给定债券类别信息的索引 ID。
* @return nonce 的 JSON 元数据。— 例如 `[title, type, description]`。
*/
function classMetadata(uint256 metadataId) external view returns (Metadata memory);

/**
* nonceMetadata
* @dev 返回 nonce 的 JSON 元数据。
* @dev 元数据应遵循稍后在 metadata.md 中解释的一组结构
* @param classId 是债券的相应 classId。
* @param nonceId 是给定债券类别的 nonce ID。
* @param metadataId 是给定元数据信息 JSON 存储的索引。更多定义在 metadata.md 中。
* @returns nonce 的 JSON 元数据。— 例如 `[title, type, description]`。
*/
function nonceMetadata(uint256 classId, uint256 metadataId) external view returns (Metadata memory);

/**
* classValues
* @dev 允许任何人读取给定债券类别 `classId` 的不同类的 Values(存储在 struct Values 中)的值。
* @dev 这些值应遵循 metadata 中解释的一组结构,以及与给定元数据结构对应的正确映射
* @param classId 是债券的相应 classId。
* @param metadataId 是给定元数据的所有值的给定元数据信息的 JSON 存储的索引。更多定义在 metadata.md 中。
* @returns 类元数据的值。— 例如 `[string, uint, address]`。
*/
function classValues(uint256 classId, uint256 metadataId) external view returns (Values memory);

/**
* nonceValues
* @dev 允许任何人读取给定债券(`nonceId`,`classId`)的不同类的 Values(存储在 struct Values 中)的值。
* @dev 这些值应遵循 metadata 中解释的一组结构,以及与给定元数据结构对应的正确映射
* @param classId 是债券的相应 classId。
* @param metadataId 是给定元数据的所有值的给定元数据信息的 JSON 存储的索引。更多定义在 metadata.md 中。
* @returns 类元数据的值。— 例如 `[string, uint, address]`。
*/
function nonceValues(uint256 classId, uint256 nonceId, uint256 metadataId) external view returns (Values memory);

/**
* getProgress
* @dev 返回用于确定债券到期当前状态的参数。
* @dev 赎回条件应使用一个或多个内部函数定义。
* @param classId 是债券的相应 classId。
* @param nonceId 是给定债券类别的 nonceId。
* @returns progressAchieved 定义了定义债券当前状态的指标(与流动性百分比、时间等相关)。
* @returns progressRemaining 定义了定义剩余时间/剩余进度的指标。
*/
function getProgress(uint256 classId, uint256 nonceId) external view returns (uint256 progressAchieved, uint256 progressRemaining);

/**
* allowance
* @dev 授权通过 `_owner` 为所有由 (classId, nonceId) 标识的债券设置给定的 `_spender` 的限额。
* @param _owner 债券的所有者的地址(也是 msg.sender)。
* @param _spender 是授权花费由 _owner 持有的信息的债券的地址 (classId, nonceId)。
* @param classId 是债券的相应 classId。
* @param nonceId 是给定债券类别的 nonceId。
* @notice 返回 _spender 仍然可以从 _owner 提取的 _amount。
*/
function allowance(address _owner, address _spender, uint256 classId, uint256 nonceId) external returns(uint256);

/**
* isApprovedFor
* @dev 如果地址 _operator 被批准管理帐户的债券类别,则返回 true。
* @notice 查询给定所有者的运营商的批准状态。
* @dev _owner 是债券的所有者。
* @dev _operator 是 EOA / 合约,将检查该批准的债券类别批准状态。
* @returns 如果运营商获得批准,则返回“true”,如果未获得批准,则返回“false”。
*/
function isApprovedFor(address _owner, address _operator) external view returns (bool);

事件

/**
* Issue
* @notice 当发行债券时,必须触发 Issue。这不应包括零值发行。
* @dev 这不应包括零值发行。
* @dev 当运营商(即银行地址)合约向给定的实体发行债券时,必须触发 Issue。
* eg: emit Issue(_operator, 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef,[IERC3475.Transaction(1,14,500)]);
* 地址发行(运营商) 500 个债券 (nonce14,class 1) 到地址 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef。
*/

event Issue(address indexed _operator, address indexed _to, Transaction[] _transactions);

/**
* Redeem
* @notice 当赎回债券时,必须触发 Redeem。这不应包括零值赎回。
*e.g: emit Redeem(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef,0x492Af743654549b12b1B807a9E0e8F397E44236E,[IERC3475.Transaction(1,14,500)]);
* 当地址0x492Af743654549b12b1B807a9E0e8F397E44236E拥有的 5000 个类别 1、nonce 14 的债券被 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef 赎回时,发出事件。
*/

event Redeem(address indexed _operator, address indexed _from, Transaction[] _transactions);


/**
* Burn.
* @dev 当债券因抵押(或被银行合约作废)而赎回时,必须触发 `Burn`。
* @dev 当销毁债券时,必须触发 `Burn`。这不应包括零值销毁。
* 例如: emit Burn(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef,0x492Af743654549b12b1B807a9E0e8F397E44236E,[IERC3475.Transaction(1,14,500)]);
* 当运营商 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef 销毁所有者 0x492Af743654549b12b1B807a9E0e8F397E44236E 的类型(类别 1、nonce 14)的 500 个债券时,发出事件。
*/

event burn(address _operator, address _owner, Transaction[] _transactions);

/**
* Transfer
* @dev 当债券由地址(运营商)从所有者地址 (_from) 转移到地址 (_to) 时,会发出该事件,其中转移的债券的参数由 _transactions struct 数组定义。
* @dev 当转移债券时,必须触发 Transfer。这不应包括零值转移。
* @dev 传输事件,其中 _from `0x0` 不得创建此事件(请改用 `event Issued`)。
* 例如 emit Transfer(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef, 0x492Af743654549b12b1B807a9E0e8F397E44236E, _to, [IERC3475.Transaction(1,14,500)]);
* 地址(_operator)的传输,金额 500 个债券(类别 1 和 Nonce 14)从 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef 转移到地址(_to)。
*/

event Transfer(address indexed _operator, address indexed _from, address indexed _to, Transaction[] _transactions);

/**
* ApprovalFor
* @dev 当地址 (_owner) 批准地址 (_operator) 转移其债券时,会发出该事件。
* @notice 当债券持有人批准 _operator 时,必须触发 Approval。这不应包括零值批准。
* 例如: emit ApprovalFor(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef, 0x492Af743654549b12b1B807a9E0e8F397E44236E, true);
* 这意味着 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef 授予 0x492Af743654549b12b1B807a9E0e8F397E44236E 访问权限以转移其债券。
*/

event ApprovalFor(address indexed _owner, address indexed _operator, bool _approved);

元数据: 债券类别或 nonce 的元数据存储为 JSON 对象数组,由以下类型表示。

注意:所有元数据模式都从 此处 引用

1. 描述:

这定义了有关存储在 nonce/类元数据结构中的数据性质的额外信息。它们是使用 此处 解释的结构定义的。 然后,参与债券市场的各个实体的前端将使用它来解释符合其管辖权的数据。

2. Nonce:

用于索引信息的关键值是“class”字段。以下是规则:

  • 标题可以是元数据描述区分的任何字母数字类型(尽管它可能取决于某些司法管辖区)。
  • 标题不应为空。

元数据的一些具体示例可能是债券的本地化、司法管辖区详细信息等,可以在 metadata.md 示例描述中找到它们。

3. 类元数据:

此结构定义了类信息的详细信息(符号、风险信息等)。 此处 的类元数据部分中对此示例进行了说明。

4. 解码数据

首先,相应的 frontend 将使用用于分析元数据的函数(即 ClassMetadataNonceMetadata)来解码债券的信息。

这是通过重写函数 classValuesnonceValues 的函数接口来完成的,方法是定义键(应该是索引)以读取存储为 JSON 对象的相应信息。

{
"title": "symbol",
"_type": "string",
"description": "以以下格式定义唯一标识符名称:(symbol, bondType, maturity in months)",
"values": ["Class Name 1","Class Name 2","DBIT Fix 6M"],
}

例如。 在上面的示例中,为了获取给定类别 ID 的 symbol,我们可以使用类别 ID 作为键来获取值中的 symbol 值,然后可以将其用于获取实例的详细信息。

理论依据

元数据结构

我们没有将有关类及其发行给用户(即 nonce)的详细信息外部存储,而是将详细信息存储在相应的结构中。 类代表不同的债券类型,而 nonce 代表不同的发行期。 同一类别下的 Nonce 共享相同的元数据。 同时,nonce 是非同质化的。 每个 nonce 都可以存储不同的元数据。 因此,在转移债券时,所有元数据都将转移给债券的新所有者。

 struct Values{
 string stringValue;
 uint uintValue;
 address addressValue;
 bool boolValue;
 bytes bytesValue;
 }
 struct Metadata {
 string title;
 string _type;
 string description;
 }

批量功能

此 EIP 支持批量操作。它允许用户在单个交易中立即将其元数据附带的不同债券转移到新地址。执行后,新所有者有权索取每个债券的面值。这种机制有助于债券的“包装” - 有助于二级市场交易等用例。

 struct Transaction {
 uint256 classId;
 uint256 nonceId;
 uint256 _amount;
 }

其中: classId 是债券的类别 ID。

nonceId 是给定债券类别的 nonce ID。此参数用于区分债券的发行条件。

_amount 是批准消费者的债券金额。

AMM 优化

此 EIP 最明显的用例之一是多层池。早期版本的 AMM 使用单独的智能合约和 EIP-20 LP 代币来管理一个交易对。通过这样做,一个池中的总体流动性显着降低,从而产生不必要的 gas 支出和滑点。使用此 EIP 标准,一个人可以使用内部的所有货币对构建一个大的流动性池(这要归功于由流动性对应于给定的债券类别和 nonce 的数据结构的存在)。因此,通过了解债券的类别和 nonce,流动性可以表示为给定池中债券所有者给定代币对的百分比。实际上,EIP-20 LP 代币(由池工厂合约中的唯一智能合约定义)被聚合到单个债券中,并整合到单个池中。

  • 该标准名称(抽象存储债券)背后的原因是它能够存储所有规范(元数据/值和交易,如下所述),而无需链上/链下外部存储。

向后兼容性

任何继承此 EIP 接口的合约都兼容。这种兼容性适用于债券发行人和接收人。 此外,如果任何客户端 EOA 钱包能够签署 issue()redeem() 命令,则它可以与该标准兼容。

但是,任何现有的 EIP-20 代币合约都可以通过将铸币角色委托给具有内置此标准接口的银行合约来发行其债券。 请查看我们的参考实现以获取正确的接口定义。

为确保在整个债券生命周期内(即“发行”、“赎回”和“转移”函数)对交易进行索引,传递此类交易时,必须发出规范部分中引用的事件。

请注意,此标准接口也与 EIP-20EIP-721EIP-1155 接口兼容。

但是,建议创建一个单独的银行合约来读取债券和满足未来升级需求。

可接受的抵押品可以是同质代币(如 EIP-20)、非同质代币(EIP-721EIP-1155)或此标准表示的其他债券的形式。

测试用例

最小参考实现的测试用例位于 此处。 使用 Truffle box 编译和测试合约。

参考实现

安全考虑

  • function setApprovalFor(address _operatorAddress) 将运营商角色授予 _operatorAddress。 默认情况下,它拥有转移、销毁和赎回债券的所有权限。

  • 如果所有者想要一次性分配给特定债券(classId、bondsId)的地址,他应该调用 function approve(),给出分配的 Transaction[],而不是使用 setApprovalFor 批准所有类别。

版权

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

Citation

Please cite this document as:

Yu Liu (@yuliu-debond), Varun Deshpande (@dr-chain), Cedric Ngakam (@drikssy), Dhruv Malik (@dhruvmalik007), Samuel Gwlanold Edoumou (@Edoumou), Toufic Batrice (@toufic0710), "ERC-3475: 抽象存储债券," Ethereum Improvement Proposals, no. 3475, April 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-3475.