ERC721 標準インターフェース

意味

ERC721 標準には、次の 4 つのインターフェイスが含まれています。

  • 主なERC721契約
  • ERC721トークンの標準を受け入れることができます
  • 2 つの拡張可能なインターフェイス

ERC721が満たさなければならない条件

  • 所有

    トークンの所有権を処理する方法

  • 作成

    トークンの作成方法

  • 譲渡と承認

    トークンの転送方法と、他のアドレスに転送機能を許可する方法

  • 破壊

    トークンの破壊方法

トークン所有権

ERC20の練習

所有権の観点から、トークン残高とユーザーアドレスの対応はマッピングによって実現されます。

mapping(address => uint256) balances

ユーザーが ERC20 トークンを購入する場合、ユーザーがトークンを購入すると、各アドレスが所有するトークンの数を示すレコードがコントラクトに記録されるため、ユーザーのトークンの最終的な所有権はコントラクトを通じて確認できます。

ERC721の練習

ERC721 は分割できないため、ERC721 を ERC20 のようなアドレスにマップすることはできませんが、所有するすべての一意のトークンを認識している必要があります。したがって、ERC721 では、所有権は、アドレスにマップされたトークンのインデックス/ID の配列によって決定されます。各トークンの価値は一意であるため、トークンの残高だけを見るのではなく、コントラクトによって作成された各トークンを注意深くチェックする必要があります。メイン コントラクトは、コントラクトによって作成されたすべてのトークンのリストを保持する必要があります。各トークンには独自のインデックス番号があり、コントラクトの allTokens 配列で定義されています。

uint256[] internal allTokens

同時に、アドレスがどのトークンを所有しているかを知るために、トークンのインデックスと数をアドレスにマップする必要があります。

mapping(address => uint256[]) internal ownedTokens

同時に、トークンがどのアドレスに属しているかを知るために、tokenId とアドレスをマッピングする必要があります。

mapping(uint256 => address) internal tokenOwner

アドレスの所有リストからトークンを削除する必要性に対処するには、情報を追跡する必要があります。ownTokensIndex マップは、各トークン ID を所有者配列内の対応する位置/インデックスにマップします。同時に、トークン ID をグローバルな allTokens 配列にマップします。

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

同時に、アドレスが持つ ERC721 トークンの数を追跡する変数が導入されました。

mapping(address => uint256 ) internal ownedTokenCount

トークンの作成

ERC20の練習

ERC20 標準では、利用可能なすべてのトークンの供給を記録する変数 **totalSupply_ があります。コンストラクターは、変数の初期値、所有権などを設定するために使用されます。同時に、増加するトークン発行の需要に対応するためにmint()** 関数が導入されました。(mint 関数で TotalSupply_ を更新する必要があります)

ERC721の練習

ERC721 の場合、各トークンは一意であるため、各トークンを手動で作成する必要があります。ERC721 コントラクトの総供給量については、addTokenTo()_mint()の 2 つの関数があります。

  • すべてのグローバル所有権変数を更新する

    1. コントラクトで addTokenTo() 関数を呼び出す

    2. 最初に、基本クラス ERC721 コントラクトの addTokenTo() 関数を super.addTokenTo() を介して呼び出します。

関数には 2 つのパラメーターがあります。to またはトークンを所有するアカウント アドレスと、tokenId またはトークンの一意の ID です。

1. まず、ERC721BasicToken コントラクトで、トークン ID がコントラクトによって所有されていないことを確認します。

2. 要求されたトークン ID の所有者を設定し、そのアカウントが所有するトークンの数を更新します

3. この新しいトークンを ownTokens 配列の末尾に追加し、新しいトークンのインデックスを保存します

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 マッピングと allTokens 配列に追加されます。
  4. 派生したERC721コントラクトで、 mint() を使用して新しいトークンを作成します
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 におけるメタデータの役割は何ですか? トークンとトークン ID は作成されていますが、まだデータがありません。OpenZeppelin は、トークン ID を URL 文字列にマップする方法の例を提供します。

mapping (uint256 => string) internal tokenURLs;

トークンの URL データを設定するために、setTokenURL() 関数がここで導入されます。

1. 最初に mint() 関数を介して tokenID と URL 情報を取得し、次にデータを設定できます

2.Token の tokenID にマップする

注: データを設定する前に、トークン 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 トークンを転送できます。transfer() 関数では、まず送信するアドレスとトークンの数を指定してから、ERC20 コントラクトを更新します。

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 標準では、許可されたグローバル変数があり、所有者のアドレスが許可されたアドレスにマップされ、同時にトークンの数にマップされることを示します。この変数を設定するには、approve() 関数で、目的の支出者と値に助成金をマップできます。

//Global variable
mapping (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;
}

別のアドレスがトークンの転送を許可されると、具体的な転送プロセスは次のようになります。

1. 許可された使用者は transferFrom() 関数を使用します。この関数のパラメータ from は元の所有者のアドレスを表し、to は受信者のアドレスを表し、値はトークンの数を表します。

2. 元の所有者が要求された量のトークンを実際に所有していることを確認するため

require(_value<=balance[_from])

3. msg.sender がトークンの送信を許可されているかどうかを確認します

4. マッピングされた残高の数を更新し、許可します'

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 標準では、approve() 関数を使用してトークン ID を承認します。tokenApprovals は、トークンのインデックスまたは ID を、トークンの転送が承認されたアドレスにマップするグローバル変数です。

承認()関数で

1. まず、所有者または msg.sender isApprovedForAll() かどうかを確認します

