定义

ERC721标准包括四个接口:

  • 主要的ERC721合约
  • 能够接受ERC721 Token的标准
  • 两个可拓展的接口

ERC721必须满足的条件

  • 所有权

    如何处理Token的所有权

  • 创建

    如何创建Token

  • 转账与授权

    Token如何转账以及如何允许其他地址具有转账的能力

  • 销毁

    如何销毁Token

Token所有权

ERC20做法

从所有权来看,通过一个映射,来实现token余额和用户地址的对应

mapping(address => uint256) balances

如果用户购买了ERC20 token,用户最终对token的所有权可以通过合约来验证,因为在用户购买token时,合约里保有一条记录表明每个地址拥有多少tokens。

ERC721做法

由于ERC721是不可分的,所以ERC721不能像ERC20一样映射到一个地址,而必须知道拥有的每一个唯一的token。所以在ERC721中,所有权是由映射到一个地址的一个token的索引/ID的数组决定的。因为每一个token的值唯一,而不能只看token的余额,必须仔细检查合约创建的每一个token。主合约必须保有所有合约创建的token的列表。每个token都有各自的索引号,定义在合约的allTokens数组。

uint256[] internal allTokens

同时为了知道一个地址拥有哪些token,需要把token的索引和数量映射到一个地址上。

mapping(address => uint256[]) internal ownedTokens

同时为了了解token属于哪个地址,需要将tokenId和地址映射起来

mapping(uint256 => address) internal tokenOwner

为了针对要把一个token从一个地址拥有列表里删除的需求,必须追踪一下信息。ownedTokensIndex映射把每个token ID映射到它们所有者数组相应的位置/索引上。同时,把token ID映射到全局的allTokens数组上。

// 将TokenId和拥有者的tokenList的指数做一个映射mapping (uint256 => uint256) internal ownedTokensIndex//将TokenId映射到全局的allTokens数组上mapping(uint256 => uint256) internal allTokensIndex

同时引入一个变量来追踪一个地址拥有多少ERC721 token

mapping(address => uint256 ) internal ownedTokenCount

Token创建

ERC20做法

在ERC20标准里,有一个变量**totalSupply_来记录所有可用token的供应量。构造函数用来设置变量的初始值,所有权等。同时引入mint()**函数用来处理增加token发行的需求。(在mint函数中需要更新totalSupply_)

ERC721做法

对于ERC721来说,因为每个token都是唯一的,所以必须手工创建每一个token。在ERC721合约中有关于总供应量的两个函数addTokenTo()_mint()

  • 更新所有全局的所有权变量

    1.调用合约里的addTokenTo()函数

    2.通过super.addTokenTo()先调用基类ERC721合约里的addTokenTo()函数。

函数有两个参数:to或者拥有token的账户地址和tokenId或者token的唯一ID。

1.首选在ERC721BasicToken合约里,检查到token ID没有被合约拥有

2.设置所请求的token ID的所有者,并且更新那个账户拥有token的数量

3.把这个新的token添加到ownedTokens数组的最后并且保存新的token的索引

4.更新所有者的数组

//在ERC721Token.sol里调用本函数function addTokenTo(address _to,uint256 _tokenId) internal {super.addTokenTo(_to,tokenId);uint256 length = ownedTokens[_to].length;ownedTokens[_to].push(_tokenId);ownedTokensIndex[_tokenId] = length;}//在ERC721TokenBasicToken.sol里调用本函数function addTokenTo(address _to,uint256 _tokenId) internal {require(tokenOwner[_tokenId] == address(0));tokenOwner[_tokenOd]=_to;ownedTokenCount[_to]=ownedTokensCount[_to].add(1)}

由此可见,用addTokenTo()函数来更新地址到了某一个用户。那么allTokens数组,mint()函数就是用来处理allTokens。

  1. mint()函数首先跳到基类的合约实现里,保证铸币地址不是0
  2. 然后调用addTokenTo(),来回调派生合约里的addTokenTo()函数。
  3. 在基类合约里的mint()函数完成了,再把tokenId加到allTokenIndex mapping及allTokens数组。
  4. 在派生的ERC721合约里,使用mint()来创建新token
