基本上,由于ERC721的所有权基于唯一索引或ID的所有权,因此需要将令牌创建和传输的基本原理外推以适应这种情况。
此外,最新的完整实现还包括一个safeTransferFrom()
函数,用于在传输令牌之前检查标准接口的实现。
围绕对ERC721的兴趣,我已经看到了许多关于可以保存元数据的非可替换标记的材料,但是我发现的材料深度让我寻找更多细节。 我对ERC721的兴趣始于2月份的EthDenver--您可以在这里阅读关于我们使用ERC721的项目。 此后,我对ERC721标准的实施进行了更新,因为我看到由此设计带来的更多价值。 作为本周从Open Zeppelin推出的ERC721的全面实施 ,我想为有兴趣创建自己的ERC721令牌的开发人员编写资源。 我花了一段时间才把头围上,希望如果你还没有看ERC721 EIP ,这可以帮助你更快地到达那里。 我试图在引入ERC721的同时平衡Solidity和ERC20的元素。 注意,有一个很棒的新网站可以合并所有合并的EIP,我强烈建议您检查一下。
对我而言,令牌标准可以通过以下方式进行总结和比较:
了解这些操作如何工作有助于全面了解标记标准的工作原理。 以下是OpenZeppelin ERC721Token.sol的全面实施,并融合了Solidity和其他EIP的一些额外知识。 作为第一次刺戳文档,我相信这将继续发展,并且我会尽力保持它的更新。 任何建议的赞赏。
作为迄今为止最流行的令牌标准,ERC20已经成为新令牌提案的比较标准。 他们很容易理解,至少现在我回头看看。 在所有权方面,ERC20所涉及的是将令牌的余额映射到其各自所有者的地址:
映射(地址=> uint256)余额
如果您购买了ERC20令牌,则通过您购买该令牌的合同来验证该令牌的最终所有权,因为该合同保留了每个地址( address
)有多少令牌( uint256
)的记录。 如果我们想要转移我们的ERC20令牌,那么我们的余额将通过balances
映射进行验证,以便我们不会尝试发送比我们自己更多的balances
。 可能会想到的一个问题是,如果我从未与特定的令牌合同进行交互,它如何知道我的余额为零? 上面的balances
映射最初默认为零,因此即使您之前从未触及过特定的令牌合约,如果您想检查该令牌的余额,您的余额也会被适当验证为零。
我们一遍又一遍地听到ERC721是如何不可互换的,再次,第100次意味着相同类别或合约的代币可以保持不同的价值。 一个ERC721 Cryptokittie的值不等于另一个ERC721 Cryptokittie的值,因为它们都是唯一的。 为了确保这一点,我们不能再简单地将地址映射到天平。 我们必须知道我们拥有的每个独特的标记。
出于这个原因,在ERC721标准中,所有权由映射到您的地址的一系列令牌索引或ID确定。 由于每个令牌值都是唯一的,我们不能再简单地查看令牌的余额 - 我们必须查看合约创建的每个单独的令牌。 主合同保存了在该合同中创建的所有 ERC721令牌的一个运行列表,因此每个令牌在其所有ERC721令牌的上下文中都有相应的索引,该令牌可以通过allTokens
数组从该特定合约中allTokens
。
uint256[] internal allTokens
但是,我们也需要知道我们拥有哪些代币,而不仅仅是合同的内容。 因此,除了整个合同中的标记索引数组之外,每个单独的地址都有一组标记索引或标识,作为所有权映射到其地址。 我们不只是简单地将一个地址映射到一个标记索引,因为如果一个人拥有多个标记呢? 如果我们只映射单个索引,比如说我们拥有5号令牌,并且映射到我们的地址。 然而,明天,我们购买令牌6,那么如果我们只映射了单个值,那么编号5将被我们的映射中的编号6覆盖,并且我们将不再拥有我们拥有令牌5的记录 - 因此需要阵列。
映射(地址=> uint256 [])内部拥有的Token
这个简单的区别刺激了ERC721令牌的许多附加要求。 使用ERC20令牌,我们正在检查余额,但现在,我们需要根据令牌的特定索引检查所有权。 当我们传输令牌时,重新排列这个数组需要进一步的需求。
那么当我们每次想验证某个标记索引的所有权时,我们是否遍历我们的标记数组呢? 不,有一个更简单和更安全的方法。 相反, 除了我们拥有的我们的标记索引数组之外 ,我们还将每个标记索引或标识映射到所有者。 这样,每次我们想知道谁拥有某个标记索引时,我们只需要提供标记索引来检查它映射到的地址。 (这个变量包含在ERC721BasicToken.sol中 ,继承到ERC721Token.sol。)
映射(uint256 =>地址)内部tokenOwner
为什么我们除了数组之外还要这样做? 难道我们不能只遍历我们的令牌数组来确保我们拥有特定的令牌吗? 我们先来问一下这个问题:如果我们传递标记,我们不能只添加或删除标记索引到我们的数组吗? 很不幸的是,不行。 回想一下,在Solidity中,我们是否应该删除一个数组中的元素,该元素实际上并没有被完全删除,而是被替换为零。 例如,假设我们有一个数组myarray = [2 5 47]
,它的长度为3.然而,我们调用一个函数说明delete myarray[myarray.length.sub(1)]
。 虽然我们可能期望myarray = [2 5]
,但我们实际上有以下数组myarray = [2 5 0]
,它仍然是长度为3.我们不奇迹般地拥有id 0的标记,所以这呈现出问题。 回想一下, delete
并不实际“删除”以太坊中的值,而是将它们重置为零。 当然,在某些情况下,我们希望从地址所有权中删除或删除令牌。 我们宁愿重新排列我们的阵列,而不是简单地从阵列中删除令牌。 稍后我们会看到转移(取消所有权)和刻录令牌如何发挥这些信息的作用。 出于这个原因,我们也跟踪下面的内容。 ownedTokensIndex
将每个令牌id映射到其所有者数组中的相应索引。 如下所述,我们还将token标识映射到allTokens
数组中的索引。
//从令牌ID映射到所有者令牌列表映射的索引(uint256 => uint256)internal ownedTokensIndex;
//从令牌id映射到allTokens数组映射中的位置(uint256 => uint256)internal allTokensIndex;
我们可能遇到的另一个问题是如果我们想检查我们实际拥有多少个 ERC721令牌。 此时,我们再引入一个变量来跟踪所有权。 (同样,这个变量在ERC721BasicToken.sol中,并继承到ERC721Token.sol。)
映射(地址=> uint256)内部ownedTokensCount
现在,我们映射一个数字来跟踪我们拥有的地址有多少个令牌。 当我们购买,转让或潜在地刻录令牌时,此ownedTokensCount
会更新。 为什么我们需要跟踪我们拥有多少ERC721令牌? 验证。 假设我们想将所有的ERC721令牌转移到新的地址? 或者只是检查我们拥有一定的金额?
在这一点上,我们可以看到如何引入唯一令牌的所有权为令牌的所有权增加了新的复杂性。 但是如何创建这些ERC721令牌?
回想一下,在ERC20令牌的情况下,我们映射的是令牌的平衡。 因此,为了创建ERC20令牌,我们只需要设置或增加可用的总令牌。 在ERC20设计中,我们的价值保持了我们的总可用令牌供应,总供应totalSupply_
在下面。 在某些情况下,您可能已经看到ERC20令牌合约通过在构造函数中初始化的值来设置总供应量。 回想一下构造函数运行一次以初始化合约(但不是必需的)。 构造函数必须使用与合同完全相同的名称 - 如果它不具有与合同相同的名称,则EVM将把您期望的构造函数注册为正常函数,这意味着任何人都可以在合同创建后调用它,很多安全漏洞取决于你在做什么。 构造函数代码是创建合同的事务的一部分,但它不是部署位置的合同的一部分。 构造函数可用于设置初始值,所有权等。在下面, MyToken
用于设置令牌的总totalSupply_
量的值。 随着需求增加以允许合同内ERC20令牌的数量变化,ERC20标准扩展到还包括一个mint
函数,其中期望数量的令牌被添加到总totalSupply_
量中,并且余额被相应地映射。 请注意,在下面的Transfer
是一个事件 ,而不是一个函数 - 我是唯一一个花时间去寻找一个函数,而这个函数在阅读Solidity的过程中变成一个事件吗? 无论如何,您可以从mint
功能中看到我们的余额已更新。
uint256 totalSupply_
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
//通过构造函数设置令牌供应的示例 合同MyToken { 函数MyToken(uint _setSupply) {totalSupply_ = _setSupply_} .....
//通过minting维护可变令牌供应的示例 函数mint(地址_to,uint256 _amount) onlyOwner canMint 上市 返回(布尔) {totalSupply_ = totalSupply_.add(_amount); 余额[_to] =余额[_to] .add(_amount); 薄荷(_to,_amount); 转移(地址(0),_to,_amount); 返回true; }
至于ERC721,我们了解到,由于每个单独的令牌都是唯一的,我们必须创建每个令牌。 使用ERC20,我们可以通过添加totalSupply_
来轻松创建100个批次。 但是,由于我们在ERC721标准中维护了一系列令牌,我们需要将每个令牌分别添加到该阵列中。
这里我们看到两个函数,它们查看ERC721合同的总供应addTokenTo()
和_mint()
。 我们先来addTokenTo()
。
在这里,我们从完整的实现契约中调用addTokenTo()
,然后, super.addTokenTo()
允许我们首先在基本的ERC721契约中调用addTokenTo()
函数。 基本上,在这两个函数的过程中,我们更新所有全局所有权变量。 这些函数采用两个参数_to
或令牌将被拥有的地址和_tokenId
或令牌的唯一_tokenId
由允许调用此函数的人选择,您可能会将此调用限制为合同所有者。 在这种情况下,用户可以选择任何唯一的号码ID。 首先,在ERC721BasicToken合同中,我们检查令牌ID是否已经拥有。 然后,我们设置所需令牌标识的令牌所有者,并为该个人帐户的拥有令牌数加1。 回到完整的实现合约,我们还通过将这个新的标记添加到他们的ownedTokens
数组的末尾并保存新标记的索引来更新新所有者( _to
)标记的数组。
从上面,我们可以看到addTokenTo()
更新地址给个人。 但是, allTokens
数组呢? 这是_mint
填补空白的地方。 在这里我们看到,当我们从完全实现的ERC721协议中调用_mint()
,我们又跳到了基本实现,这确保我们不会调用零地址并调用addTokenTo()
,尽管它很混乱,将实际回拨到我们的完整实施合同,以启动addTokenTo()
调用。 (同样, Transfer()
是一个事件,而不是一个函数。)在基本合约中的_mint()
函数完成之后,回到我们的完整实现中,我们将_tokenId
添加到我们的allTokensIndex
的映射以及我们的allTokens
数组。
从上面可以看出,尽管你可以自己调用 addTokenTo()
,但是你需要做什么才能保证全部实现ERC721合同中的所有信息是使用 _mint()
来创建新的令牌。
但是ERC721可以保存的元数据呢? 我们已经创建了令牌和令牌ID,但是他们还没有保存任何数据。 打开Zeppelin给了我们一个例子,它是如何将映射令牌id映射到URI数据的字符串。
//可选映射令牌URI 映射(uint256 => string)内部tokenURIs;
为了设置令牌的URI数据,还包括以下_setTokenURI()
函数。 在这里使用您通过_mint()
创建的令牌Id和所需的URI信息,可以设置映射到tokenURIs
令牌ID的tokenURIs
。 注意这个函数中的要求,我们在分配数据之前确定一个令牌Id存在(意味着某人拥有它)。
尽管更复杂和更耗费精力,但我发现使用结构来存储数据的能力,而不是映射到更有趣的索引 - 至少,创建一个带有少量变量的不可互换的标记仍然比相反,每个“资产”创建一个智能合约。 无论如何,如果你想知道如何包含不同的数据,这些元素就是你想要改变的。
和以前一样,我们首先回顾一下ERC20标准中的转移和补贴是如何发生的。 我们可以使用transfer()
函数直接传输ERC20令牌,在该函数中我们指定了要发送到的地址以及多少,该数据会根据我们的余额进行检查,然后在主ERC20合同中进行更新。
函数传递(地址_to,uint256 _value)public returns(bool){require(_to!= address(0)); require(_value <= balances [msg.sender]); 余额[msg.sender] =余额[msg.sender] .sub(_value); 余额[_to] =余额[_to] .add(_value); 转移(msg.sender,_to,_value); 返回true; }
但是,我们的津贴是什么意思? 当我们希望另一份合同或地址能够转移我们的ERC20令牌时,我们需要允许使用ERC20合同地址为我们做到这一点 - 在分布式应用程序中的许多情况下都会出现这种需求 - 托管,游戏,拍卖等。因此,我们需要一种方法来批准其他地址来使用我们的令牌。 然后,另一个传递函数要求合同检查允许谁允许花费他们的津贴。 我将从如何设置津贴开始,然后展示如何发挥转移。
在ERC20标准中,我们有一个全局变量, allowed
所有者地址映射到已批准的支出地址,然后映射一定数量的标记。 为了设置这个变量,有一个approve()
函数,其中一个人能够将批准映射到他们想要的_spender
和_value
。 请注意,在这里,我们没有检查发件人拥有的实际数量的令牌 - 这些数据稍后会在传输过程中进行。 再一次, Approval
是一个不是功能的事件。
//全局变量 映射(地址=>映射(地址=> uint256))内部允许
//允许另一个地址花费你的代币 功能批准(地址_spender,uint256 _value) 上市 返回(布尔) {允许[msg.sender] [_ spender] = _value; 审批(msg.sender,_spender,_value); 返回true; }
现在,一旦我们批准另一个地址来转移我们的令牌,我们的令牌如何实际转移? 我们批准的transferFrom()
将使用下面的transferFrom()
函数,在这个函数中他们将指定_from
或原始拥有者的地址,接收者的地址_to
和金额_value
。 在这里,我们检查原始拥有者实际上是否拥有期望转移的金额require(_value ≤ balances[_from])
,然后我们检查是否允许msg.sender
通过allowed
变量转移余额,最终我们更新所有我们的映射balances
以及我们allowed
金额。 Tranfer
再次是一个事件。 请注意,还有两个附加功能可以允许增加( increaseApproval()
批准increaseApproval()
)和减少( decreaseApproval()
批准decreaseApproval()
)批准的分摊者津贴。
因此,我们需要再次认为,在ERC721的情况下,我们需要批准和转让令牌ID,而不是批准和转移余额。 ERC721标准提供机会批准通过id传递令牌的地址,或者我们可以批准地址来传输所有的令牌。 要批准通过ID传输,我们使用approve()
函数如下。 在这里,全局变量tokenApprovals
将令牌索引或标识映射到已批准传输的地址。 在approve()
函数中,我们首先检查所有权或msg.sender
isApprovedForAll()
。 在下文中,您可以看到,您可以使用setApprovalForAll()
函数来批准一个地址来传输和处理由特定地址拥有的所有令牌,因为我们有一个全局变量operatorApprovals
,其中所有者的地址映射到批准的支票地址,然后映射到布尔。 默认设置为0或false,但通过使用setApprovalForAll()
我们可以将此映射设置为true,并允许地址处理所有ERC721的拥有。 请注意,如果一个分配器被批准用于所有的令牌,那么他们也可以分配额外的地址支出能力。 接下来,我们使用getApproved()
来检查我们没有设置address(0)
许可。 最后,我们的tokenApprovals
映射完成到所需的地址。 和ERC20一样, Approval
是事件。
现在,我们来到我们如何实际转移ERC721的。 全面实施实际上提供了两种转移方式。 第一种方法是不鼓励的,但让我们回过头来理解。 在transferFrom()
,发送者和接收者地址与_tokenId
一起指定传输,我们使用修饰符canTransfer()
来确保msg.sender
被批准传输令牌或拥有它。 在检查发件人和收件人地址有效后, clearApproval()
函数用于从原始令牌的所有者中移除批准转让,以便以前批准的支票人可能不会继续转移令牌。 接下来,在ERC721完整实现合约中调用removeTokenFrom()
,类似于使用super的removeTokenFrom()
函数在ERC721基本实现中调用removeTokenFrom()
函数。 您可以看到从拥有的ownedTokensCount
映射( tokenOwner
映射)中移除了该令牌,还有一个tokenOwner
是我们将拥有者ownedTokens
数组中的最后一个令牌移动到正在传输的令牌的索引,并将数组缩短一个见22-30行)。 最后,我们使用addTokenTo()
函数将此令牌索引添加到其新所有者。 Transfer
是一个事件。
现在,有一个问题需要问,我们如何确保我们将ERC721发送给可处理额外转帐的合同? 我们知道如果需要,外部拥有账户(EOA)可以使用我们的ERC721完整实施合同交易代币; 然而,如果我们将令牌发送给一个没有相应功能的合同,通过我们原始的ERC721合同进行交易和转让令牌,那么由于无法将令牌拿出来,导致令牌有效丢失。 这种情绪反映了通过ERC223提案所揭示的许多关注, ERC223提案是对ERC20提出的修改建议,以防止这些错误转让。
为了避免问题和标准化,ERC721完整实施标准引入了safeTransferFrom()
函数。 在深入研究这些工作之前,让我们看看一些额外的需求,在这些需求中我们有一个实现ERC721Receiver.sol接口的ERC721Holder.sol合约。 ERC721Holder.sol合同将成为您希望持有ERC721令牌的钱包,拍卖或经纪合同的一部分。 这个标准的原因可以追溯到EIP165 ,其目标是创建“一种标准的方法来发布和检测智能合约实现的接口。”我们如何检测接口? 下面我们将看到一个“魔术值” ERC721_RECEIVED
,它是onERC721Received()
函数的函数签名。 函数签名是规范签名字符串散列的前四个字节。 在这种情况下,它按bytes4(keccak256(“onERC721Received(address, uint256, bytes)”))
,如下所述。 什么是函数签名用于? 在字节码中找到包含被调用函数调用代码的位置。 合同中的每个功能都会拥有自己的签名,当您打电话给您的合同时,EVM会使用一系列切换案例来查找与您的呼叫相匹配的功能签名并相应地执行您的代码。 因此,在我们的ERCHolder合同中,我们看到onERCReceived()
函数签名将匹配ERC721Receiver
接口中的ERC721_RECEIVED
变量。
您的ERC721Holder
合同不是处理ERC721令牌的完整合同。 该模板旨在为您提供标准化接口,以验证是否使用了ERC721Receiver
标准接口。 您需要扩展或继承ERC721Holder
合同,在您的钱包或拍卖合同中包含处理ERC721的功能。 即使托管代币,您也需要添加功能,以便持有人合同可以根据需要拨打电话将合约转出合同。
现在,回到我们原来的ERC721合同, safeTransferFrom()
工作方式如下 - 您可以使用选项1进行传输,其中safeTransferFrom()
函数不包含附加数据,或者您可以使用选项2将数据包含在bytes _data
的形式。 与之前一样, transferFrom()
函数用于从_from
地址中删除标记所有权并将标记所有权添加到_to
地址。 但是,我们有一个额外的要求,即运行checkAndCallSafeTransfer()
函数。 首先,我们通过使用AddressUtils.sol库检查_to
地址是否是一个实际的合同 - 我在下面包含了函数isContract()
,以便您快速了解它正在做什么。 如前所述,目前研究和开发允许以太坊的外部拥有账户(EOAs)也维护他们自己的代码,所以无论何时何时出现,都需要注意这样的支票。 在验证_to
是合同地址后,我们检查调用onERC721Received()
函数是否会返回我们期望从标准接口获得的相同函数签名。 如果没有返回正确的值,那么transferFrom()
函数会回滚,因为我们已经确定_to
没有实现预期的接口。
噢,我们有它。 传输ERC721令牌。 现在,刻录令牌应该看起来很容易。
至于ERC20,由于我们只操纵单一映射余额,因此我们只需要烧毁或销毁特定地址的令牌,这可以是用户或合约。 在下面的burn()
,我们指定了我们想通过_value
变量刻录的令牌的数量。 要烧的地址是msg.sender
,所以我们更新它们各自的余额,然后我们也减少令牌的总totalSupply_
量。 这里的Burn
和Transfer
是事件。
对于ERC721令牌,我们需要确保特定令牌ID或索引已被删除。 与addTokenTo()
和_mint()
函数非常相似,我们的_burn()
函数使用super在我们的基本ERC721实现中调用函数。 首先,我们clearApproval()
,然后通过removeTokenFrom()
从所有权中删除令牌,并使用Transfer
事件在前端警告此更改。 接下来,我们通过删除映射到特定标记索引的内容来消除与该标记关联的元数据。 最后,就像从所有权中删除令牌一样,我们重新排列我们的allTokens
数组,以便用数组中的最后一个令牌替换_tokenId
索引。
如果你完成了,感谢阅读! 我想最大的挑战将是如何适应这些标准,如何将ERC721与所需的元数据一起铸造,以及如何确保基于独特价值交换的转移。 目前已经有很多例子 - 当然,着名的Cryptokitties , Cryptogs (来自我的EthDenver团队), Cryptocelebrities , Decentraland ,如果您访问OpenSea,您可以找到大量数字资产和收藏品。 我可以想象这个标准有更多的用例 - 希望这篇文章有助于让你了解......!
https://medium.com/blockchannel/walking-through-the-erc721-full-implementation-72ad72735f3c