2. 次に、setApprovalForAll() 関数を使用して、すべてのトークンを転送および処理するためのアドレスを付与できます。

  • トークンは特定のアドレスによって所有されている必要があります
  • In the global variable operatorApprovals, the address of the owner is maps to the address of an Authorized spader, and then a bool variable is maps. この変数のデフォルトは 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);
}

伝染 ; 感染

完全な実装では、2 つの方法があります。

  • transferFrom() 関数では、送信者と受信者のアドレス、および転送の tokenID が設定され、修飾子 canTransfer() を使用して、msg.sender が承認されているか、トークンの所有者であることを確認します。

    1. 送信者と受信者のアドレスが正当であることを確認した後、clearApproval() 関数を使用して、トークンの元の所​​有者の承認を削除します。つまり、元の所有者には承認権限がなくなります。以前の承認 使用者はトークンを転送できなくなりました。

    2. その後、ERC721 コントラクトの完全な実装では、ERC721 の基本クラス コントラクトの実装と同様に removeTokenFrom() 関数が呼び出され、addTokenTo() 関数が呼び出されて removeTokenFrom() 関数が呼び出されます。基本クラス コントラクトでは、ご覧のとおり、指定されたトークンは、ownedTokensCount マッピング、tokenOwner マッピングから削除されます。

    3. さらに、所有者の ownToken 配列の最後のトークンを、転送されたトークンのインデックス位置に移動する必要があり、配列の長さを 1 つ減らす必要があります。

    4. 最後に、addTokenTo() 関数を使用して、トークンのインデックス/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 コントラクトを使用してトークンを取引できますが、元の ERC721 コントラクトを通じてトークンを取引および転送する機能を持たないコントラクトにトークンが送信された場合、トークンは失われ、取得する方法がありません。それ。この問題に対してERC223が提案され、ERC20をベースにERC223が改良され、このような誤送信が防止されました。

上記の問題を解決するために、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 トークンを処理するための完全なコントラクトではありません。このモジュールは、ERC721Receive 標準インターフェイスが使用されているかどうかを確認するための標準的な方法を提供するために使用されます。ERC721 トークンを処理するには、ウォレットまたはオークション コントラクトでコードを呼び出す ERC721Holder コントラクトを継承または派生する必要があります。エスクロー トークンの場合でも、必要に応じてコントラクト関数を呼び出してトークンをコントラクトから転送するには、このような機能が必要です。

//option 1
function safeTransferFrom(address _from,address _to,uint256 _tokenId) public canTransfer(_tokenId){
	safeTransferFrom(_from,_to,_tokenId,"");
}
//option 2
function 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 を選択してトークンを転送できます。この場合、safeTransferFrom() 関数の呼び出しにパラメーターは必要ありません。パラメーター bytes_data を使用してオプション 2 を選択することもできます。同様に、transferFrom() 関数を使用して、トークンの所有権を from アドレスから to アドレスに転送します。同時に、checkAndCallSafeTransfer() 関数が呼び出され、AddressUtils.sol ライブラリ パッケージを使用して、宛先アドレスがコントラクト アドレスであるかどうかが最初にチェックされます。実装プロセスは、isContract() 関数を通じて理解できます。to がコントラクト アドレスかどうかを確認した後、onERC721Received() の関数シグネチャが期待されるインターフェイスの標準インターフェイスに準拠しているかどうかを確認します。一致しない場合、転送先アドレスのコントラクトが期待されるインターフェイスを実装していないと判断されるため、transferFrom() 関数は取り消されます。

破壊

ERC20の練習

ERC20 規格では、マッピングされた残高のみを操作するため、特定のアドレスのトークンを破棄するだけで済みます。アドレスは、ユーザーまたは契約のアドレスにすることができます。以下の burn() 関数では、破棄されるトークンの数を指定するために value 変数が使用されています。破棄するトークンの所有者は msg.sender で指定されるため、アドレスのバランスを更新し、totalSupply の総供給量を減らす必要があります。ここで Burn と Transfer はイベントです

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 トークンの場合、特定のトークン ID またはインデックスが削除されていることを確認する必要があります。addTokenTo() および mint() 関数と同様に、burn() 関数は super を使用して基本的な ERC721 実装を呼び出します。

1. clearApproval() 関数が最初に呼び出されない

2. 次に、removeTokenFrom() によってトークンの所有権を削除し、Transfer イベントをトリガーしてフロントエンドに通知します。

3. トークンに関連付けられたメタデータを削除します

4. 最後に、トークンの所有権を削除するのと同じように、allTokens 配列を再配置し、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 トークンなど) を与え、トークンを一意にする追加データを各トークンに与えます。列挙可能な拡張機能により、tokenID だけでなくトークンの並べ替えが容易になります。メタデータ エクステンションはオプションであり、メタデータ インターフェイスにより、スマート コントラクトは、名前やその他の詳細など、分離できないトークンに関するメタデータを取得できます。

ここで宣言されているコントラクトは、TokenERC721.sol コントラクトと ERC721Metadata 拡張から継承されたインターフェイスです。

contract TokenERC721Metadata is TokenERC721,ERC721Metadata{

メタデータ エクステンションは、次の 3 つの関数で構成されます。

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 Check
	supportedInterface[
		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);
}

列挙可能な拡張子

  • インターフェース定義
  • 総供給
  • tokenByIndex
  • tokenOfOwnerByIndex
  • からの転送
  • バーントークン
  • issueToken

ERC165規格

ERC165には、コントラクトのフィンガープリントが指定されたインターフェースのフィンガープリントと一致するかどうかを確認するために使用される機能が1つしかありません

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を実現する必要があります

おすすめ

転載: blog.csdn.net/weixin_45976751/article/details/126246500