function _mint(address _to,uint256 _tokenId) internal{super._mint(_to,_tokenId);allTokensIndex[_tokenId]=allTokens.length;allTokens.push(_tokenId);}function _mint(address _to,uint256 _tokenId) internal{require(_to!=address(0));addTokenTo(_to,_tokenId);Transfer(address(0),_to,_tokenId);}

ERC721中的元数据的作用是什么呢?已经创建了token和token ID,但是还没任何数据。OpenZeppelin提供了一个例子,说明如何将一个token ID映射到URL字符串。

mapping (uint256 => string) internal tokenURLs;

为了设置一个token的URL数据,这里引入了setTokenURL()函数

1.首先通过mint()函数得到tokenID和URL信息,便可以设置数据

2.在Token里映射到一个tokenID

注意:在设置数据前,必须要确认一个token ID存在

function _setTokenURL(uint256 _tokenId,string _uri) internal{require(exists(_tokenId));tokenURLs[_tokenId] = _uri;}function exist(uint256 _tokenId) public view returns(bool){address owner = tokenOwner[_tokenId];return owner != address(0);}

传输和授权

ERC20做法

传输

在ERC20里,可以直接使用transfer()函数来传输ERC20 token。在transfer()函数里,首先指定一个希望发送到的地址以及token的数量,然后再更新ERC20 contract。

function transfer(address _to,uint256 _value) public returns(bool){require(_to!=address(0));require(_value<=balances[msg.sender]);balances[msg.sender] = balances[msg.sender].sub(_value);balances[_to] = balances[_to].add(_value);Transfer(msg.sender,_to,_value);return ture;}

授权

在ERC20标准中,有一个全局变量allowed,表示一个所有者的地址被映射到一个已授权的地址并且同时被映射到token的数量。为了设置这个变量,在approve()函数里,可以映射授权到期望的spender和value。

//Global variablemapping (address => mapping(address=>uint256)) internal allowed;function approve(address _spender,uint256 _value) public returns (bool){allowed[msg.sender][_spender] = _value;Approval(msg.sender,_spender,_value);return true;}

一旦允许另外一个地址来传输token,具体运输过程如下

1.已授权的spender使用transferFrom()函数,其中,函数的参数from代表原始的所有者地址;to代表接受者地址;value代表token数量

2.要检查最初的所有者确实拥有请求数量的token

require(_value<=balance[_from])

3.检查msg.sender是不是被授权来传输token

4.更新映射的balances和allowed的数量’

function transferFrom(address _from,address _to,uint256 _value) public returns (bool){require(_to != address(0));require(_value <= balances[_from]);require(_value <= allowed[_from][msg.sender]);balances[_from] =balances[_from].sub(_value);balances[_to] = balances[_to].add(_value);allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);Transfer(_from,_to,_value);return true;}

ERC721做法

授权

对于ERC721标准,通过token ID来授权使用approve()函数。tokenApprovals是一个全局变量,把一个token索引或者ID映射到一个地址上,而这个地址是获得授权可以传输token的。

在approve()函数中里

1.首先检查一下所有权或者msg.sender是不是isApprovedForAll()

2.然后可以使用setApprovalForAll()函数来授予一个地址去传输和处理所有的token

  • Token必须被一个特定地址所拥有
  • 在全局变量operatorApprovals中,所有者的地址被映射到一个被授权的spender的地址,然后再被映射一个bool变量,这个变量默认为false,但使用setApprovalForAll()函数可以设置这个映射为true,并且允许该地址来处理所有拥有的ERC721
mapping (uint256 => address) internal tokenApprovals;mapping (address => mapping(address => bool)) internal operatorApprovals;function approve(address _to,uint256 _tokenId) public {address owner = ownerOf(_tokenId);require(_to != owner);require(msg.sender == owner || isApprovedForAll(owner,msg.sender));if(getApproved(_tokenId) != address(0) || _to != address(0) ){tokenApprovals[_tokenId] = _to;Approval(_owner,_to,_tokenId);}}function isApprovedForAll(address _owner,address _operator) public view returns (bool){return operatorApprovals[_owner][_operator];}function getApproved(uint256 _tokenId) public view returns (address){return tokenApprovals[_tokenId];}function setApprovalForAll(address _to,bool _approved) public {require(_to != msg.sender);operatorApprovals[msg.sender][_to] = _approved;ApprovalForAll(msg.sender,_to,_approved);}

