这篇文章主要介绍了EIP-721的openzeppelin实现 ,文中通过代码以及文档配合进行讲解,很详细,它对在座的每个人的研究和工作具有很经典的参考价值。 如果需要,让我们与区块链资料网一起学习。

https://www.interchains.cc/24708.html

EIP-721的openzeppelin实现是很好的区块链资料,他说明了区块链当中的经典原理,可以给我们提供资料,EIP-721的openzeppelin实现学习起来其实是很简单的,

不多的几个较为抽象的概念也很容易理解,之所以很多人感觉EIP-721的openzeppelin实现比较复杂,一方面是因为大多数的文档没有做到由浅入深地讲解,概念上没有注意先后顺序,给读者的理解带来困难

EIP-721的openzeppelin实现

  • samczsun

这段时间总是与NFT打交道,大部分NFT都采用了EIP721标准,且均采用了Openzepplin的EIP721实现。前段时间详细看过Openzepplin的相关实现,但是偷懒了,没有整理成文档,导致后面的记忆总是不深刻,理解也不深刻。此次正好将其实现全部整理一下。

EIP-721的openzeppelin实现

这段时间总是与NFT打交道,大部分NFT都采用了EIP721标准,且均采用了Openzepplin的EIP721实现。前段时间详细看过Openzepplin的相关实现,但是偷懒了,没有整理成文档,导致后面的记忆总是不深刻,理解也不深刻。此次正好将其实现全部整理一下。 EIP-721的openzeppelin实现

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中的函数签名一致。但是需要明确如下几点:

  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,即存在
  1. 针对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
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部分。

  1. 首先是需要设计全局变量:

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:

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; }
  1. 实现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); }
  1. 实现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 ""; }
  1. 实现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; }
  1. 实现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

  1. 其他的辅助方法: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;
  1. 枚举中的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]; }
  1. 枚举中的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();  }

部分转自网络,侵权联系删除www.interchains.cchttps://www.interchains.cc/24708.html

区块链毕设网(www.interchains.cc)全网最靠谱的原创区块链毕设代做网站 部分资料来自网络,侵权联系删除! 最全最大的区块链源码站 ! QQ3039046426
区块链知识分享网, 以太坊dapp资源网, 区块链教程, fabric教程下载, 区块链书籍下载, 区块链资料下载, 区块链视频教程下载, 区块链基础教程, 区块链入门教程, 区块链资源 » EIP-721的openzeppelin实现

提供最优质的资源集合

立即查看 了解详情