EIP-: ```md
Authors |
---|
Table of Contents
---
eip: 998
title: 可组合的非同质化代币
description: 扩展 ERC-721 以拥有其他 ERC-721 和 ERC-20 代币。
author: Matt Lockyer <mattdlockyer@gmail.com>, Nick Mudge <nick@perfectabstractions.com>, Jordan Schalm <jordan.schalm@gmail.com>, sebastian echeverry <sebastian.echeverry@robotouniverse.com>, Zainan Victor Zhou (@xinbenlv)
discussions-to: https://ethereum-magicians.org/t/erc-998-composable-non-fungible-tokens-cnfts/387
status: Draft
type: Standards Track
category: ERC
created: 2018-07-07
requires: 20, 165, 721
---
## 摘要
[ERC-721 标准](/docs/eips/EIPS/eip-721/) 的扩展,允许 ERC-721 代币拥有其他 ERC-721 代币和 [ERC-20](/docs/eips/EIPS/eip-20/) 代币。
[ERC-20](/docs/eips/EIPS/eip-20/) 和 `ERC-223 https://github.com/ethereum/EIPs/issues/223` 标准的扩展,允许 ERC-20 和 `ERC-223` 代币被 ERC-721 代币拥有。
本规范涵盖四种不同类型的可组合代币:
1. [`ERC998ERC721` 自顶向下可组合代币,用于接收、持有和转移 ERC-721 代币](#erc-721-top-down-composable)
2. [`ERC998ERC20` 自顶向下可组合代币,用于接收、持有和转移 ERC-20 代币](#erc-20-top-down-composable)
3. [`ERC998ERC721` 自底向上可组合代币,用于将自身附加到其他 ERC-721 代币。](#erc-721-bottom-up-composable)
4. [`ERC998ERC20` 自底向上可组合代币,用于将自身附加到 ERC-721 代币。](#erc-20-bottom-up-composable)
对应于
1. `ERC998ERC721` 自顶向下可组合代币是一种具有附加功能的 ERC-721 代币,用于拥有其他 ERC-721 代币。
2. `ERC998ERC20` 自顶向下可组合代币是一种具有附加功能的 ERC-721 代币,用于拥有 ERC-20 代币。
3. `ERC998ERC721` 自底向上可组合代币是一种具有附加功能的 ERC-721 代币,用于被 ERC-721 代币拥有。
4. `ERC998ERC20` 自底向上可组合代币是一种具有附加功能的 ERC-20 代币,用于被 ERC-721 代币拥有。
自顶向下可组合合约存储并跟踪其每个代币的子代币。
自底向上可组合合约存储并跟踪其每个代币的父代币。
使用可组合代币,可以组合由所有权连接的 ERC-721 和 ERC-20 代币的列表或树。任何这样的结构都将在结构的根部有一个所有者地址,该地址是整个组合的所有者。通过更改根所有者,可以用一个交易转移整个组合。
不同的可组合代币,自顶向下和自底向上,各有其优点和缺点,这将在 [原理部分](#rationale) 中解释。一个代币可以是一种或多种类型的可组合代币。
如果一个非同质化代币实现以下一个或多个接口,则它符合此 EIP 并且是可组合的:
* `ERC998ERC721TopDown`
* `ERC998ERC20TopDown`
* `ERC998ERC721BottomUp`
* `ERC998ERC20BottomUp`
## 规范
### ERC-721
`ERC998ERC721` 自顶向下、`ERC998ERC20` 自顶向下和 `ERC998ERC721` 自底向上可组合合约必须实现 [ERC-721 接口](/docs/eips/EIPS/eip-721/)。
### ERC-20
`ERC998ERC20` 自底向上可组合合约必须实现 [ERC-20 接口](/docs/eips/EIPS/eip-20/)。
### [ERC-165](/docs/eips/EIPS/eip-165/)
[ERC-165 标准](/docs/eips/EIPS/eip-165/) 必须应用于每个使用的 [ERC-998](/docs/eips/EIPS/eip-998/) 接口。
### 认证
对于 `ERC998ERC721` 自顶向下和 `ERC998ERC721` 自底向上可组合代币,验证用户或合约是否可以执行某些操作的方式相同。
`rootOwner` 指的是可组合代币和 ERC-721 代币树顶部的所有者地址。
任何可组合代币中的验证都是通过查找 `rootOwner` 并将其与 `msg.sender`、`getApproved(tokenId)` 的返回结果以及 `isApprovedForAll(rootOwner, msg.sender)` 的返回结果进行比较来完成的。如果找到匹配项,则验证通过,否则验证失败并且合约抛出异常。
这是一个身份验证代码的示例:
```solidity
address rootOwner = address(rootOwnerOf(_tokenId));
require(rootOwner == msg.sender ||
isApprovedForAll(rootOwner,msg.sender) ||
getApproved(tokenId) == msg.sender;
approve(address _approved, uint256 _tokenId)
和 getApproved(uint256 _tokenId)
ERC-721 函数专门为 rootOwner
实现。 这使得可组合代币树可以转移到新的 rootOwner
,而无需担心哪些地址已在子代币中获得批准,因为任何先前的批准只能由先前的 rootOwner
使用。
以下是示例实现:
function approve(address _approved, uint256 _tokenId) external {
address rootOwner = address(rootOwnerOf(_tokenId));
require(rootOwner == msg.sender || isApprovedForAll(rootOwner,msg.sender));
rootOwnerAndTokenIdToApprovedAddress[rootOwner][_tokenId] = _approved;
emit Approval(rootOwner, _approved, _tokenId);
}
function getApproved(uint256 _tokenId) public view returns (address) {
address rootOwner = address(rootOwnerOf(_tokenId));
return rootOwnerAndTokenIdToApprovedAddress[rootOwner][_tokenId];
}
遍历
可组合代币的 rootOwner
可以通过调用 rootOwnerOf(uint256 _tokenId)
或 rootOwnerOfChild(address _childContract, uint256 _childTokenId)
来获取。 这些函数被自顶向下和自底向上可组合代币用来遍历可组合代币和 ERC-721 代币树以找到 rootOwner
。
ERC998ERC721
自顶向下和自底向上可组合代币可以相互操作。 自顶向下可组合代币可以拥有自底向上可组合代币,或者自顶向下可组合代币可以拥有拥有自底向上代币的 ERC-721 代币。 在任何配置中,在可组合代币上调用 rootOwnerOf(uint256 _tokenID)
都将返回所有权树顶部的根所有者地址。
重要的是要正确理解 rootOwnerOf
的遍历逻辑。 rootOwnerOf
的逻辑是相同的,无论可组合代币是自底向上、自顶向下还是两者都有。
这是逻辑:
rootOwnerOf(uint256 _tokenId) 的逻辑
如果代币是自底向上可组合代币并且具有父代币,则为父代币调用 rootOwnerOf。
如果调用成功,则返回的地址是 rootOwner。
否则为父代币调用 rootOwnerOfChild。
如果调用成功,则返回的地址是 rootOwner。
否则获取代币的所有者地址,这就是 rootOwner。
否则为代币调用 rootOwnerOfChild
如果调用成功,则返回的地址是 rootOwner。
否则获取代币的所有者地址,这就是 rootOwner。
为代币调用 rootOwnerOfChild
意味着以下逻辑:
// 调用 tokenId 的 rootOwnerOfChild 的逻辑
address tokenOwner = ownerOf(tokenId);
address childContract = address(this);
bytes32 rootOwner = ERC998ERC721(tokenOwner).rootOwnerOfChild(childContract, tokenId);
但请理解,对 rootOwnerOfChild
的实际调用应该使用汇编语言进行,以便代码可以检查调用是否失败,并且使用 staticcall
操作码以确保不修改任何状态。
实现上述身份验证和遍历功能的代币/合约是“可组合感知”的。
可组合转移函数参数格式
进行转移的可组合函数遵循相同的参数格式:from:to:what。
例如,getChild(address _from, uint256 _tokenId, address _childContract, uint256 _childTokenId)
可组合函数将 ERC-721 代币从地址转移到自顶向下可组合代币。 _from
参数是 from,_tokenId
参数是 to,address _childContract, uint256 _childTokenId
参数是 what。
另一个示例是 safeTransferChild(uint256 _fromTokenId, address _to, address _childContract, uint256 _childTokenId)
函数。 _fromTokenId
是 from,_to
是 to,address _childContract, address _childTokenId
参数是 what。
transferFrom/safeTransferFrom 函数不转移代币拥有的代币
在自底向上和自顶向下可组合合约中,如果直接调用 transferFrom
和 safeTransferFrom
函数来转移另一个代币拥有的代币,则必须抛出异常。
原因是这些函数没有明确指定哪个代币拥有要转移的代币。 有关此的更多信息,请参见原理部分。
transferFrom/safeTransferFrom
函数必须用于转移地址拥有的代币。
ERC-721 自顶向下可组合代币
ERC-721 自顶向下可组合代币充当 ERC-721 代币的容器。
ERC-721 自顶向下可组合代币是可以接收、持有和转移 ERC-721 代币的 ERC-721 代币。
有两种方法可以将 ERC-721 代币转移到自顶向下可组合代币:
- 使用
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data)
函数。_to
参数是自顶向下可组合合约地址。bytes data
参数保存 ERC-721 代币转移到的自顶向下可组合 tokenId 的整数值。 - 在自顶向下可组合合约的 ERC-721 代币合约中调用
approve
。 然后在可组合合约中调用getChild
。
第一种方法适用于具有 safeTransferFrom
函数的 ERC-721 合约。 第二种方法适用于没有此功能的合约,例如加密猫。
这是一个将 ERC-721 代币 3 从地址转移到自顶向下可组合代币 6 的示例:
uint256 tokenId = 6;
bytes memory tokenIdBytes = new bytes(32);
assembly { mstore(add(tokenIdBytes, 32), tokenId) }
ERC721(contractAddress).safeTransferFrom(userAddress, composableAddress, 3, tokenIdBytes);
每个符合 ERC-721 自顶向下可组合合约都必须实现 ERC998ERC721TopDown
接口。
ERC998ERC721TopDownEnumerable
和 ERC998ERC20TopDownEnumerable
接口是可选的。
pragma solidity ^0.4.24;
/// @title `ERC998ERC721` 自顶向下可组合非同质化代币
/// @dev 参见 https://github.com/ethereum/EIPs/blob/master/EIPS/eip-998.md
/// 注意:此接口的 ERC-165 标识符为 0xcde244d9
interface ERC998ERC721TopDown {
/// @dev 当代币收到子代币时,会发出此事件。
/// @param _from 代币之前的拥有者。
/// @param _toTokenId 接收子代币的代币。
event ReceivedChild(
address indexed _from,
uint256 indexed _toTokenId,
address indexed _childContract,
uint256 _childTokenId
);
/// @dev 当子代币从代币转移到地址时,会发出此事件。
/// @param _fromTokenId 要从中转移子代币的父代币。
/// @param _to 子代币的新所有者地址。
event TransferChild(
uint256 indexed _fromTokenId,
address indexed _to,
address indexed _childContract,
uint256 _childTokenId
);
/// @notice 获取 tokenId 的根所有者。
/// @param _tokenId 要查询根所有者地址的代币
/// @return rootOwner 代币树顶部的根所有者和 ERC-998 魔法值。
function rootOwnerOf(uint256 _tokenId) public view returns (bytes32 rootOwner);
/// @notice 获取子代币的根所有者。
/// @param _childContract 子代币的合约地址。
/// @param _childTokenId 子代币的 tokenId。
/// @return rootOwner 代币树顶部的根所有者和 ERC-998 魔法值。
function rootOwnerOfChild(
address _childContract,
uint256 _childTokenId
)
public
view
returns (bytes32 rootOwner);
/// @notice 获取子代币的父代币 tokenId。
/// @param _childContract 子代币的合约地址。
/// @param _childTokenId 子代币的 tokenId。
/// @return parentTokenOwner 父代币的父地址和 ERC-998 魔法值
/// @return parentTokenId _tokenId 的父代币 tokenId
function ownerOfChild(
address _childContract,
uint256 _childTokenId
)
external
view
returns (
bytes32 parentTokenOwner,
uint256 parentTokenId
);
/// @notice 代币收到子代币
/// @param _operator 导致转移的地址。
/// @param _from 子代币的拥有者。
/// @param _childTokenId 要转移到父代币的代币。
/// @param _data 前 32 个字节包含一个整数,该整数是接收的父 tokenId。
function onERC721Received(
address _operator,
address _from,
uint256 _childTokenId,
bytes _data
)
external
returns(bytes4);
/// @notice 将子代币从自顶向下可组合代币转移到地址。
/// @param _fromTokenId 要从中转移的拥有代币。
/// @param _to 接收子代币的地址
/// @param _childContract 子代币的 ERC-721 合约。
/// @param _childTokenId 要转移的代币的 tokenId。
function transferChild(
uint256 _fromTokenId,
address _to,
address _childContract,
uint256 _childTokenId
)
external;
/// @notice 将子代币从自顶向下可组合代币转移到地址。
/// @param _fromTokenId 要从中转移的拥有代币。
/// @param _to 接收子代币的地址
/// @param _childContract 子代币的 ERC-721 合约。
/// @param _childTokenId 要转移的代币的 tokenId。
function safeTransferChild(
uint256 _fromTokenId,
address _to,
address _childContract,
uint256 _childTokenId
)
external;
/// @notice 将子代币从自顶向下可组合代币转移到地址。
/// @param _fromTokenId 要从中转移的拥有代币。
/// @param _to 接收子代币的地址
/// @param _childContract 子代币的 ERC-721 合约。
/// @param _childTokenId 要转移的代币的 tokenId。
/// @param _data 没有指定格式的附加数据
function safeTransferChild(
uint256 _fromTokenId,
address _to,
address _childContract,
uint256 _childTokenId,
bytes _data
)
external;
/// @notice 将自底向上可组合子代币从自顶向下可组合代币转移到其他 ERC-721 代币。
/// @param _fromTokenId 要从中转移的拥有代币。
/// @param _toContract 接收代币的 ERC-721 合约
/// @param _toTokenId 接收代币
/// @param _childContract 子代币的自底向上可组合合约。
/// @param _childTokenId 要转移的代币。
/// @param _data 没有指定格式的附加数据
function transferChildToParent(
uint256 _fromTokenId,
address _toContract,
uint256 _toTokenId,
address _childContract,
uint256 _childTokenId,
bytes _data
)
external;
/// @notice 从 ERC-721 合约获取子代币。
/// @param _from 拥有子代币的地址。
/// @param _tokenId 变为父所有者的代币
/// @param _childContract 子代币的 ERC-721 合约
/// @param _childTokenId 子代币的 tokenId
function getChild(
address _from,
uint256 _tokenId,
address _childContract,
uint256 _childTokenId
)
external;
}
rootOwnerOf
1
/// @notice 获取 tokenId 的根所有者。
/// @param _tokenId 要查询根所有者地址的代币
/// @return rootOwner 代币树顶部的根所有者和 ERC-998 魔法值。
function rootOwnerOf(uint256 _tokenId) public view returns (bytes32 rootOwner);
此函数遍历代币所有者,直到找到 _tokenId
的根所有者地址。
rootOwner 的前 4 个字节包含 ERC-998 魔法值 0xcd740db5
。 最后 20 个字节包含根所有者地址。
返回魔法值是因为此函数可能在不知道合约是否具有 rootOwnerOf
函数的情况下在合约上调用。 魔法值用于此类调用,以确保收到有效返回值。
如果不知道合约是否具有 rootOwnerOf
函数,则必须将 rootOwner
返回值的前四个字节与 0xcd740db5
进行比较。
0xcd740db5
等于:
this.rootOwnerOf.selector ^ this.rootOwnerOfChild.selector ^
this.tokenOwnerOf.selector ^ this.ownerOfChild.selector;
这是一个 rootOwnerOf
返回的值的示例。
0xcd740db50000000000000000e5240103e1ff986a2c8ae6b6728ffe0d9a395c59
rootOwnerOfChild
/// @notice 获取子代币的根所有者。
/// @param _childContract 子代币的合约地址。
/// @param _childTokenId 子代币的 tokenId。
/// @return rootOwner 代币树顶部的根所有者和 ERC-998 魔法值。
function rootOwnerOfChild(
address _childContract,
uint256 _childTokenId
)
public
view
returns (bytes32 rootOwner);
此函数遍历代币所有者,直到找到提供的子代币的根所有者地址。
rootOwner 的前 4 个字节包含 ERC-998 魔法值 0xcd740db5
。 最后 20 个字节包含根所有者地址。
返回魔法值是因为此函数可能在不知道合约是否具有 rootOwnerOf
函数的情况下在合约上调用。 魔法值用于此类调用,以确保收到有效返回值。
如果不知道合约是否具有 rootOwnerOfChild
函数,则必须将 rootOwner
返回值的前四个字节与 0xcd740db5
进行比较。
ownerOfChild
/// @notice 获取子代币的父代币 tokenId。
/// @param _childContract 子代币的合约地址。
/// @param _childTokenId 子代币的 tokenId。
/// @return parentTokenOwner 父代币的父地址和 ERC-998 魔法值
/// @return parentTokenId _tokenId 的父代币 tokenId
function ownerOfChild(
address _childContract,
uint256 _childTokenId
)
external
view
returns (
address parentTokenOwner,
uint256 parentTokenId
);
此函数用于获取子代币的父代币 tokenId 并获取父代币的所有者地址。
parentTokenOwner 的前 4 个字节包含 ERC-998 魔法值 0xcd740db5
。 最后 20 个字节包含父代币所有者地址。
返回魔法值是因为此函数可能在不知道合约是否具有 ownerOfChild
函数的情况下在合约上调用。 魔法值用于此类调用,以确保收到有效返回值。
如果不知道合约是否具有 ownerOfChild
函数,则必须将 parentTokenOwner
返回值的前四个字节与 0xcd740db5
进行比较。
onERC721Received
/// @notice 代币收到子代币
/// @param _operator 导致转移的地址。
/// @param _from 子代币的拥有者。
/// @param _childTokenId 要转移到父代币的代币。
/// @param _data 前 32 个字节包含一个整数,该整数是接收的父 tokenId。
function onERC721Received(
address _operator,
address _from,
uint256 _childTokenId,
bytes _data
)
external
returns(bytes4);
这是在 ERC-721 标准中定义的函数。 当调用 safeTransferFrom
时,会在 ERC-721 合约中调用此函数。 bytes _data
参数包含一个从 1 到 32 字节长的整数值,该整数值是将 ERC-721 代币转移到的父 tokenId。
onERC721Received
函数是自顶向下可组合合约如何收到已将 ERC-721 代币转移到它并且自顶向下可组合代币中的哪个 tokenId 是父 tokenId 的通知。
onERC721Received
的返回值是魔法值 0x150b7a02
,它等于 bytes4(keccak256(abi.encodePacked("onERC721Received(address,address,uint256,bytes)")))
。
transferChild
/// @notice 将子代币从自顶向下可组合代币转移到地址。
/// @param _fromTokenId 要从中转移的拥有代币。
/// @param _to 接收子代币的地址
/// @param _childContract 子代币的 ERC-721 合约。
/// @param _childTokenId 要转移的代币的 tokenId。
function transferChild(
uint256 _fromTokenId,
address _to,
address _childContract,
uint256 _childTokenId
)
external;
此函数验证 msg.sender
并将子代币从自顶向下可组合代币转移到不同的地址。
此函数在其中进行以下调用:
ERC721(_childContract).transferFrom(this, _to, _childTokenId);
safeTransferChild 1
/// @notice 将子代币从自顶向下可组合代币转移到地址。
/// @param _fromTokenId 要从中转移的拥有代币。
/// @param _to 接收子代币的地址
/// @param _childContract 子代币的 ERC-721 合约。
/// @param _childTokenId 要转移的代币的 tokenId。
function safeTransferChild(
uint256 _fromTokenId,
address _to,
address _childContract,
uint256 _childTokenId
)
external;
此函数验证 msg.sender
并将子代币从自顶向下可组合代币转移到不同的地址。
此函数在其中进行以下调用:
ERC721(_childContract).safeTransferFrom(this, _to, _childTokenId);
safeTransferChild 2
/// @notice 将子代币从自顶向下可组合代币转移到地址或其他自顶向下可组合代币。
/// @param _fromTokenId 要从中转移的拥有代币。
/// @param _to 接收子代币的地址
/// @param _childContract 子代币的 ERC721 合约。
/// @param _childTokenId 要转移的代币的 tokenId。
/// @param _data 没有指定格式的附加数据,可用于指定要转移到的 tokenId
function safeTransferChild(
uint256 _fromTokenId,
address _to,
address _childContract,
uint256 _childTokenId,
bytes _data
)
external;
此函数验证 msg.sender
并将子代币从自顶向下可组合代币转移到不同的地址或不同的自顶向下可组合代币。
如果 _to
地址是自顶向下可组合合约,并且 bytes _data
提供了表示父 tokenId 的整数,则将子代币转移到不同的自顶向下可组合代币。
此函数在其中进行以下调用:
ERC721(_childContract).safeTransferFrom(this, _to, _childTokenId, _data);
transferChildToParent
/// @notice 将自底向上可组合子代币从自顶向下可组合代币转移到其他 ERC-721 代币。
/// @param _fromTokenId 要从中转移的拥有代币。
/// @param _toContract 接收代币的 ERC-721 合约
/// @param _toToken 接收代币
/// @param _childContract 子代币的自底向上可组合合约。
/// @param _childTokenId 要转移的代币。
/// @param _data 没有指定格式的附加数据
function transferChildToParent(
uint256 _fromTokenId,
address _toContract,
uint256 _toTokenId,
address _childContract,
uint256 _childTokenId,
bytes _data
)
external
此函数验证 msg.sender
并将子自底向上可组合代币从自顶向下可组合代币转移到不同的 ERC-721 代币。 仅当子代币是自底向上可组合代币时,才能使用此函数。 它旨在将自底向上可组合代币从自顶向下可组合代币以一个交易转移到 ERC-721 代币(自底向上样式)。
此函数在其中进行以下调用:
ERC998ERC721BottomUp(_childContract).transferToParent(
address(this),
_toContract,
_toTokenId,
_childTokenId,
_data
);
getChild
/// @notice 从 ERC-721 合约获取子代币。
/// @param _from 拥有子代币的地址。
/// @param _tokenId 变为父所有者的代币
/// @param _childContract 子代币的 ERC-721 合约
/// @param _childTokenId 子代币的 tokenId
function getChild(
address _from,
uint256 _tokenId,
address _childContract,
uint256 _childTokenId
)
external;
当 ERC-721 代币的合约没有 safeTransferChild(uint256 _fromTokenId, address _to, address _childContract, uint256 _childTokenId, bytes _data)
函数时,可以使用此函数来转移 ERC-721 代币。
使用此函数进行的转移分两个步骤完成:
- ERC-721 代币的拥有者在 ERC-721 合约中为自顶向下可组合合约调用
approve
或setApprovalForAll
。 - ERC-721 代币的拥有者在 ERC-721 代币的自顶向下可组合合约中调用
getChild
。
getChild
函数必须验证 msg.sender
是 ERC-721 合约中 ERC-721 代币的拥有者,或者已在 ERC-721 合约中获得批准或作为 ERC-721 代币的运营商。
ERC-721 自顶向下可组合枚举
用于自顶向下可组合枚举的可选接口:
/// @dev 此接口的 ERC-165 标识符为 0xa344afe4
interface ERC998ERC721TopDownEnumerable {
/// @notice 获取由 tokenId 拥有的具有代币的子合约的总数。
/// @param _tokenId 子合约中子代币的父代币
/// @return uint256 由 tokenId 拥有的具有代币的子合约的总数。
function totalChildContracts(uint256 _tokenId) external view returns(uint256);
/// @notice 按 tokenId 和索引获取子合约
/// @param _tokenId 子合约中子代币的父代币
/// @param _index 子合约的索引位置
/// @return childContract 在 tokenId 和索引处找到的合约。
function childContractByIndex(
uint256 _tokenId,
uint256 _index
)
external
view
returns (address childContract);
/// @notice 获取存在于子合约中且由 tokenId 拥有的子代币的总数。
/// @param _tokenId 子代币的父代币
/// @param _childContract 包含子代币的子合约
/// @return uint256 在子合约中找到的且由 tokenId 拥有的子代币的总数。
function totalChildTokens(
uint256 _tokenId,
address _childContract
)
external
view
returns(uint256);
/// @notice 在索引位置获取由 tokenId 拥有的子合约中的子代币
/// @param _tokenId 子代币的父代币
/// @param _childContract 子代币的子合约
/// @param _index 子代币的索引位置。
/// @return childTokenId 父代币、子代币和索引的子 tokenId
function childTokenByIndex(
uint256 _tokenId,
address _childContract,
uint256 _index
)
external
view
returns (uint256 childTokenId);
}
ERC-20自顶向下可组合代币
ERC-20 自顶向下可组合代币充当 ERC-20 代币的容器。
ERC-20 自顶向下可组合代币是可以接收、持有和转移 ERC-20 代币的 ERC-721 代币。
有两种方法可以将 ERC-20 代币转移到 ERC-20 自顶向下可组合代币:
- 使用
ERC-223
合约中的transfer(address _to, uint256 _value, bytes _data);
函数。_to
参数是 ERC-20 自顶向下可组合合约地址。_value
参数是要转移的 ERC-20 代币的数量。bytes
参数保存接收 ERC-20 代币的自顶向下可组合 tokenId 的整数值。 - 在 ERC-20 合约中为 ERC-20 自顶向下可组合合约调用
approve
。 然后从 ERC-20 自顶向下可组合合约调用getERC20(address _from, uint256 _tokenId, address _erc20Contract, uint256 _value)
。
第一种方法适用于支持 ERC-223
标准的 ERC-20 合约。 第二种方法适用于不支持的合约。
ERC-20 自顶向下可组合代币实现以下接口:
/// @title `ERC998ERC20` 自顶向下可组合的非同质化代币
/// @dev 参见 https://github.com/ethereum/EIPs/blob/master/EIPS/eip-998.md
/// 注意:此接口的 ERC-165 标识符为 0x7294ffed
interface ERC998ERC20TopDown {
/// @dev 当代币收到 ERC-20 代币时,会发出此事件。
/// @param _from 代币之前的拥有者。
/// @param _toTokenId 接收 ERC-20 代币的代币。
/// @param _erc20Contract ERC-20 合约。
/// @param _value 收到的 ERC-20 代币的数量。
event ReceivedERC20(
address indexed _from,
uint256 indexed _toTokenId,
address indexed _erc20Contract,
uint256 _value
);
/// @dev 当代币转移 ERC-20 代币时,会发出此事件。
/// @param _tokenId 拥有 ERC-20 代币的代币。
/// @param _to 接收 ERC-20 代币的地址。
/// @param _erc20Contract ERC-20 合约该函数来自 `ERC-223`,它是 ERC-20 标准的扩展。当 ERC-20 代币被转移时,会从发送合约调用接收合约上的此函数。此函数是 ERC-20 自上而下可组合合约如何收到其某个代币收到 ERC-20 代币的通知的方式。哪个代币收到了 ERC-20 代币在 `_data` 参数中指定。
#### `balanceOfERC20`
```solidity
/// @notice 查询特定代币和 ERC-20 合约的 ERC-20 代币余额
/// @param _tokenId 拥有 ERC-20 代币的代币
/// @param _erc20Contract ERC-20 合约
/// @return 从 ERC-20 合约中一个代币拥有的 ERC-20 代币数量
function balanceOfERC20(
uint256 _tokenId,
address _erc20Contract
)
external
view
returns(uint256);
获取特定 ERC-20 合约中一个代币拥有的 ERC-20 代币余额。
transferERC20
/// @notice 将 ERC-20 代币转移到地址
/// @param _tokenId 要转移的代币
/// @param _value 要将 ERC-20 代币发送到的地址
/// @param _erc20Contract ERC-20 合约
/// @param _value 要转移的 ERC-20 代币数量
function transferERC20(
uint256 _tokenId,
address _to,
address _erc20Contract,
uint256 _value
)
external;
用于将 ERC-20 代币从一个代币转移到一个地址。此函数调用 ERC20(_erc20Contract).transfer(_to, _value)
;
此函数必须验证 msg.sender
。
transferERC223
/// @notice 将 ERC-20 代币转移到地址或 ERC-20 自上而下可组合对象
/// @param _tokenId 要转移的代币
/// @param _value 要将 ERC-20 代币发送到的地址
/// @param _erc223Contract `ERC-223` 代币合约
/// @param _value 要转移的 ERC-20 代币数量
/// @param _data 没有指定格式的附加数据,可用于指定要转移到的 tokenId
function transferERC223(
uint256 _tokenId,
address _to,
address _erc223Contract,
uint256 _value,
bytes _data
)
external;
此函数来自 ERC-223
。它用于将 ERC-20 代币从一个代币转移到一个地址,或通过将整数代币值放入 _data
参数中来转移到另一个代币。
此函数必须验证 msg.sender
。
getERC20
/// @notice 从 ERC-20 合约获取 ERC-20 代币。
/// @param _from 正在转移的 ERC-20 代币的当前所有者地址。
/// @param _tokenId 要将 ERC-20 代币转移到的代币。
/// @param _erc20Contract ERC-20 代币合约
/// @param _value 要转移的 ERC-20 代币数量
function getERC20(
address _from,
uint256 _tokenId,
address _erc20Contract,
uint256 _value
)
external;
当 ERC-20 合约没有 transferERC223(uint256 _tokenId, address _to, address _erc223Contract, uint256 _value, bytes _data)
函数时,此函数用于将 ERC-20 代币转移到 ERC-20 自上而下可组合对象。
在使用此函数之前,必须在 ERC-20 合约中批准 ERC-20 自上而下可组合合约地址以转移 ERC-20 代币。
此函数必须验证 msg.sender
等于 _from
或已在 ERC-20 合约中获得批准。
ERC-20 自上而下可组合枚举
自上而下可组合枚举的可选接口:
/// @dev 此接口的 ERC-165 标识符为 0xc5fd96cd
interface ERC998ERC20TopDownEnumerable {
/// @notice 获取代币拥有 ERC-20 代币的 ERC-20 合约数量
/// @param _tokenId 拥有 ERC-20 代币的代币。
/// @return uint256 ERC-20 合约的数量
function totalERC20Contracts(uint256 _tokenId) external view returns(uint256);
/// @notice 通过索引获取代币拥有 ERC-20 代币的 ERC-20 合约
/// @param _tokenId 拥有 ERC-20 代币的代币。
/// @param _index ERC-20 合约的索引位置。
/// @return address ERC-20 合约
function erc20ContractByIndex(
uint256 _tokenId,
uint256 _index
)
external
view
returns(address);
}
ERC-721 自下而上可组合
ERC-721 自下而上可组合对象是将自身附加到其他 ERC-721 代币的 ERC-721 代币。
ERC-721 自下而上可组合合约存储代币的所有者地址和父 tokenId(如果有)。
/// @title `ERC998ERC721` 自下而上可组合非同质化代币
/// @dev 参见 https://github.com/ethereum/EIPs/blob/master/EIPS/eip-998.md
/// 注意:此接口的 ERC-165 标识符为 0xa1b23002
interface ERC998ERC721BottomUp {
/// @dev 当代币转移到 ERC-721 代币时,会发出此事件
/// @param _toContract 代币转移到的合约
/// @param _toTokenId 代币转移到的代币
/// @param _tokenId 转移的代币
event TransferToParent(
address indexed _toContract,
uint256 indexed _toTokenId,
uint256 _tokenId
);
/// @dev 当代币从 ERC-721 代币转移时,会发出此事件
/// @param _fromContract 代币转移到的合约
/// @param _fromTokenId 代币转移到的代币
/// @param _tokenId 转移的代币
event TransferFromParent(
address indexed _fromContract,
uint256 indexed _fromTokenId,
uint256 _tokenId
);
/// @notice 获取 tokenId 的根所有者。
/// @param _tokenId 要查询根所有者地址的代币
/// @return rootOwner 代币树顶部的根所有者和 ERC-998 魔法值。
function rootOwnerOf(uint256 _tokenId) external view returns (bytes32 rootOwner);
/// @notice 获取代币的所有者地址和父代币(如果有)
/// @param _tokenId 要查询的 tokenId。
/// @return tokenOwner 代币的所有者地址
/// @return parentTokenId 代币的父所有者和 ERC-998 魔法值
/// @return isParent 如果 parentTokenId 是有效的父 tokenId,则为 True,如果没有父 tokenId,则为 false
function tokenOwnerOf(
uint256 _tokenId
)
external
view
returns (
bytes32 tokenOwner,
uint256 parentTokenId,
bool isParent
);
/// @notice 将代币从所有者地址转移到代币
/// @param _from 所有者地址
/// @param _toContract 接收代币的 ERC-721 合约
/// @param _toToken 接收代币
/// @param _data 没有指定格式的附加数据
function transferToParent(
address _from,
address _toContract,
uint256 _toTokenId,
uint256 _tokenId,
bytes _data
)
external;
/// @notice 将代币从代币转移到地址
/// @param _fromContract 拥有合约的地址
/// @param _fromTokenId 拥有代币
/// @param _to 代币转移到的地址。
/// @param _tokenId 转移的代币
/// @param _data 没有指定格式的附加数据
function transferFromParent(
address _fromContract,
uint256 _fromTokenId,
address _to,
uint256 _tokenId,
bytes _data
)
external;
/// @notice 将代币从代币转移到另一个代币
/// @param _fromContract 拥有合约的地址
/// @param _fromTokenId 拥有代币
/// @param _toContract 接收代币的 ERC-721 合约
/// @param _toToken 接收代币
/// @param _tokenId 转移的代币
/// @param _data 没有指定格式的附加数据
function transferAsChild(
address _fromContract,
uint256 _fromTokenId,
address _toContract,
uint256 _toTokenId,
uint256 _tokenId,
bytes _data
)
external;
}
rootOwnerOf
/// @notice 获取 tokenId 的根所有者。
/// @param _tokenId 要查询根所有者地址的代币
/// @return rootOwner 代币树顶部的根所有者和 ERC-998 魔法值。
function rootOwnerOf(uint256 _tokenId) public view returns (bytes32 rootOwner);
此函数遍历代币所有者,直到找到 _tokenId
的根所有者地址。
rootOwner 的前 4 个字节包含 ERC-998 魔法值 0xcd740db5
。最后 20 个字节包含根所有者地址。
返回魔法值是因为当不知道合约是否具有 rootOwnerOf
函数时,可能会在合约上调用此函数。该魔法值用于此类调用中,以确保收到有效的返回值。
如果不知道合约是否具有 rootOwnerOf
函数,则必须将 rootOwner
返回值的前四个字节与 0xcd740db5
进行比较。
0xcd740db5
等于:
this.rootOwnerOf.selector ^ this.rootOwnerOfChild.selector ^
this.tokenOwnerOf.selector ^ this.ownerOfChild.selector;
这是 rootOwnerOf
返回的值的示例。
0xcd740db50000000000000000e5240103e1ff986a2c8ae6b6728ffe0d9a395c59
tokenOwnerOf
/// @notice 获取代币的所有者地址和父代币(如果有)
/// @param _tokenId 要查询的 tokenId。
/// @return tokenOwner 代币的所有者地址和 ERC-998 魔法值。
/// @return parentTokenId 代币的父所有者
/// @return isParent 如果 parentTokenId 是有效的父 tokenId,则为 True,如果没有父 tokenId,则为 false
function tokenOwnerOf(
uint256 _tokenId
)
external
view
returns (
bytes32 tokenOwner,
uint256 parentTokenId,
bool isParent
);
此函数用于获取代币的拥有地址和父 tokenId(如果合约中存储了父 tokenId)。
如果 isParent
为 true,则 tokenOwner
是拥有 ERC-721 合约地址,parentTokenId
是有效的父 tokenId。如果 isParent
为 false,则 tokenOwner
是用户地址,parentTokenId
不包含有效的父 tokenId,必须忽略。
tokenOwner
的前 4 个字节包含 ERC-998 魔法值 0xcd740db5
。最后 20 个字节包含代币所有者地址。
返回魔法值是因为当不知道合约是否具有 tokenOwnerOf
函数时,可能会在合约上调用此函数。该魔法值用于此类调用中,以确保收到有效的返回值。
如果不知道合约是否具有 rootOwnerOf
函数,则必须将 tokenOwner
返回值的前四个字节与 0xcd740db5
进行比较。
transferToParent
/// @notice 将代币从所有者地址转移到代币
/// @param _from 所有者地址
/// @param _toContract 接收代币的 ERC-721 合约
/// @param _toToken 接收代币
/// @param _data 没有指定格式的附加数据
function transferToParent(
address _from,
address _toContract,
uint256 _toTokenId,
uint256 _tokenId,
bytes _data
)
external;
此函数用于将代币从地址转移到代币。必须验证 msg.sender
。
此函数必须检查 _toContract
中是否存在 _toToken
,如果不存在,则抛出异常。
transferFromParent
/// @notice 将代币从代币转移到地址
/// @param _fromContract 拥有合约的地址
/// @param _fromTokenId 拥有代币
/// @param _to 代币转移到的地址。
/// @param _tokenId 转移的代币
/// @param _data 没有指定格式的附加数据
function transferFromParent(
address _fromContract,
uint256 _fromTokenId,
address _to,
uint256 _tokenId,
bytes _data
)
external;
此函数用于将代币从代币转移到地址。必须验证 msg.sender
。
此函数必须检查 _fromContract
和 _fromTokenId
是否拥有 _tokenId
,否则抛出异常。
transferAsChild
/// @notice 将代币从代币转移到另一个代币
/// @param _fromContract 拥有合约的地址
/// @param _fromTokenId 拥有代币
/// @param _toContract 接收代币的 ERC-721 合约
/// @param _toToken 接收代币
/// @param _tokenId 转移的代币
/// @param _data 没有指定格式的附加数据
function transferAsChild(
address _fromContract,
uint256 _fromTokenId,
address _toContract,
uint256 _toTokenId,
uint256 _tokenId,
bytes _data
)
external;
此函数用于将代币从代币转移到另一个代币。必须验证 msg.sender
。
此函数必须检查 _toContract
中是否存在 _toToken
,如果不存在,则抛出异常。
此函数必须检查 _fromContract
和 _fromTokenId
是否拥有 _tokenId
,否则抛出异常。
ERC-721 自下而上可组合枚举
自下而上可组合枚举的可选接口:
/// @dev 此接口的 ERC-165 标识符为 0x8318b539
interface ERC998ERC721BottomUpEnumerable {
/// @notice 获取父代币拥有的 ERC-721 代币数量。
/// @param _parentContract 父 ERC-721 代币来自的合约。
/// @param _parentTokenId 拥有代币的父 tokenId
// @return uint256 父代币拥有的 ERC-721 代币数量。
function totalChildTokens(
address _parentContract,
uint256 _parentTokenId
)
external
view
returns (uint256);
/// @notice 按索引获取子代币
/// @param _parentContract 父 ERC-721 代币来自的合约。
/// @param _parentTokenId 拥有代币的父 tokenId
/// @param _index 子代币的索引位置
/// @return uint256 父代币拥有的子 tokenId
function childTokenByIndex(
address _parentContract,
uint256 _parentTokenId,
uint256 _index
)
external
view
returns (uint256);
}
ERC-20 自下而上可组合
ERC-20 自下而上可组合对象是将自身附加到 ERC-721 代币的 ERC-20 代币,或者由用户地址拥有,如标准 ERC-20 代币。
当由 ERC-721 代币拥有时,ERC-20 自下而上可组合合约存储代币的所有者地址和父 tokenId。ERC-20 自下而上可组合对象向 ERC-20 和 ERC-223
接口添加了多个方法,允许查询父代币的余额,以及将代币转移到父代币、从父代币转移到父代币以及在父代币之间转移。
可以通过添加一个额外的映射来跟踪代币的余额,以及标准映射用于跟踪用户地址余额来实现此功能。
/// @dev 此映射跟踪标准 ERC20/`ERC-223` 所有权,其中地址拥有
/// 特定数量的代币。
mapping(address => uint) userBalances;
/// @dev 此附加映射跟踪 ERC-998 所有权,其中 ERC-721 代币拥有
/// 特定数量的代币。 这跟踪 contractAddres => tokenId => balance
mapping(address => mapping(uint => uint)) nftBalances;
完整的接口如下。
/// @title `ERC998ERC20` 自下而上可组合同质化代币
/// @dev 参见 https://github.com/ethereum/EIPs/blob/master/EIPS/eip-998.md
/// 注意:此接口的 ERC-165 标识符为 0xffafa991
interface ERC998ERC20BottomUp {
/// @dev 当代币转移到 ERC-721 代币时,会发出此事件
/// @param _toContract 代币转移到的合约
/// @param _toTokenId 代币转移到的代币
/// @param _amount 转移的代币数量
event TransferToParent(
address indexed _toContract,
uint256 indexed _toTokenId,
uint256 _amount
);
/// @dev 当代币从 ERC-721 代币转移时,会发出此事件
/// @param _fromContract 代币转移到的合约
/// @param _fromTokenId 代币转移到的代币
/// @param _amount 转移的代币数量
event TransferFromParent(
address indexed _fromContract,
uint256 indexed _fromTokenId,
uint256 _amount
);
/// @notice 获取非同质化父代币的余额
/// @param _tokenContract 跟踪父代币的合约
/// @param _tokenId 父代币的 ID
/// @return amount 代币的余额
function balanceOfToken(
address _tokenContract,
uint256 _tokenId
)
external
view
returns (uint256 amount);
/// @notice 将代币从所有者地址转移到代币
/// @param _from 所有者地址
/// @param _toContract 接收代币的 ERC-721 合约
/// @param _toToken 接收代币
/// @param _amount 要转移的代币数量
function transferToParent(
address _from,
address _toContract,
uint256 _toTokenId,
uint256 _amount
)
external;
/// @notice 将代币从代币转移到地址
/// @param _fromContract 拥有合约的地址
/// @param _fromTokenId 拥有代币
/// @param _to 代币转移到的地址
/// @param _amount 要转移的代币数量
function transferFromParent(
address _fromContract,
uint256 _fromTokenId,
address _to,
uint256 _amount
)
external;
/// @notice 使用 `ERC-223` 语义将代币从代币转移到地址
/// @param _fromContract 拥有合约的地址
/// @param _fromTokenId 拥有代币
/// @param _to 代币转移到的地址
/// @param _amount 要转移的代币数量
/// @param _data 没有指定格式的附加数据,可用于指定发送者 tokenId
function transferFromParentERC223(
address _fromContract,
uint256 _fromTokenId,
address _to,
uint256 _amount,
bytes _data
)
external;
/// @notice 将代币从代币转移到另一个代币
/// @param _fromContract 拥有合约的地址
/// @param _fromTokenId 拥有代币
/// @param _toContract 接收代币的 ERC-721 合约
/// @param _toToken 接收代币
/// @param _amount 要转移的代币数量
function transferAsChild(
address _fromContract,
uint256 _fromTokenId,
address _toContract,
uint256 _toTokenId,
uint256 _amount
)
external;
}
balanceOfToken
/// @notice 获取非同质化父代币的余额
/// @param _tokenContract 跟踪父代币的合约
/// @param _tokenId 父代币的 ID
/// @return amount 代币的余额
function balanceOfToken(
address _tokenContract,
uint256 _tokenId
)
external
view
returns (uint256 amount);
此函数返回非同质化代币的余额。它镜像了标准 ERC-20 方法 balanceOf
,但接受父代币合约的地址和父代币的 ID。此方法的行为与 balanceOf
相同,但检查的是 ERC-721 代币的所有权,而不是用户地址。
transferToParent
/// @notice 将代币从所有者地址转移到代币
/// @param _from 所有者地址
/// @param _toContract 接收代币的 ERC-721 合约
/// @param _toToken 接收代币
/// @param _amount 要转移的代币数量
function transferToParent(
address _from,
address _toContract,
uint256 _toTokenId,
uint256 _amount
)
external;
此函数将一定数量的代币从用户地址转移到 ERC-721 代币。此函数必须确保接收者合约使用 ERC-165 supportsInterface
函数来实现 ERC-721。此函数应该通过调用接收者代币合约上的 ownerOf
并确保它既不抛出异常也不返回零地址来确保接收者代币实际存在。此函数必须在成功转移后发出 TransferToParent
事件(除了标准 ERC-20 Transfer
事件!)。如果 _from
帐户余额没有足够的代币可供消费,则此函数必须抛出异常。
transferFromParent
/// @notice 将代币从代币转移到地址
/// @param _fromContract 拥有合约的地址
/// @param _fromTokenId 拥有代币
/// @param _to 代币转移到的地址
/// @param _amount 要转移的代币数量
function transferFromParent(
address _fromContract,
uint256 _fromTokenId,
address _to,
uint256 _amount
)
external;
此函数将一定数量的代币从 ERC-721 代币转移到地址。此函数必须在成功转移后发出 TransferFromParent
事件(除了标准 ERC-20 Transfer
事件!)。如果发送者 ERC-721 代币的余额小于指定的 _amount
,则此函数必须抛出异常。此函数必须验证 msg.sender
是否拥有发送者 ERC-721 代币,否则必须抛出异常。
transferFromParentERC223
/// @notice 使用 `ERC-223` 语义将代币从代币转移到地址
/// @param _fromContract 拥有合约的地址
/// @param _fromTokenId 拥有代币
/// @param _to 代币转移到的地址
/// @param _amount 要转移的代币数量
/// @param _data 没有指定格式的附加数据,可用于指定发送者 tokenId
function transferFromParentERC223(
address _fromContract,
uint256 _fromTokenId,
address _to,
uint256 _amount,
bytes _data
)
external;
此函数将一定数量的代币从 ERC-721 代币转移到地址。此函数与 transferFromParent
具有相同的要求,只不过它还必须根据 ERC-223
的规定在接收者地址上调用 tokenFallback
(如果该地址是合约)。
transferAsChild 1
/// @notice 将代币从代币转移到另一个代币
/// @param _fromContract 拥有合约的地址
/// @param _fromTokenId 拥有代币
/// @param _toContract 接收代币的 ERC-721 合约
/// @param _toToken 接收代币
/// @param _amount 要转移的代币数量
function transferAsChild(
address _fromContract,
uint256 _fromTokenId,
address _toContract,
uint256 _toTokenId,
uint256 _amount
)
external;
此函数将一定数量的代币从 ERC-721 代币转移到另一个 ERC-721 代币。此函数必须发出 TransferFromParent
和 TransferToParent
事件(除了标准 ERC-20 Transfer
事件!)。如果发送者 ERC-721 代币的余额小于指定的 _amount
,则此函数必须抛出异常。此函数必须验证 msg.sender
是否拥有发送者 ERC-721 代币,否则必须抛出异常。此函数必须确保接收者合约使用 ERC-165 supportsInterface
函数来实现 ERC-721。此函数应该通过调用接收者代币合约上的 ownerOf
并确保它既不抛出异常也不返回零地址来确保接收者代币实际存在。
笔记
为了向后兼容,无论发送者和接收者是地址还是 ERC-721 代币,实现都必须在发生转移时发出标准 ERC-20 Transfer
事件。如果发送者或接收者是代币,则 Transfer
事件中的相应参数应该是代币的合约地址。
除了此接口中指定的函数之外,实现还必须实现所有 ERC-20 和 ERC-223
函数。
理由
存在两种不同类型的可组合对象(自上而下和自下而上)来处理不同的用例。常规 ERC-721 代币不能拥有自上而下可组合对象,但它可以拥有自下而上可组合对象。自下而上可组合对象不能拥有常规 ERC-721,但自上而下可组合对象可以拥有常规 ERC-721 代币。拥有多种类型的可组合对象可以实现不同的代币所有权可能性。
使用哪种可组合对象?
如果要将常规 ERC-721 代币转移到非同质化代币,请使用自上而下可组合对象。
如果要将非同质化代币转移到常规 ERC-721 代币,请使用自下而上可组合对象。
显式转移参数
每个 ERC-998 转移函数都包含显式参数,用于指定代币的先前所有者和新所有者。显式提供 from 和 to 是有意为之,旨在避免以意外方式转移代币的情况。
以下是一个如果 from 未在转移函数中显式提供可能发生的情况的示例:
交易所合约是特定可组合合约中用户 A、用户 B 和用户 C 的批准运营商。
用户 A 将代币 1 转移给用户 B。同时,交易所合约将代币 1 转移给用户 C(隐含地意图从用户 A 转移)。用户 B 在被错误地转移给用户 C 之前获得代币 1 一分钟。第二次转移应该失败,但它没有失败,因为没有提供显式的 from 来确保代币 1 来自用户 A。
向后兼容性
可组合对象旨在与 ERC-721、ERC-223
和 ERC-20 代币一起使用。
一些较旧的 ERC-721 合约没有 safeTransferFrom
函数。getChild
函数仍然可以用于将代币转移到 ERC-721 自上而下可组合对象。
如果 ERC-20 合约没有 ERC-223
函数 transfer(address _to, uint _value, bytes _data)
,则仍然可以使用getERC20
函数将 ERC-20 代币转移到 ERC-20 自上而下可组合对象。
参考实现
可以在此处找到实现:https://github.com/mattlockyer/composables-998
安全考虑
需要讨论。
版权
版权和相关权利通过 CC0 放弃。
Citation
Please cite this document as:
, "EIP-: ```md," Ethereum Improvement Proposals, no. , . [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-.