传输

在完整的实现中,有两种方法

  • 在transferFrom()函数中,发送者和接收者的地址以及传输的tokenID都被设定,用一个修饰符canTransfer()来确保msg.sender是获得授权的或者就是token的所有者。

    1.当确认发送者和接受者的地址都是合法的后,clearApproval()函数被用来删除token原来的拥有者授权,也就是原来的拥有者不再拥有授权的权限,从而使以前获得授权的spender不再能够传输token。

    2.其后,在ERC721合约的完整实现中,调用removeTokenFrom()函数,在于ERC721的基类合约实现中类似,调用addTokenTo()函数来调用基类合约里的removeTokenFrom()函数,可以看到,指定的token被从ownedTokensCount mapping,tokenOwner mapping里删除。

    3.另外,需要把所有者的ownedToken数组里的最后一个token移到被传输的token的索引位置,同时将数组长度减一

    4.最后,使用addTokenTo()函数来把token的索引/id加到新的所有者名下。

modifier canTransfer (uint256 _tokenId){require(isApprovedOrOwner(msg.sender,_tokenId));_;}function isApprovedOrOwner(address _spender,uint256 _tokenId) internal view returns(bool){address owner = ownerOf(_tokenId);return _spender == owner || getApprived(_tokenId) == _spender ||isApprovedForAll(owner,_spender);}function transferFrom(address _from,address _to,uint256 _tokenId) public canTransfer(_tokenId){require(_from != address(0));require(_to !=address(0));clearApproval(_from,_tokenId);removeTokenFrom(_from,_tokenId);addTokenTo(_to,_tokenId);Transfer(_from,_to,_tokenId);}function clearApproval(address _owner,uint256 _tokenId) internal {super.removeTokenFrom(_from,_tokenId);uint256 tokenIndex = ownedTokensIndex[_tokenId];uint256 lastTokenIndex = ownedTokensIndex[_from].length.sub(1);uint256 lastToken = ownedTokens[_from][lastTokenIndex];ownedTokens[_from][tokenIndex] = lastToken;ownedTokens[_from][lastTokenIndex] = 0;owned[_from].length--;ownedTokensIndex[_tokenId]=0;ownedTokensIndex[lastToken] = tokenIndex;}function removeTokenFrom(address _from,uint256 _tokenId) internal{require(ownerOf(_tokenId)==_from);ownedTokenCount[_from]=ownedTokenCount[_from].sub(1);tokenOwner[_tokenId]=address(0)}

外部拥有的账户可以使用ERC721的完整合约来交易token;但是如果发送token到一个合约而这个合约没有函数通过原有的ERC721合约来交易和传输token的化,token就会丢失并且没有办法找回。针对这个问题,提出了ERC223,ERC223实在ERC20的基础上进行了改进来防止这种错误的传输。

为了解决以上问题,ERC721标准的完整实现引入了一个safeTransferFrom()函数。

在讨论这个函数前,首先来看实现了ERC721Receiver.sol接口的ERC721Holder.sol里的一些需求。

ERC721Holder.sol是钱包的一部分,也是一个拍卖或者经纪人合约。

EIP165的目标是创建一个标准用来发布和发现一个智能合约实现了哪些接口。那么如何来发现一个接口?

这里实用了一个魔术值ERC721_RECEIVED,它是onERCReceived()函数签名。一个函数签名是标准签名字符串的前4个字节。这种情况下,可通过bytes(keccak256(“onERC721Received(address,uint256,bytes)”))来计算。

实用函数签名来验证在合约的字节码中是否被使用,从而判断函数是否被调用。

合约里每一个函数都有独有的签名,并且当调用合约时,EVM使用一系列的switch/case语句来找到函数签名,通过函数签名来找到所匹配的函数,然后执行相应的函数。

结果,在ERCHolder合约里,可以发现只有onERCReceived()函数和函数签名与ERC721Received接口里的ERC721_RECEIVED变量相匹配。

