EIP-721的openzeppelin实现
这段时间总是与NFT打交道,大部分NFT都采用了EIP721标准,且均采用了Openzepplin的EIP721实现。前段时间详细看过Openzepplin的相关实现,但是偷懒了,没有整理成文档,导致后面的记忆总是不深刻,理解也不深刻。此次正好将其实现全部整理一下。
# EIP-721的openzeppelin实现 这段时间总是与NFT打交道,大部分NFT都采用了EIP721标准,且均采用了Openzepplin的EIP721实现。前段时间详细看过Openzepplin的相关实现,但是偷懒了,没有整理成文档,导致后面的记忆总是不深刻,理解也不深刻。此次正好将其实现全部整理一下。 ![image.png](https://img.learnblockchain.cn/attachments/2021/09/FgldYnKC614d6a4b67ca4.png) ## EIP-721标准 首先简单介绍下EIP-721标准,可以参考[EIP-721: Non-Fungible Token Standard (ethereum.org)](https://eips.ethereum.org/EIPS/eip-721) ### EIP-721接口 在EIP-721标准中,定义了如下的标准函数和标准事件,任何NFT合约都必须实现EIP-721标准中定义的函数和事件 ```js event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId); event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); ``` 从EIP-721标准中,定义的事件来看,一个NFT的标准事件其实只有三种,Transfer,Approval和ApprovalForAll。其中Transfer事件与EIP-20中定义的Transfer一致,Approval指的是一个NFT的所有者批准使用者使用指定的一个tokenId的NFT,ApprovalForAll指的是NFT的所有者批准操作员使用其所有的NFT。 ```js function balanceOf(address _owner) external view returns (uint256); function ownerOf(uint256 _tokenId) external view returns (address); function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes calldata data) external payable; function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable; function transferFrom(address _from, address _to, uint256 _tokenId) external payable; function approve(address _approved, uint256 _tokenId) external payable; function setApprovalForAll(address _operator, bool _approved) external; function getApproved(uint256 _tokenId) external view returns (address); function isApprovedForAll(address _owner, address _operator) external view returns(bool); ``` 从上述的方法名来看,EIP-721定义的方法中`balanceOf,ownerOf,transferFrom`这些是与ERC20中的函数签名一致。但是需要明确如下几点: 1. `transferFrom`的逻辑与ERC20的`transferFrom`的逻辑不同。在ERC-20中,当调用`transferFrom`时,需要事先`approve`,而ERC-721中,作为`owner`或者`operator`或者已经获批的地址调用时,不需要`approve`。 2. 针对`transferFrom`方法,其必须在方法内部验证to地址不能是`address(0)`, 且需要验证`tokenId`对应的NFT事先存在 3. EIP-721中新增了`safeTransferFrom`方法,主要目的是在`transfer`结束后,判断to地址是否是一个合约地址,如果to地址是一个合约地址,则需要调用to地址上的`onERC721Received`方法,并返回特定的值,即:`bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`,这样就可以避免将一个NFT转移到一个不支持的地址中锁死。 4. 当调用`safeTransferFrom`方法时,需要满足如下条件: | 参数 | 要求 | | ---------- | ------------------------------------------------------------ | | msg.sender | 要求msg.sender 必须为owner或者是获批的operator或者是获批的approved地址 | | from | 要求from字段必须填写owner地址,不能是其他地址 | | to | 要求to字段不能是address(0) | | tokenId | 要求该tokenId必须是有效的NFT,即存在 | 5. 针对`setApprovalForAll`方法,一个owner可以给多个operator进行全量授权,而不是仅限一个operator。 ### EIP-165实现 在实现EIP-721的合约中,其必须也要实现EIP-165标准,即通用接口注册标准。用于接口发现和验证。其思路是合约实现EIP-165中定义的`supportsInterface(bytes4 interfaceId)`方法,该方法中将一个合约中所有的external函数签名进行亦或求值得到一个bytes4. 然后验证时遵循如下思路进行验证: 1. 调用目标合约的supportInterface方法,并传入参数:`bytes4(keccak256("supportsInterface(bytes4)"))`即`0x01ffc9a7`, 此时应该返回true 2. 调用目标合约的supportsInterface方法,并传入参数:`0xffffffff`,此时应该返回false 3. 调用目标合约的supportsInterface方法,并传入参数:this.interfaceId, 此时应该返回true ```js this.balanceOf.selector ^ this.ownerOf.selector ^ this.safeTransferFrom ^ this.transferFrom ^ this.approve ^ this.setApprovalForAll ^ this.getApproved ^ this.isApprovedForAll = this.interfaceId ``` > A bytes4 value containing the EIP-165 interface identifier of the given interface I. This identifier is defined as the XOR of all function selectors defined within the interface itself - excluding all inherited functions. ### Metadata元数据 在目前的NFT合约实现中,基本所有的NFT都实现了MetaData这一部分的接口定义。其主要作用是定义NFT的名称,符号和tokenURI. 在EIP-721中,tokenURI的定义是要符合RFC-3986标准,但事实上目前的NFT合约中基本上都是一个自定义的状态。可能是项目方的一个网址,或者是一个IPFS文件,也可能是一串字符串。 ```js function name() external view returns(string); function symbol() external view returns(string); function tokenURI(uint256 _tokenId) view returns(string); ``` ### NFT枚举 Enumerable的目的是给用户提供一个快速查询NFT的方法。接口设计上是让用户可以根据用户自己的索引查询她所拥有的NFT对应的tokenId,另一个是根据索引查询合约中的NFT的tokenId, 然后是总的供给量查询,很多的NFT合约的总供给量反应的是现在所有的NFT的数量。简单来讲就是提供两个索引,一个索引用来索引整个合约中的NFT,另一个索引是用来索引用户所拥有的NFT ```js function totalSupply() external view returns (uint256); function tokenByIndex(uint256 _tokenId) external view returns(uint256); function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns(uint256); ``` ### EIP-721接受合约 作为EIP-721的要求,如果一个合约要接受EIP-721,其必须要实现`onERC721Received`方法,当用户调用`safeTransferFrom`时,会在转账结束时,调用to地址的`onERC721Received`方法,此时该方法的返回值应该为`bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))` ```js function onERC721Received(address _operator,address _from,uint256 _tokenId,bytes calldata _data) external returns(bytes4); ``` ## openzeppelin的EIP-721实现 由于目前见到的所有的NFT合约其都是基于Openzepplin的EIP-721实现,故充分了解Openzepplin的EIP-721实现是非常有必要的,也是非常有帮助的。 在openzeppelin的实现中,其实现EIP-721的主要在ERC721.sol文件中,实现枚举部分在ERC721Enumberable.sol文件中。 ### ERC721.sol ERC721文件中,需要实现的接口有EIP-721和metadata两部分,含EIP-165部分。 0. #### 首先是需要设计全局变量: ```js name() => string private name; symbol() => string private symbol; balanceOf() => map(address=>uint256) private _balances; ownerOf() => map(uint256=>address) private _owners; getApproved() => map(uint256=>address) private _tokenApproves; isApprovedForAll() => map(address=>map(address=>bool)) private _operatorApproves; ``` 然后是依次实现EIP-721中定义的接口方法: 1. #### EIP-165中定义的`supportsInterface`: ```js function supportsInterface(bytes4 interfaceId) public view returns (bool) { bytes4 EIP165Interface = bytes4(keccak256("supportsInterface(bytes4)")); bytes4 dummyInterface = bytes4(0xffffffff); if (interfaceId == dummyInterface) { return false; } if (interfaceId == EIP165Interface) { return true; } if (interfaceId == type(IERC721).interfaceId) { return true; } if (interfaceId == type(IERC721Metadata).interfaceId) { return true; } return false; } ``` 2. #### 实现EIP-721中定义的get方法: ```js function balanceOf(address _owner) public view returns (uint256) { //要求_owner不能为address(0) require(_owner != address(0), "ERC721/balanceOf owner can not be address(0)"); return _balances[_owner]; } function ownerOf(uint256 _tokenId) public view returns (address) { //要求任何一个tokenId的owner都不能是address(0) address owner = _owners[_tokenId]; require(owner != address(0), "ERC721/ownerOf owner can not be address(0)"); return owner; } function getApproved(uint256 _tokenId) public view returns (address) { //要求_tokenId必须是有效的tokenId //怎么判断一个tokenId是否是有效的tokenId呢?添加一个辅助函数_exists,即判断该tokenId的owner不应该是address(0) //address(0)能否是一个被授权的地址呢?是可以的,意味着该TokenId不对其他任何地址授权 require(_exists(_tokenId), "ERC721/getApproved not a valid tokenId"); return _tokenApproved[_tokenId]; } function isApprovedForAll(address _owner, address _operator) public view returns (bool) { return _operatorApproved[_owner][_operator]; } function _exists(uint256 _tokenId) internal view returns (bool) { return _owners[_tokenId] != address(0); } ``` 3. #### 实现EIP-721 Metadata中定义的get方法: ```js function name() public view returns (string) { return name; } function symbol() public view returns (string) { return symbol; } function tokenURI(uint256 _tokenId) public view returns (string) { //tokenURI指向一个特定的JSON文件,也可以是一个字符串,其是由baseURI和tokenId进行组合得到 //要求tokenId是一个有效的tokenId require(_exists(_tokenId), "ERC721/tokenURI not a valid tokenID"); //首先检查是否定义了baseURI,如果定义了baseURI则将其与tokenID进行组合得到tokenURI,如果没有定义baseURI,则直接返回空 bytes memory baseURI = _baseURI(); if (bytes(baseURI).length > 0) { return string(abi.encodePacked(baseURI,_tokenId.toString())); } return ""; } function _baseURI() internal view returns (string) { return ""; } ``` 4. #### 实现EIP-721中定义的transfer方法: ```js function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes calldata _data) external payable { //要求msg.sender必须是owner或者授权的operator或者是授权的地址 //要求from必须是owner的地址,不能是operator的地址或者其他地址 //要求to必须不能是address(0) //要求tokenId必须是有效的tokenId //要求当transfer结束时,检查to地址是否是合约地址,如果是合约地址则需要调用onERC721Received方法,返回特定的值 address owner = ownerOf(_tokenId); address approvedAddress = getApproved(_tokenId); require(msg.sender == owner || msg.sender == approvedAddress || isApprovedForAll(owner,msg.sender),"EIP721/safeTransferFrom msg.sender not correct"); require(from == owner, "EIP721/safeTransferFrom from not correct"); require(to != address(0), "EIP721/safeTransferFrom to not correct"); require(_exists(_tokenId), "EIP721/safeTransferFrom tokenId not exists"); _transfer(_from,_to,_tokenId); require(_checkOnERC721Received(_from,_to,_tokenId,_data)); } function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable { safeTransferFrom(_from,_to,_tokenId,""); } function transferFrom(address _from, address _to, uint256 _tokenId) external payable { //要求msg.sender必须是owner或者授权的operator或者是授权的地址 //要求from必须是owner的地址,不能是operator的地址或者其他地址 //要求to必须不能是address(0) //要求tokenId必须是有效的tokenId _transfer(_from,_to,_tokenId); } function _transfer(address _from, address _to, uint256 _tokenId) internal { //要求from必须是owner的地址,不能是operator的地址或者其他地址 //要求to必须不能是address(0) require(from == ownerOf(_tokenId), "EIP721/safeTransferFrom from not correct"); require(to != address(0), "EIP721/safeTransferFrom to not correct"); //更改tokenId对应的所有权,取消相应tokenId的授权地址的权限,但不能取消经销商的权限 _balances[_from] = _balances[_from].sub(1); _balances[_to] = _balances[_to].add(1); _owners[_tokenId] = _to; _tokenApproves[_tokenId] = address(0); } function _checkOnERC721Received(address _from, address _to, uint256 _tokenId, bytes calldata _data) internal returns (bool) { //作用是判断地址to是否是一个合约地址,如果不是一个合约地址则直接返回true,如果是一个合约地址,则需要调用地址to的onERC721Received方法来判断返回值是否是一个特定的返回值 //是EOA,必须同我直接交互,不能通过proxy bytes4 funcSelector = bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")); if (msg.sender == tx.origin) { return true; } //是合约地址 //这样写会把to地址的报错给吞掉,没有把报错信息抛出来 if (_to.isContract()) { bytes4 retVal = IERC721Received(_to).onERC721Received(msg.sender,_from,_tokenId,_data); return funcSelector == retVal; } //是合约地址 //这样写可以把to地址的报错抛出来 if (_to.isContract()) { (bool success, bytes memory res) = _to.call(abi.encodeWithSelector(funcSelector,_from,_to,_tokenId,_data)); bytes4 retVal; uint256 retSize; assembly { retSize := mload(res) retVal := mload(add(res,0x20)) } if (success) { require(retSize == 0x04); return funcSelector == retVal; } else { if (retSize == 0) { revert("ERC721: transfer to non ERC721Receiver Implementer"); } else { assembly { revert(add(0x20, res),mload(res)) } } } } return false; } ``` 5. #### 实现EIP-721中定义的set方法 ```js function approve(address _approved, uint256 _tokenId) external payable { //要求msg.sender 必须是owner或者是授权的经销商 //要求tokenId必须是存在的tokenId //可以给address(0)授权,意味着该tokenId没有授权的地址 //不能给自己授权 address owner = ownerOf(_tokenId); require(msg.sender == owner || isApprovedForAll[owner][msg.sender]); require(_exists[_tokenId]); _tokenApproves[_tokenId] = _approved; } function setApprovalForAll(address _operator,bool _approved) external{ //要求经销商不能是自己 require(msg.sender != _operator,"ERC721/setApprovalForAll msg.sender can not be the operator itself"); _operatorApproves[owner][_operator] = _approved; } ``` 关键点:自己不能是自己的经销商! > 原因在于如果alice是alice自己的经销商,意味着`_operatorApproves[alice][alice] = true`,则当alice作为owner给bob转一个tokenId时,由于在`_transfer`函数的逻辑设计中,只清楚了该tokenId对应的授权地址的授权,即`_tokenApproves[_tokenId] = address(0)`, 并没有清除相应的经销商的授权。同时,清除经销商的权限也是不合理的。其实此时作为经销商的alice还是无法再去transfer一次tokenId 6. #### 其他的辅助方法:mint,burn 在当前的NFT合约中,大量使用了mint方法,然而此方法并不是EIP-721中规定的方法,但是其已经成为事实标准。简单来讲mint方法是新增一个tokenId,该tokenId不能是已经存在的,然后把该tokenId添加到对应的owner中。burn方法是删除该tokenId即可。mint和burn在openzeppelin的实现中都遵循了safeTransfeFrom的思路。mint方法并未提供一个公开的方法,而是一个`_safeMint()`内部方法,需要项目方自己去结合逻辑实现一个mint方法。 ```js function _safeMint(address _to, uint256 _tokenId, bytes memory _data) internal { //要求tokenId必须不能是一个已经存在tokenId //要求地址to如果是合约地址,则需要实现onERC721Received方法 require(!_exists[_tokenId],"ERC721/_safeMint tokenId already exists"); _mint(_to,_tokenId); require(_checkOnERC721Received(address(0),_to,_tokenId,_data),"ERC721/_safeMint not a valid receiver"); } function _safeMint(address _to, uint256 _tokenId) internal { _safeMint(_to,_tokenId,""); } function _mint(address _to, uint256 _tokenId) internal { //要求_tokenId必须不能是一个已经存在的tokenId //要求地址_to必须不能是address(0) require(!_exists(_tokenId), "ERC721/_mint tokenId already exists"); require(_to != address(0),"ERC721/_mint _to can not be address(0)"); _owners[_tokenId] = _to; _balances[_to] += 1; emit Transfer(address(0), _to, _tokenId); } function _burn(uint256 _tokenId) internal { //要求tokenId必须存在,但是不能真的把tokenId转给地址0,只是删除owners中对应的tokenId require(_exists(_tokenId),""); //要求清除该tokenId对应的授权地址,但不能清除经销商的授权 _tokenApproves[_tokenId] = address(0); _balances[msg.sender] -= 1; delete _owners[_tokenId]; emit Transfer(msg.sender, address(0), _tokenId); } ``` ### ERC721Enumerable.sol ERC721的枚举部分,该部分与ERC721主体部分分开,其实现的功能主要是提供totalSupply以及提供了两个索引,一个索引是`tokenByIndex`全局索引,另一个索引是`tokenOfOwnerByIndex`,即用户的索引。 这里需要思考如何实现这两个索引。目前在ERC721.sol文件中,提供了`_owners,_balances,_tokenApproves,_operatorApproves`四个map,现在需要提供两个索引,这两个索引应该如何与这些已有的map结合起来? ```js //要得到最新的总供应量,即返回目前被NFT合约追踪下来的总的有效NFT数量 totalSupply => uint256[] private _allTokens; => totalSupply = _allTokens.length; //根据全局索引来查找对应的tokenId tokenByIndex => uint256[] private _allTokens; => return _allTokens[index]; //根据特定的owner的索引查找其拥有的所有tokenId //tokenOfOwnerByIndex => mapping(address=>uint256[]) private _ownedTokens; => return _ownedTokens[owner][index]; tokenOfOwnerByIndex => mapping(address=>mapping(uint256=>uint256)) private _ownedTokens; => return _ownedTokens[owner][index]; //在索引用户的tokenId时,需要保证index值小于用户的balance ``` 结合目前的需求,因为要delete 列表`_allTokens`中的某一个`tokenId`,故还需要额外维护一个`tokenId=>index`的逆向map。 ```js mapping(uint256=>uint256) private _allTokensIndex; ``` 因为要delete列表`_ownedTokens[owner]`中的某一个tokenId,故还需要额外维护一个tokenId=>index的逆向map: ```js mapping(uint256=>uint256) private _ownedTokensIndex; ``` 1. 枚举中的get方法: ```js function totalSupply() public view returns (uint256) { return _allTokens.length; } function tokenByIndex(uint256 _index) public view returns (uint256) { require(_index < totalSupply(), "ERC721Enumerable/tokenByIndex index overflow"); return _allTokens[_index]; } function tokenOfOwnerByIndex(address _owner,uint256 _index) public view returns (uint256) { //要求index不能大于等于owner的余额 //要求owner不能是地址0 require(_index < balanceOf(_owner), "ERC721Enumberable/tokenOfOwnerByIndex index overflow balance"); require(_owner != address(0)); return _ownedTokens[_owner][_index]; } ``` 2. 枚举中的set方法 这里需要思考枚举中的set方法应该在什么时候调用:其应该在每一次transfer之前都需要调用一次,因为transfer时肯定就发生了状态的变化。这里就需要用到ERC721中预先留下来的勾子函数: ```js function _beforeTokenTransfer(address _from,address _to,uint256 _tokenId) internal {} ``` 在这个函数中,需要做如下的逻辑判断: | from | to | 含义 | | -------------- | -------------- | ------------------------------------------- | | 不为address(0) | 不为address(0) | 普通的transfer,此时的tokenId应该从from->to | | 为address(0) | 不为address(0) | 此时是mint操作 | | 不为address(0) | 为address(0) | 此时是burn操作 | 根据上述表格可以看到有三种类型的操作,transfer,mint和burn,需要针对三种不同的类型来分别更新mapping中的值 #### 普通的transfer操作 针对普通的transfer操作: ```js _allTokens列表应该保持不变; _ownedTokens列表需要更新 => _ownedTokens[from]相应减去该tokenId,_ownedTokens[to]应增加相应tokenId _ownedTokensIndex需要更新 => _ownedTokensIndex[_tokenId] = newIndex; _allTokensIndex 不需要更新 ``` 在openzeppelin的实现中,即为: ```js function _removeTokenFromOwnerEnumeration(address from, uint256 tokenId) private {} function _addTokenToOwnerEnumeration(address to,uint256 tokenId) internal {} ``` #### mint操作 针对mint操作: ```js _allTokens列表需要新增 => _allTokens.push(_tokenId); _ownedTokens列表需要新增 => _ownedTokens[to][balanceOf(to)]=_tokenId; _ownedTokensIndex列表需要新增 => _ownedTokensIndex[_tokenId] = balanceOf(to); _allTokensIndex 需要新增 => _allTokensIndex[_tokenId] = totalSupply(); ``` 在openzeppelin的实现中,即为: ```js function _addTokenToAllTokensEnumeration(uint256 tokenId) private { //注意先后顺序 _allTokensIndex[tokenId] = _allTokens.length; _allTokens.push(tokenId); } function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private { uint256 length = balanceOf(to); _ownedTokens[to][length] = tokenId; _ownedTokensIndex[tokenId] = length; } ``` #### burn操作 针对burn操作: ```js _allTokens列表需要删除 => delete _allTokens[_allTokensIndex[tokenId]]; _allTokensIndex 需要更新 => delete _allTokensIndex[tokenId]; //问题:如果删除后,该map保存的其他index应该都不准确了,应该如何设计? _ownedTokens列表需要删除 => delete _ownedTokens[from][_ownedTokensIndex[tokenId]]; _ownedTokensIndex 需要更新 => delete _ownedTokensIndex[tokenId]; ``` 在openzeppelin的实现中,即为: ```js function _removeTokenFromOwnerEnumeration(address from, uint256 tokenId) private { //为了解决上面提出的问题,这里删除时,预先将要删除的tokenId放置在最后一个槽位,然后只删除最后一个槽位 //swap and pop uint256 lastTokenIndex = balanceOf(from) - 1; uint256 tokenIndex = _ownedTokensIndex[tokenId]; //swap 如果不是最后一个槽位则 swap if (tokenIndex != lastTokenIndex) { uint256 lastTokenId = _ownedTokens[from][lastTokenIndex]; //swap _ownedTokens[from][tokenIndex] = lastTokenId; _ownedTokensIndex[lastTokenId] = tokenIndex; } //pop delete _ownedTokens[from][lastTokenIndex]; delete _ownedTokensIndex[tokenId]; } function _removeTokenFromAllTokensEnumeration(uint256 tokenId) private { //swap and pop uint256 lastTokenIndex = _allTokens.length - 1; uint256 tokenIdex = _allTokensIndex[tokenId]; //swap 为节约gas费用,不考虑是否是最后一个槽位 uint256 lastTokenId = _allTokens[lastTokenIndex]; _allTokens[tokenIndex] = lastTokenId; _allTokensIndex[lastTokenId] = tokenIndex; //pop delete _allTokensIndex[tokenId]; _allTokens.pop(); } ```
EIP-721的openzeppelin实现
这段时间总是与NFT打交道,大部分NFT都采用了EIP721标准,且均采用了Openzepplin的EIP721实现。前段时间详细看过Openzepplin的相关实现,但是偷懒了,没有整理成文档,导致后面的记忆总是不深刻,理解也不深刻。此次正好将其实现全部整理一下。
EIP-721标准
首先简单介绍下EIP-721标准,可以参考EIP-721: Non-Fungible Token Standard (ethereum.org)
EIP-721接口
在EIP-721标准中,定义了如下的标准函数和标准事件,任何NFT合约都必须实现EIP-721标准中定义的函数和事件
event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
从EIP-721标准中,定义的事件来看,一个NFT的标准事件其实只有三种,Transfer,Approval和ApprovalForAll。其中Transfer事件与EIP-20中定义的Transfer一致,Approval指的是一个NFT的所有者批准使用者使用指定的一个tokenId的NFT,ApprovalForAll指的是NFT的所有者批准操作员使用其所有的NFT。
function balanceOf(address _owner) external view returns (uint256);
function ownerOf(uint256 _tokenId) external view returns (address);
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes calldata data) external payable;
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
function approve(address _approved, uint256 _tokenId) external payable;
function setApprovalForAll(address _operator, bool _approved) external;
function getApproved(uint256 _tokenId) external view returns (address);
function isApprovedForAll(address _owner, address _operator) external view returns(bool);
从上述的方法名来看,EIP-721定义的方法中balanceOf,ownerOf,transferFrom
这些是与ERC20中的函数签名一致。但是需要明确如下几点:
transferFrom
的逻辑与ERC20的transferFrom
的逻辑不同。在ERC-20中,当调用transferFrom
时,需要事先approve
,而ERC-721中,作为owner
或者operator
或者已经获批的地址调用时,不需要approve
。- 针对
transferFrom
方法,其必须在方法内部验证to地址不能是address(0)
, 且需要验证tokenId
对应的NFT事先存在 - EIP-721中新增了
safeTransferFrom
方法,主要目的是在transfer
结束后,判断to地址是否是一个合约地址,如果to地址是一个合约地址,则需要调用to地址上的onERC721Received
方法,并返回特定的值,即:bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))
,这样就可以避免将一个NFT转移到一个不支持的地址中锁死。 - 当调用
safeTransferFrom
方法时,需要满足如下条件:
参数 | 要求 |
---|---|
msg.sender | 要求msg.sender 必须为owner或者是获批的operator或者是获批的approved地址 |
from | 要求from字段必须填写owner地址,不能是其他地址 |
to | 要求to字段不能是address(0) |
tokenId | 要求该tokenId必须是有效的NFT,即存在 |
- 针对
setApprovalForAll
方法,一个owner可以给多个operator进行全量授权,而不是仅限一个operator。
EIP-165实现
在实现EIP-721的合约中,其必须也要实现EIP-165标准,即通用接口注册标准。用于接口发现和验证。其思路是合约实现EIP-165中定义的supportsInterface(bytes4 interfaceId)
方法,该方法中将一个合约中所有的external函数签名进行亦或求值得到一个bytes4. 然后验证时遵循如下思路进行验证:
- 调用目标合约的supportInterface方法,并传入参数:
bytes4(keccak256("supportsInterface(bytes4)"))
即0x01ffc9a7
, 此时应该返回true - 调用目标合约的supportsInterface方法,并传入参数:
0xffffffff
,此时应该返回false - 调用目标合约的supportsInterface方法,并传入参数:this.interfaceId, 此时应该返回true
this.balanceOf.selector ^ this.ownerOf.selector ^ this.safeTransferFrom ^ this.transferFrom ^ this.approve ^ this.setApprovalForAll ^ this.getApproved ^ this.isApprovedForAll = this.interfaceId
A bytes4 value containing the EIP-165 interface identifier of the given interface I. This identifier is defined as the XOR of all function selectors defined within the interface itself - excluding all inherited functions.
Metadata元数据
在目前的NFT合约实现中,基本所有的NFT都实现了MetaData这一部分的接口定义。其主要作用是定义NFT的名称,符号和tokenURI. 在EIP-721中,tokenURI的定义是要符合RFC-3986标准,但事实上目前的NFT合约中基本上都是一个自定义的状态。可能是项目方的一个网址,或者是一个IPFS文件,也可能是一串字符串。
function name() external view returns(string);
function symbol() external view returns(string);
function tokenURI(uint256 _tokenId) view returns(string);
NFT枚举
Enumerable的目的是给用户提供一个快速查询NFT的方法。接口设计上是让用户可以根据用户自己的索引查询她所拥有的NFT对应的tokenId,另一个是根据索引查询合约中的NFT的tokenId, 然后是总的供给量查询,很多的NFT合约的总供给量反应的是现在所有的NFT的数量。简单来讲就是提供两个索引,一个索引用来索引整个合约中的NFT,另一个索引是用来索引用户所拥有的NFT
function totalSupply() external view returns (uint256);
function tokenByIndex(uint256 _tokenId) external view returns(uint256);
function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns(uint256);
EIP-721接受合约
作为EIP-721的要求,如果一个合约要接受EIP-721,其必须要实现onERC721Received
方法,当用户调用safeTransferFrom
时,会在转账结束时,调用to地址的onERC721Received
方法,此时该方法的返回值应该为bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))
function onERC721Received(address _operator,address _from,uint256 _tokenId,bytes calldata _data) external returns(bytes4);
openzeppelin的EIP-721实现
由于目前见到的所有的NFT合约其都是基于Openzepplin的EIP-721实现,故充分了解Openzepplin的EIP-721实现是非常有必要的,也是非常有帮助的。
在openzeppelin的实现中,其实现EIP-721的主要在ERC721.sol文件中,实现枚举部分在ERC721Enumberable.sol文件中。
ERC721.sol
ERC721文件中,需要实现的接口有EIP-721和metadata两部分,含EIP-165部分。
-
首先是需要设计全局变量:
name() => string private name;
symbol() => string private symbol;
balanceOf() => map(address=>uint256) private _balances;
ownerOf() => map(uint256=>address) private _owners;
getApproved() => map(uint256=>address) private _tokenApproves;
isApprovedForAll() => map(address=>map(address=>bool)) private _operatorApproves;
然后是依次实现EIP-721中定义的接口方法:
-
EIP-165中定义的
supportsInterface
:
function supportsInterface(bytes4 interfaceId) public view returns (bool) {
bytes4 EIP165Interface = bytes4(keccak256("supportsInterface(bytes4)"));
bytes4 dummyInterface = bytes4(0xffffffff);
if (interfaceId == dummyInterface) {
return false;
}
if (interfaceId == EIP165Interface) {
return true;
}
if (interfaceId == type(IERC721).interfaceId) {
return true;
}
if (interfaceId == type(IERC721Metadata).interfaceId) {
return true;
}
return false;
}
-
实现EIP-721中定义的get方法:
function balanceOf(address _owner) public view returns (uint256) {
//要求_owner不能为address(0)
require(_owner != address(0), "ERC721/balanceOf owner can not be address(0)");
return _balances[_owner];
}
function ownerOf(uint256 _tokenId) public view returns (address) {
//要求任何一个tokenId的owner都不能是address(0)
address owner = _owners[_tokenId];
require(owner != address(0), "ERC721/ownerOf owner can not be address(0)");
return owner;
}
function getApproved(uint256 _tokenId) public view returns (address) {
//要求_tokenId必须是有效的tokenId
//怎么判断一个tokenId是否是有效的tokenId呢?添加一个辅助函数_exists,即判断该tokenId的owner不应该是address(0)
//address(0)能否是一个被授权的地址呢?是可以的,意味着该TokenId不对其他任何地址授权
require(_exists(_tokenId), "ERC721/getApproved not a valid tokenId");
return _tokenApproved[_tokenId];
}
function isApprovedForAll(address _owner, address _operator) public view returns (bool) {
return _operatorApproved[_owner][_operator];
}
function _exists(uint256 _tokenId) internal view returns (bool) {
return _owners[_tokenId] != address(0);
}
-
实现EIP-721 Metadata中定义的get方法:
function name() public view returns (string) {
return name;
}
function symbol() public view returns (string) {
return symbol;
}
function tokenURI(uint256 _tokenId) public view returns (string) {
//tokenURI指向一个特定的JSON文件,也可以是一个字符串,其是由baseURI和tokenId进行组合得到
//要求tokenId是一个有效的tokenId
require(_exists(_tokenId), "ERC721/tokenURI not a valid tokenID");
//首先检查是否定义了baseURI,如果定义了baseURI则将其与tokenID进行组合得到tokenURI,如果没有定义baseURI,则直接返回空
bytes memory baseURI = _baseURI();
if (bytes(baseURI).length > 0) {
return string(abi.encodePacked(baseURI,_tokenId.toString()));
}
return "";
}
function _baseURI() internal view returns (string) {
return "";
}
-
实现EIP-721中定义的transfer方法:
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes calldata _data) external payable {
//要求msg.sender必须是owner或者授权的operator或者是授权的地址
//要求from必须是owner的地址,不能是operator的地址或者其他地址
//要求to必须不能是address(0)
//要求tokenId必须是有效的tokenId
//要求当transfer结束时,检查to地址是否是合约地址,如果是合约地址则需要调用onERC721Received方法,返回特定的值
address owner = ownerOf(_tokenId);
address approvedAddress = getApproved(_tokenId);
require(msg.sender == owner || msg.sender == approvedAddress || isApprovedForAll(owner,msg.sender),"EIP721/safeTransferFrom msg.sender not correct");
require(from == owner, "EIP721/safeTransferFrom from not correct");
require(to != address(0), "EIP721/safeTransferFrom to not correct");
require(_exists(_tokenId), "EIP721/safeTransferFrom tokenId not exists");
_transfer(_from,_to,_tokenId);
require(_checkOnERC721Received(_from,_to,_tokenId,_data));
}
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable {
safeTransferFrom(_from,_to,_tokenId,"");
}
function transferFrom(address _from, address _to, uint256 _tokenId) external payable {
//要求msg.sender必须是owner或者授权的operator或者是授权的地址
//要求from必须是owner的地址,不能是operator的地址或者其他地址
//要求to必须不能是address(0)
//要求tokenId必须是有效的tokenId
_transfer(_from,_to,_tokenId);
}
function _transfer(address _from, address _to, uint256 _tokenId) internal {
//要求from必须是owner的地址,不能是operator的地址或者其他地址
//要求to必须不能是address(0)
require(from == ownerOf(_tokenId), "EIP721/safeTransferFrom from not correct");
require(to != address(0), "EIP721/safeTransferFrom to not correct");
//更改tokenId对应的所有权,取消相应tokenId的授权地址的权限,但不能取消经销商的权限
_balances[_from] = _balances[_from].sub(1);
_balances[_to] = _balances[_to].add(1);
_owners[_tokenId] = _to;
_tokenApproves[_tokenId] = address(0);
}
function _checkOnERC721Received(address _from, address _to, uint256 _tokenId, bytes calldata _data) internal returns (bool) {
//作用是判断地址to是否是一个合约地址,如果不是一个合约地址则直接返回true,如果是一个合约地址,则需要调用地址to的onERC721Received方法来判断返回值是否是一个特定的返回值
//是EOA,必须同我直接交互,不能通过proxy
bytes4 funcSelector = bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"));
if (msg.sender == tx.origin) {
return true;
}
//是合约地址
//这样写会把to地址的报错给吞掉,没有把报错信息抛出来
if (_to.isContract()) {
bytes4 retVal = IERC721Received(_to).onERC721Received(msg.sender,_from,_tokenId,_data);
return funcSelector == retVal;
}
//是合约地址
//这样写可以把to地址的报错抛出来
if (_to.isContract()) {
(bool success, bytes memory res) = _to.call(abi.encodeWithSelector(funcSelector,_from,_to,_tokenId,_data));
bytes4 retVal;
uint256 retSize;
assembly {
retSize := mload(res)
retVal := mload(add(res,0x20))
}
if (success) {
require(retSize == 0x04);
return funcSelector == retVal;
} else {
if (retSize == 0) {
revert("ERC721: transfer to non ERC721Receiver Implementer");
} else {
assembly {
revert(add(0x20, res),mload(res))
}
}
}
}
return false;
}
-
实现EIP-721中定义的set方法
function approve(address _approved, uint256 _tokenId) external payable {
//要求msg.sender 必须是owner或者是授权的经销商
//要求tokenId必须是存在的tokenId
//可以给address(0)授权,意味着该tokenId没有授权的地址
//不能给自己授权
address owner = ownerOf(_tokenId);
require(msg.sender == owner || isApprovedForAll[owner][msg.sender]);
require(_exists[_tokenId]);
_tokenApproves[_tokenId] = _approved;
}
function setApprovalForAll(address _operator,bool _approved) external{
//要求经销商不能是自己
require(msg.sender != _operator,"ERC721/setApprovalForAll msg.sender can not be the operator itself");
_operatorApproves[owner][_operator] = _approved;
}
关键点:自己不能是自己的经销商!
原因在于如果alice是alice自己的经销商,意味着
_operatorApproves[alice][alice] = true
,则当alice作为owner给bob转一个tokenId时,由于在_transfer
函数的逻辑设计中,只清楚了该tokenId对应的授权地址的授权,即_tokenApproves[_tokenId] = address(0)
, 并没有清除相应的经销商的授权。同时,清除经销商的权限也是不合理的。其实此时作为经销商的alice还是无法再去transfer一次tokenId
-
其他的辅助方法:mint,burn
在当前的NFT合约中,大量使用了mint方法,然而此方法并不是EIP-721中规定的方法,但是其已经成为事实标准。简单来讲mint方法是新增一个tokenId,该tokenId不能是已经存在的,然后把该tokenId添加到对应的owner中。burn方法是删除该tokenId即可。mint和burn在openzeppelin的实现中都遵循了safeTransfeFrom的思路。mint方法并未提供一个公开的方法,而是一个_safeMint()
内部方法,需要项目方自己去结合逻辑实现一个mint方法。
function _safeMint(address _to, uint256 _tokenId, bytes memory _data) internal {
//要求tokenId必须不能是一个已经存在tokenId
//要求地址to如果是合约地址,则需要实现onERC721Received方法
require(!_exists[_tokenId],"ERC721/_safeMint tokenId already exists");
_mint(_to,_tokenId);
require(_checkOnERC721Received(address(0),_to,_tokenId,_data),"ERC721/_safeMint not a valid receiver");
}
function _safeMint(address _to, uint256 _tokenId) internal {
_safeMint(_to,_tokenId,"");
}
function _mint(address _to, uint256 _tokenId) internal {
//要求_tokenId必须不能是一个已经存在的tokenId
//要求地址_to必须不能是address(0)
require(!_exists(_tokenId), "ERC721/_mint tokenId already exists");
require(_to != address(0),"ERC721/_mint _to can not be address(0)");
_owners[_tokenId] = _to;
_balances[_to] += 1;
emit Transfer(address(0), _to, _tokenId);
}
function _burn(uint256 _tokenId) internal {
//要求tokenId必须存在,但是不能真的把tokenId转给地址0,只是删除owners中对应的tokenId
require(_exists(_tokenId),"");
//要求清除该tokenId对应的授权地址,但不能清除经销商的授权
_tokenApproves[_tokenId] = address(0);
_balances[msg.sender] -= 1;
delete _owners[_tokenId];
emit Transfer(msg.sender, address(0), _tokenId);
}
ERC721Enumerable.sol
ERC721的枚举部分,该部分与ERC721主体部分分开,其实现的功能主要是提供totalSupply以及提供了两个索引,一个索引是tokenByIndex
全局索引,另一个索引是tokenOfOwnerByIndex
,即用户的索引。
这里需要思考如何实现这两个索引。目前在ERC721.sol文件中,提供了_owners,_balances,_tokenApproves,_operatorApproves
四个map,现在需要提供两个索引,这两个索引应该如何与这些已有的map结合起来?
//要得到最新的总供应量,即返回目前被NFT合约追踪下来的总的有效NFT数量
totalSupply => uint256[] private _allTokens; => totalSupply = _allTokens.length;
//根据全局索引来查找对应的tokenId
tokenByIndex => uint256[] private _allTokens; => return _allTokens[index];
//根据特定的owner的索引查找其拥有的所有tokenId
//tokenOfOwnerByIndex => mapping(address=>uint256[]) private _ownedTokens; => return _ownedTokens[owner][index];
tokenOfOwnerByIndex => mapping(address=>mapping(uint256=>uint256)) private _ownedTokens; => return _ownedTokens[owner][index];
//在索引用户的tokenId时,需要保证index值小于用户的balance
结合目前的需求,因为要delete 列表_allTokens
中的某一个tokenId
,故还需要额外维护一个tokenId=>index
的逆向map。
mapping(uint256=>uint256) private _allTokensIndex;
因为要delete列表_ownedTokens[owner]
中的某一个tokenId,故还需要额外维护一个tokenId=>index的逆向map:
mapping(uint256=>uint256) private _ownedTokensIndex;
- 枚举中的get方法:
function totalSupply() public view returns (uint256) {
return _allTokens.length;
}
function tokenByIndex(uint256 _index) public view returns (uint256) {
require(_index < totalSupply(), "ERC721Enumerable/tokenByIndex index overflow");
return _allTokens[_index];
}
function tokenOfOwnerByIndex(address _owner,uint256 _index) public view returns (uint256) {
//要求index不能大于等于owner的余额
//要求owner不能是地址0
require(_index < balanceOf(_owner), "ERC721Enumberable/tokenOfOwnerByIndex index overflow balance");
require(_owner != address(0));
return _ownedTokens[_owner][_index];
}
- 枚举中的set方法
这里需要思考枚举中的set方法应该在什么时候调用:其应该在每一次transfer之前都需要调用一次,因为transfer时肯定就发生了状态的变化。这里就需要用到ERC721中预先留下来的勾子函数:
function _beforeTokenTransfer(address _from,address _to,uint256 _tokenId) internal {}
在这个函数中,需要做如下的逻辑判断:
from | to | 含义 |
---|---|---|
不为address(0) | 不为address(0) | 普通的transfer,此时的tokenId应该从from->to |
为address(0) | 不为address(0) | 此时是mint操作 |
不为address(0) | 为address(0) | 此时是burn操作 |
根据上述表格可以看到有三种类型的操作,transfer,mint和burn,需要针对三种不同的类型来分别更新mapping中的值
普通的transfer操作
针对普通的transfer操作:
_allTokens列表应该保持不变;
_ownedTokens列表需要更新 => _ownedTokens[from]相应减去该tokenId,_ownedTokens[to]应增加相应tokenId
_ownedTokensIndex需要更新 => _ownedTokensIndex[_tokenId] = newIndex;
_allTokensIndex 不需要更新
在openzeppelin的实现中,即为:
function _removeTokenFromOwnerEnumeration(address from, uint256 tokenId) private {}
function _addTokenToOwnerEnumeration(address to,uint256 tokenId) internal {}
mint操作
针对mint操作:
_allTokens列表需要新增 => _allTokens.push(_tokenId);
_ownedTokens列表需要新增 => _ownedTokens[to][balanceOf(to)]=_tokenId;
_ownedTokensIndex列表需要新增 => _ownedTokensIndex[_tokenId] = balanceOf(to);
_allTokensIndex 需要新增 => _allTokensIndex[_tokenId] = totalSupply();
在openzeppelin的实现中,即为:
function _addTokenToAllTokensEnumeration(uint256 tokenId) private {
//注意先后顺序
_allTokensIndex[tokenId] = _allTokens.length;
_allTokens.push(tokenId);
}
function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private {
uint256 length = balanceOf(to);
_ownedTokens[to][length] = tokenId;
_ownedTokensIndex[tokenId] = length;
}
burn操作
针对burn操作:
_allTokens列表需要删除 => delete _allTokens[_allTokensIndex[tokenId]];
_allTokensIndex 需要更新 => delete _allTokensIndex[tokenId]; //问题:如果删除后,该map保存的其他index应该都不准确了,应该如何设计?
_ownedTokens列表需要删除 => delete _ownedTokens[from][_ownedTokensIndex[tokenId]];
_ownedTokensIndex 需要更新 => delete _ownedTokensIndex[tokenId];
在openzeppelin的实现中,即为:
function _removeTokenFromOwnerEnumeration(address from, uint256 tokenId) private {
//为了解决上面提出的问题,这里删除时,预先将要删除的tokenId放置在最后一个槽位,然后只删除最后一个槽位
//swap and pop
uint256 lastTokenIndex = balanceOf(from) - 1;
uint256 tokenIndex = _ownedTokensIndex[tokenId];
//swap 如果不是最后一个槽位则 swap
if (tokenIndex != lastTokenIndex) {
uint256 lastTokenId = _ownedTokens[from][lastTokenIndex];
//swap
_ownedTokens[from][tokenIndex] = lastTokenId;
_ownedTokensIndex[lastTokenId] = tokenIndex;
}
//pop
delete _ownedTokens[from][lastTokenIndex];
delete _ownedTokensIndex[tokenId];
}
function _removeTokenFromAllTokensEnumeration(uint256 tokenId) private {
//swap and pop
uint256 lastTokenIndex = _allTokens.length - 1;
uint256 tokenIdex = _allTokensIndex[tokenId];
//swap 为节约gas费用,不考虑是否是最后一个槽位
uint256 lastTokenId = _allTokens[lastTokenIndex];
_allTokens[tokenIndex] = lastTokenId;
_allTokensIndex[lastTokenId] = tokenIndex;
//pop
delete _allTokensIndex[tokenId];
_allTokens.pop();
}
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。
- 发表于 2021-09-24 14:04
- 阅读 ( 1123 )
- 学分 ( 171 )
- 分类:智能合约
评论