这段时间总是与NFT打交道,大部分NFT都采用了EIP721标准,且均采用了Openzepplin的EIP721实现。前段时间详细看过Openzepplin的相关实现,但是偷懒了,没有整理成文档,导致后面的记忆总是不深刻,理解也不深刻。此次正好将其实现全部整理一下。
这段时间总是与NFT打交道,大部分NFT都采用了EIP721标准,且均采用了Openzepplin的EIP721实现。前段时间详细看过Openzepplin的相关实现,但是偷懒了,没有整理成文档,导致后面的记忆总是不深刻,理解也不深刻。此次正好将其实现全部整理一下。
首先简单介绍下EIP-721标准,可以参考EIP-721: Non-Fungible Token Standard (ethereum.org)
在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事先存在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-721的合约中,其必须也要实现EIP-165标准,即通用接口注册标准。用于接口发现和验证。其思路是合约实现EIP-165中定义的supportsInterface(bytes4 interfaceId)
方法,该方法中将一个合约中所有的external函数签名进行亦或求值得到一个bytes4. 然后验证时遵循如下思路进行验证:
bytes4(keccak256("supportsInterface(bytes4)"))
即0x01ffc9a7
, 此时应该返回true0xffffffff
,此时应该返回falsethis.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.
在目前的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);
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,其必须要实现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);
由于目前见到的所有的NFT合约其都是基于Openzepplin的EIP-721实现,故充分了解Openzepplin的EIP-721实现是非常有必要的,也是非常有帮助的。
在openzeppelin的实现中,其实现EIP-721的主要在ERC721.sol文件中,实现枚举部分在ERC721Enumberable.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中定义的接口方法:
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;
}
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);
}
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 "";
}
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;
}
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
在当前的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);
}
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;
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方法应该在什么时候调用:其应该在每一次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操作:
_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操作:
_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操作:
_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();
}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!