contract ERC721Receiver{/***@dev 如果接收到NFT则返回魔术值,魔术值等于*‘bytes(keccak256("onERC721Received(address,uint256,bytes)"))’*也可以通过‘ERC721Receiver(0).onERC721Received.selector’获取*/bytes constant ERC721_RECEIVED = 0xf0b9e5ba;/**    *@notice处理函数当收到一个    *@devERC721合约在'safetransfer'后调用这个函数在收到NFT的时候    *这个函数可能抛出异常,导致回退以及拒绝Transfer    *这个函数可能使用20000GAS。如果返回的不是魔术值,则必须回退    *Note:合约地址时msg.sender    *@para_from 发送地址    *@para_tokenId 被传送的NFT ID    *@para_data 额外数据,没有指定的数据格式    *@retutn 'bytes4(keccak("onERC721Received(address,uint256,bytes)"))'    */    function onERC721Received(address _from,uint256 _tokenId,bytes _data) public retyrbs(bytes4);}contract ERC721Holder is ERC721Received{function onERC721Received(address,uint256,bytes) public retyrbs(bytes4){return ERC721_RECEIVED;}}

现在ERC721Holder合约还不是一个处理ERC721 token的完整的合约。这个模块用来提供一种标准的方法来验证ERC721Receive标准接口是否被使用。需要继承或者派生ERC721Holder合约来调用钱包或者拍卖合约里的代码来处理ERC721 token。甚至对于代管的token,也需要这样的功能来调用合约函数来在需要的时候从合约里转出token。

//option 1function safeTransferFrom(address _from,address _to,uint256 _tokenId) public canTransfer(_tokenId){safeTransferFrom(_from,_to,_tokenId,"");}//option 2function safeTransferFrom(address _from,address _to,uint256 _tokenId bytes_data) public canTransfer(_tokenId){    transferFrom(_from,_to,_tokenId);    require(checkAndCallSafeTransfer(_from,_to,_tokenId,_data))}function checkAndCallSafeTransfer(address _from,address _to,uint256 _tokenId,bytes _data) internal returns (bool){if(!_to.isContract()){return true;}bytes4 retval = ERC721Received(_to).onERC721Received(_from,_tokenId,_data);return (retval==ERC721_RECEIVED);}function isContract(address addr) internal view returns(bool){uint256 size;assembly {size:=extcodesize(addr)}return size>0;}

下面讨论safeTransferFrom()函数的工作原理。可以选择选项1来传输token,这种情况下调用safeTransferFrom()函数不需要任何参数;也可以选择选项2,用参数bytes_data。与此类似,transferFrom()函数被用来把token的所有权从from地址转到to地址。同时还调用了checkAndCallSafeTransfer()函数,首先通过AddressUtils.sol库包来检查to地址是否是一个合约地址。可以通过isContract()函数来理解实现过程。在确认to是否是一个合约地址后,检查onERC721Received()的函数签名是否符合期望的接口的标准接口。如果不匹配的话,则transferFrom()函数就会吊销,因为判定to地址上的合约没有实现所期望的接口。

销毁

ERC20做法

对于ERC20标准,因为只是操作一个映射的余额,因此只需要销毁一个特定地址的token。地址可以是一个用户或者合约地址。在下面的burn()函数中,通过value变量来指定住呢比销毁的token的数量。要销毁的token的拥有者由msg.sender指定,所以必须要更新他们的地址余额,然后减少token的总供应量totalSupply。这里Burn和Transfer是events

function burn (uint256 _value) public{require(_value <= balances[msg.sender]);address burner = msg.sender;balances[burner] = balances[burner].sub(_value);totalSupply_ = totalSupply_.sub(_value);Burn(burner,_value);Transfer(burner,address(0),_value);}

ERC721做法

对于ERC721 tokens,必须确保特定的token ID或者索引被移除。与addTokenTo()和mint()函数类似,burn()函数使用super来调用基本的ERC721实现。

1.首先没调用clearApproval()函数

2.然后通过removeTokenFrom()删除token的所有权,并且触发Transfer事件来通知前端

3.删除和token相关联的元数据

4.最后,就像删除token的所有权,重排allTokens数组,用数组里的最后一个token代替tokenId索引位置

