ERC-7743: 多所有者非同质化代币 (MO-NFT)
支持多所有者的非同质化代币,允许用户之间的共享所有权和可转让性。
Authors | Cheng Qian (@jamesavechives) <james.walstonn@gmail.com> |
---|---|
Created | 2024-07-13 |
Table of Contents
摘要
本 ERC 提出了一种支持多所有者的非同质化代币 (NFT) 的新标准。MO-NFT 标准允许单个 NFT 拥有多个所有者,反映了数字资产的共享和可分配性。该模型结合了提供者定义转让费和所有权归档的机制,从而实现灵活且协作的所有权结构。它与现有的 ERC-721 标准保持兼容,以确保与当前工具和平台的互操作性。
动机
传统的 NFT 强制执行单一所有权模型,这与数字资产固有的可复制性和协作潜力不符。MO-NFT 允许共享所有权,在保持安全访问控制的同时,促进更广泛的分配和协作。包含提供者费用和所有权归档增强了 NFT 在表示数字资产和服务方面的效用和灵活性。
规范
代币创建和所有权模型
-
铸造:
-
函数
mintToken()
允许创建新的 MO-NFT。调用者既成为代币的初始所有者,也成为提供者。function mintToken() public onlyOwner returns (uint256);
-
生成一个新的
tokenId
,并将调用者添加到所有者集合中,并记录为提供者。调用者的balanceOf
增加。
-
-
所有权列表:
- MO-NFT 维护每个代币的所有者列表。所有者存储在一个可枚举的集合中,以防止重复并允许高效查找。
-
提供者角色:
- 提供者是可以设置和更新
transferValue
费用的初始所有者。只有提供者才能修改某些代币参数。
- 提供者是可以设置和更新
-
转移机制:
-
所有者可以使用
transferFrom
将代币转移给新的所有者。转移会将新的所有者添加到列表中,而不会删除现有所有者,并将transferValue
费用转移给提供者。function transferFrom(address from, address to, uint256 tokenId) public;
-
所有权转移
-
累加所有权:
- 转移所有权会将新的所有者添加到所有权列表中,而不会删除当前所有者。这种方法反映了数字资产的共享性质。
-
提供者费用处理:
- 在转移过程中,指定的
transferValue
费用将转移给提供者。合约必须有足够的余额来支付这笔费用。
- 在转移过程中,指定的
-
归档所有权:
-
所有者可以将自己标记为已归档特定代币,这意味着他们不能再转让该代币。一旦设置,此状态无法撤销。而对于数字资产平台,此状态意味着所有者可以直接下载真实的数字资产;如果未归档,则必须先更改为已归档,然后所有者才能下载真实的数字资产。
function archive(uint256 tokenId) external;
-
接口定义
铸造函数
-
function mintToken() public onlyOwner returns (uint256);
-
function provide(string memory assetName, uint256 size, bytes32 fileHash, address provider, uint256 transferValue) external returns (uint256);
转移函数
-
function transferFrom(address from, address to, uint256 tokenId) public;
-
function safeTransferFrom(address from, address to, uint256 tokenId) public;
(已禁用或覆盖) -
function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public;
(已禁用或覆盖)
所有权管理函数
-
function isOwner(uint256 tokenId, address account) public view returns (bool);
-
function getOwnersCount(uint256 tokenId) public view returns (uint256);
-
function balanceOf(address owner) external view returns (uint256 balance);
-
function ownerOf(uint256 tokenId) external view returns (address owner);
提供者函数
function setTransferValue(uint256 tokenId, uint256 newTransferValue) external;
归档状态函数
function archive(uint256 tokenId) external;
事件
-
event TokenMinted(uint256 indexed tokenId, address indexed owner);
-
event TokenTransferred(uint256 indexed tokenId, address indexed from, address indexed to);
-
event TransferValueUpdated(uint256 indexed tokenId, uint256 oldTransferValue, uint256 newTransferValue);
-
event ArchivedStatusUpdated(uint256 indexed tokenId, address indexed owner, bool archived);
ERC-721 兼容性
MO-NFT 标准旨在与 ERC-721 标准兼容。它实现了 ERC721
接口中的必需函数,例如 balanceOf
、ownerOf
和 transferFrom
。
-
批准函数:
approve
、getApproved
、setApprovalForAll
和isApprovedForAll
等函数被有意禁用或覆盖,因为它们与 MO-NFT 多所有者模型不一致。 -
安全转移函数:
safeTransferFrom
函数受到限制,因为当所有权是累加的而不是排他的时,传统的 ERC-721 转移安全检查不适用。 -
支持接口:
supportsInterface
函数确保 MO-NFT 声明与 ERC-721 标准的兼容性,允许它与现有工具和平台集成。function supportsInterface(bytes4 interfaceId) public view returns (bool) { return interfaceId == type(IERC721).interfaceId || interfaceId == type(IERC165).interfaceId; }
理由
-
多所有权模型:
- 数字资产本质上是可复制的,可以共享而不会损失质量。多所有者模型允许更广泛的分配和协作,同时保持唯一的代币身份。
-
累加所有权:
- 通过添加新的所有者而不删除现有的所有者,我们可以支持协作环境和数字内容分发中常见的共享所有权模型。
-
提供者费用机制:
- 纳入提供者费用可以通过在资产转移时奖励创作者和提供者来激励他们。这与创作者为其作品收取版税或费用的模型相一致。
-
所有权归档:
- 允许所有者从所有权列表中归档自己,从而提供灵活性,使所有者能够放弃权利或阻止资产的进一步转移。这取代了以前的“销毁”所有权的概念。
-
ERC-721 兼容性:
- 保持与 ERC-721 的兼容性允许 MO-NFT 利用现有的基础设施、工具和平台,从而促进采用和互操作性。
向后兼容性
虽然 MO-NFT 标准旨在保持与 ERC-721 的兼容性,但由于多所有者模型,某些偏差是必要的:
- 禁用批准函数: 传统的 ERC-721 批准假设单个所有者有权授予或撤销批准。在 MO-NFT 中,一个代币可以有多个所有者。如果没有协议定义的“共同所有权共识”机制,如果只有一个共同所有者授予批准,批准系统将为意外或不需要的转移打开大门。我们认为这种方法可能会破坏共享所有权模型,因此参考实现选择恢复对批准相关函数的调用。
这可能会限制与依赖 approve 或 setApprovalForAll 的现有 NFT 平台和市场的兼容性。原则上,开发人员可以使用更高级的“多签名”或“基于共识”的批准来扩展 MO-NFT 标准,但这超出了此初始 EIP 的范围。我们的目标是保持核心标准的最小化,并避免为基于单所有者假设构建的市场造成混淆。
- 安全转移函数: ERC-721 转移的“安全”变体 (safeTransferFrom) 用于保护代币免于意外发送到不兼容的合约。因为 MO-NFT 的多所有者方法不会固有地破坏“检查接收者是否可以处理 ERC-721 代币的逻辑。
以与 ERC-721 相同的方式实现“安全”:添加新所有者后,检查接收者合约上的 onERC721Received
。
ownerOf
函数: 返回所有者列表中第一个所有者以实现兼容性,但单个所有者的概念并不完全适用。
在将 MO-NFT 集成到为标准 ERC-721 代币设计的系统中时,开发人员应注意这些差异。
测试用例
-
铸造 MO-NFT 并验证初始所有权:
-
输入:
- 以提供者身份调用
mintToken()
。
- 以提供者身份调用
-
预期输出:
-
生成一个新的
tokenId
。 -
调用者被添加为第一个所有者。
-
调用者的
balanceOf
增加 1。 -
为代币记录提供者。
-
发出
TokenMinted
事件。
-
-
-
转移 MO-NFT 并验证提供者费用转移:
-
输入:
- 调用
transferFrom(from, to, tokenId)
,其中from
是现有所有者,to
是新地址。
- 调用
-
预期输出:
-
to
地址被添加到所有者列表中。 -
transferValue
费用转移给提供者。 -
to
地址的balanceOf
增加 1。 -
发出
TokenTransferred
事件。
-
-
-
归档所有权:
- 输入:
- 所有者调用
archive(tokenId)
。
- 所有者调用
- 预期输出:
- 所有者对该代币的转移能力被归档。
- 所有者不能再转移该代币。
- 发出
ArchivedStatusUpdated
事件。
- 输入:
-
设置转让价值:
-
输入:
- 提供者调用
setTransferValue(tokenId, newTransferValue)
。
- 提供者调用
-
预期输出:
-
transferValue
在合约中更新。 -
发出
TransferValueUpdated
事件。
-
-
-
转移到现有所有者失败:
-
输入:
- 尝试
transferFrom
到已经是所有者的地址。
- 尝试
-
预期输出:
-
交易回滚并显示错误
"MO-NFT: Recipient is already an owner"
。 -
所有权或余额未发生任何变化。
-
-
参考实现
MO-NFT 标准的完整参考实现代码包含在 EIPs 存储库的 assets 文件夹下。这确保了代码与 EIP 一起保存并保持可访问。
-
合约:
-
MONFT.sol
: MO-NFT 标准的基本实现。 -
DigitalAsset.sol
: 针对具有提供者费用的数字资产的扩展实现。
-
-
接口:
IDigitalAsset.sol
: 定义数字资产管理功能的接口。
参考实现中的关键函数
铸造代币
function mintToken() public onlyOwner returns (uint256) {
_nextTokenId++;
// Add the sender to the set of owners for the new token
// 将发送者添加到新代币的所有者集合中
_owners[_nextTokenId].add(msg.sender);
// Increment the balance of the owner
// 增加所有者的余额
_balances[msg.sender] += 1;
// Set the provider to the caller
// 将提供者设置为调用者
_providers[_nextTokenId] = msg.sender;
emit TokenMinted(_nextTokenId, msg.sender);
return _nextTokenId;
}
转移代币
function transferFrom(address from, address to, uint256 tokenId) public override {
require(isOwner(tokenId, msg.sender), "MO-NFT: Caller is not an owner");
require(to != address(0), "MO-NFT: Transfer to zero address");
require(!isOwner(tokenId, to), "MO-NFT: Recipient is already an owner");
// Add the new owner to the set
// 将新所有者添加到集合中
_owners[tokenId].add(to);
_balances[to] += 1;
// Transfer the transferValue to the provider
// 将 transferValue 转移给提供者
uint256 transferValue = _transferValues[tokenId];
address provider = _providers[tokenId];
require(address(this).balance >= transferValue, "Insufficient contract balance");
(bool success, ) = provider.call{value: transferValue}("");
require(success, "Transfer to provider failed");
emit TokenTransferred(tokenId, from, to);
}
归档所有权
function archive(uint256 tokenId) external {
require(
isOwner(tokenId, msg.sender),
"MO-NFT: Caller is not the owner of this token"
);
// Once archived, the status cannot be reversed
// 归档后,状态无法撤销
require(
_archivedStatus[tokenId][msg.sender] == false,
"MO-NFT: Token can only be archived once for an owner"
);
_archivedStatus[tokenId][msg.sender] = true;
emit ArchivedStatusUpdated(tokenId, msg.sender, archived);
}
安全注意事项
-
重入攻击:
-
缓解: 在转移 Ether 时使用 Checks-Effects-Interactions 模式(例如,将
transferValue
转移给提供者)。 -
建议: 考虑使用 OpenZeppelin 的
ReentrancyGuard
来防止重入调用。
-
-
整数溢出和下溢:
- 缓解: Solidity 0.8.x 会自动检查溢出和下溢,并在发生时抛出异常。
-
访问控制:
-
确保通过:
-
只有所有者才能调用转移函数。
-
只有提供者才能设置
transferValue
。 -
使用
require
语句来强制执行访问控制。
-
-
-
拒绝服务 (DoS):
-
考虑: 迭代所有者的函数在 gas 方面可能很昂贵,如果所有者列表很大。
-
缓解: 避免此类函数或限制所有者的数量。
-
-
数据完整性:
- 确保通过: 正确使用 Solidity 的数据类型和结构,并通过为所有状态更改操作发出事件以进行链下验证。
-
以太币处理:
-
考虑: 确保合约可以接收 Ether 以处理提供者付款。
-
缓解: 实施
receive()
函数以接受 Ether。
-
版权
在 CC0 下放弃版权和相关权利。
Citation
Please cite this document as:
Cheng Qian (@jamesavechives) <james.walstonn@gmail.com>, "ERC-7743: 多所有者非同质化代币 (MO-NFT)," Ethereum Improvement Proposals, no. 7743, July 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7743.