function burn(address _owner,uint256 _tokenId)internal{super._burn(_owner,_tokenId);//清除metadata(if any)if(bytes(tokenURLs[_tokenId]).length !=0){delete tokenURLs[_tokenId];}//重排所有Token的数组uint256 tokenIndex = allTokenIndex[_tokenId];uint256 lastTokenIndex = allTokens.length.sub(1);uint256 lastToken = allTokens[lastTokenIndex];allTokens[tokenIndex] = lastToken;allTokens[lastTokenIndex]=0;allTokens.length--;allTokensIndex[_tokenId]=0;}function burn(address _owner,uint256 _tokenId) internal{clearApproval(_owner,_tokenId);removeTokenFrom(_owner,_tokenId);Transfer(_owner,address(0),_tokenId);}

钱包接口

钱包应用必须要实现钱包接口。一个合法的ERC721TokenReceiver需要实现函数:

function onERC721Received(address operator,address from,uint256 tokenId,bytes data) external returns(bytes);

并且返回:

bytes(keccak256("onERC721Received(address,address,uint256,bytes)"))

一个非法的Receiver要么不是先那个函数,要么返回其他任何内容,下面是一个合法的返回:

contract ValidReceiver is ERC721TokenReceiver{function onERC721 Receiver(address operator,address from,uint256 tokenId,bytes data) external returns(bytes){return bytes(keccak256("onERC721Receiver(address,address,uint256,bytes)"));}}

下面的示例是一个非法的返回

contract InvalidReceiver is ERC721TokenReceiver{function onERC721Receiver(address operator,address from,uint256 tokenId,bytes data) external returns(bytes){return bytes(keccak256("some invalid return data"));}}

元数据拓展

元数据扩展给代币合约一个名字和代码(如ERC20 token),并且给每个代币一些额外的数据使代币独一无二。可枚举的拓展使对代币的排序更容易,而不仅仅通过tokenID来排序。元数据拓展是可选的,元数据接口允许智能合约获得不可分通证的元数据,如名字以及其他详细信息。

这里声明合约是继承自TokenERC721.sol合约和ERC721Metadata扩展的接口。

contract TokenERC721Metadata is TokenERC721,ERC721Metadata{

Metadata扩展由以下三个函数组成:

function name() external view returns (string _name);function symbol() external view returns (string _symbol);function tokenURL(uint256 _tokenId) external view returns (string);

构造函数

constructor(uint _initialSupply,string _name,string _symbol,string _uriBase)public TokenERC721(_initialSupply){_name=_name;_symbol=_symbol;_uriBase=bytes(_uriBase);//Add to ERC165 Interface ChecksupportedInterface[this.name.selector ^this.symbol.selector ^this.tokenURL.selector ^]=true;}function name() external view returns(string _name){_name=_name;}function symbol() external view returns(string _symbol){_symbol=_symbol;}function tokenURL(uint256 _tokenId) external view returns (string){require(isValidToken(_tokenId));uint maxLength=78;bytes memory reversed = new bytes(maxLength);uint i=0;//循环并且将字节加入数组while(_tokenId!=0){uint remainder = _tokenId%10;_tokenId/=10;reversed[i++]=bytes(48 + remainder);}//分配生成最终数组bytes memory s = new bytes(_uriBase.length + i);uint j;for(j=0;j<_uriBase.length;j++){s[j]=_uriBase[j];}//将tokenId加入最后的数组for(j=0;j<i;j++){s[j+_uriBase.length] = reversed[i-1-j];}return string(s);}

可枚举扩展

  • 接口定义
  • 实例
  • totalSupply
  • tokenByIndex
  • tokenOfOwnerByIndex
  • transferFrom
  • burnToken
  • issueTokens

ERC165标准

ERC165只有一个函数,用来检查合约的指纹是否和指定的接口的指纹相符

interface ERC165{///@notice 查询一个合约是否实现了某个接口///@param interfaceID ERC165标准里指定的接口ID///@dev 接口定义在ERC165标准中,这个函数使用的燃料费少于30000Gas///@return 如果合约实现了指定的接口,则返回‘true’///并且"interfaceID"不是0xffffffff,否则返回"false"function supportInterface(bytes interfaceID) external view returns (bool);}

要实现ERC721就必须实现ERC165