跳至主要内容

Vyper ERC-721 合約逐步解說

Vyper
erc-721
Python
新手
Ori Pomerantz
2021年4月1日
29 分鐘閱讀

介紹

ERC-721 標準是用於持有非同質化代幣 (NFT) 所有權的標準。 ERC-20 代幣的行為類似商品,因為個別代幣之間沒有任何區別。 與之對比,ERC-721 代幣是為相似但不相同的資產所設計,例如不同的貓 卡通或不同房地產的所有權狀。

在本文中,我們將分析 Ryuya Nakamura 的 ERC-721 合約 (opens in a new tab)。 此合約以 Vyper (opens in a new tab) 編寫,這是一種近似 Python 的合約語言,其設計目的是讓編寫不安全的程式碼比在 Solidity 中更困難。

合約

1# @dev ERC-721 非同質化代幣標準的實作。
2# @author Ryuya Nakamura (@nrryuya)
3# 修改自:https://github.com/vyperlang/vyper/blob/de74722bf2d8718cca46902be165f9fe0e3641dd/examples/tokens/ERC721.vy

Vyper 中的註解與 Python 相同,以井字號 (#) 開頭,並一直延續到該行結尾。 包含 @<關鍵字> 的註解會由 NatSpec (opens in a new tab) 用來產生人類可讀的文件。

1from vyper.interfaces import ERC721
2
3implements: ERC721

ERC-721 介面內建於 Vyper 語言中。 你可以在這裡看到程式碼定義 (opens in a new tab)。 介面定義是以 Python 而非 Vyper 編寫的,因為介面不僅在區塊鏈內部使用,也用於從可能以 Python 編寫的外部用戶端向區塊鏈傳送交易。

第一行匯入介面,第二行則指明我們在此處實作該介面。

ERC721Receiver 介面

1# safeTransferFrom() 呼叫的合約介面
2interface ERC721Receiver:
3 def onERC721Received(

ERC-721 支援兩種轉移類型:

  • transferFrom,它讓傳送方可以指定任何目標地址,並將轉移的責任歸於傳送方。 這意味著你可以轉移到無效地址,在這種情況下,NFT 將會永久遺失。
  • safeTransferFrom,它會檢查目標地址是否為合約。 若是,ERC-721 合約會詢問接收合約是否願意接收該 NFT。

為了回應 safeTransferFrom 的請求,接收合約必須實作 ERC721Receiver

1 _operator: address,
2 _from: address,

_from 地址是代幣的目前擁有者。 _operator 地址是請求轉移的地址 (由於有授權額度的關係,這兩個地址可能不相同)。

1 _tokenId: uint256,

ERC-721 代幣 ID 是 256 位元。 一般而言,它們是透過對代幣所代表之物的描述進行哈希運算而建立的。

1 _data: Bytes[1024]

此請求最多可包含 1024 位元組的使用者資料。

1 ) -> bytes32: view

為防止合約意外接受轉移的情況,傳回值不是布林值,而是帶有特定值的 256 位元數值。

此函式為 view 函式,代表它可以讀取區塊鏈的狀態,但不能修改它。

Events

事件 (opens in a new tab) 會被發出以通知區塊鏈外部的使用者和伺服器發生的事件。 請注意,事件的內容對於區塊鏈上的合約是不可用的。

1# @dev 當任何 NFT 的所有權因任何機制發生變更時發出。當 NFT 被
2# 建立 (`from` == 0) 和銷毀 (`to` == 0) 時會發出此事件。例外:在合約建立期間,可以
3# 建立並指派任意數量的 NFT 而不發出 Transfer 事件。在任何
4# 轉移時,該 NFT 的核准地址 (若有) 會被重設為無。
5# @param _from NFT 的傳送方 (若地址為零地址,則表示代幣建立)。
6# @param _to NFT 的接收方 (若地址為零地址,則表示代幣銷毀)。
7# @param _tokenId 已轉移的 NFT。
8event Transfer:
9 sender: indexed(address)
10 receiver: indexed(address)
11 tokenId: indexed(uint256)
顯示全部

這與 ERC-20 的 Transfer 事件相似,差別在於我們回報的是 tokenId 而非數量。 沒有人擁有零地址,因此按照慣例,我們用它來回報代幣的建立和銷毀。

1# @dev 當一個 NFT 的核准地址被變更或再次確認時發出。零
2# 地址表示沒有核准地址。當 Transfer 事件發出時,這也
3# 表示該 NFT 的核准地址 (若有) 被重設為無。
4# @param _owner NFT 的擁有者。
5# @param _approved 我們正在核准的地址。
6# @param _tokenId 我們正在核准的 NFT。
7event Approval:
8 owner: indexed(address)
9 approved: indexed(address)
10 tokenId: indexed(uint256)
顯示全部

ERC-721 的核准類似於 ERC-20 的授權額度。 一個特定的地址被允許轉移一個特定的代幣。 這提供了一種機制,讓合約在接受代幣時能夠做出回應。 合約無法監聽事件,所以如果你只是將代幣轉移給它們,它們不會「知道」這件事。 這樣一來,擁有者首先提交一項核准,然後向合約傳送請求:"我已核准你轉移代幣 X,請執行..."。

這是一項設計上的選擇,目的是讓 ERC-721 標準與 ERC-20 標準相似。 由於 ERC-721 代幣並非同質化代幣,合約也可以透過查看代幣的所有權來識別它是否取得了特定的代幣。

1# @dev 當擁有者的操作員被啟用或停用時發出。操作員可以管理
2# 該擁有者的所有 NFT。
3# @param _owner NFT 的擁有者。
4# @param _operator 我們要為其設定操作員權限的地址。
5# @param _approved 操作員權限的狀態 (true 表示授予操作員權限,false 表示
6# 撤銷)。
7event ApprovalForAll:
8 owner: indexed(address)
9 operator: indexed(address)
10 approved: bool
顯示全部

有時候,有一個可以管理某帳戶所有特定類型代幣(由特定合約管理的代幣)的_操作員_是很有用的,這類似於授權書。 例如,我可能會想將此權力賦予一個合約,讓它檢查我是否六個月未與其聯繫,若是,則將我的資產分配給我的繼承人 (前提是其中一位繼承人提出請求,因為合約若無交易呼叫則無法執行任何動作)。 在 ERC-20 中,我們可以只給予繼承合約一個高額的授權額度,但這在 ERC-721 中行不通,因為其代幣並非同質化的。 這就是等效的做法。

approved 值告訴我們該事件是進行核准,還是撤銷核准。

狀態變數

這些變數包含代幣的目前狀態:哪些是可用的以及誰擁有它們。 這些變數大部分是 HashMap 物件,是存在於兩種類型之間的單向映射 (opens in a new tab)

1# @dev 從 NFT ID 到其擁有者地址的映射。
2idToOwner: HashMap[uint256, address]
3
4# @dev 從 NFT ID 到核准地址的映射。
5idToApprovals: HashMap[uint256, address]

在以太坊中,使用者和合約的身分由 160 位元的地址表示。 這兩個變數從代幣 ID 映射到其擁有者以及被核准轉移它們的人 (每個代幣最多一個)。 在以太坊中,未初始化的資料始終為零,所以如果沒有擁有者或核准的轉移者,該代幣的值就是零。

1# @dev 從擁有者地址到其代幣數量的映射。
2ownerToNFTokenCount: HashMap[address, uint256]

此變數持有每個擁有者的代幣數量。 由於沒有從擁有者到代幣的映射,所以識別特定擁有者所擁有的代幣的唯一方法是回顧區塊鏈的事件歷史,並查看相應的 Transfer 事件。 我們可以使用這個變數來知道我們何時擁有了所有的 NFT,而不需要再回溯更早的時間。

請注意,此演算法僅適用於使用者介面和外部伺服器。 在區塊鏈上執行的程式碼本身無法讀取過去的事件。

1# @dev 從擁有者地址到操作員地址映射的映射。
2ownerToOperators: HashMap[address, HashMap[address, bool]]

一個帳戶可能有多個操作員。 一個簡單的 HashMap 並不足以追蹤它們,因為每個鍵只會對應到一個值。 取而代之,你可以使用 HashMap[address, bool] 作為值。 預設情況下,每個地址的值為 False,這意味著它不是操作員。 你可以視需要將值設定為 True

1# @dev 鑄幣者的地址,可以鑄造代幣
2minter: address

新代幣必須以某種方式被建立出來。 在此合約中,只有一個實體被允許這麼做,即 minter (鑄幣者)。 例如,對於一個遊戲來說,這可能就足夠了。 對於其他目的,可能需要建立更複雜的商業邏輯。

1# @dev 從介面 ID 到布林值的映射,代表是否支援該介面
2supportedInterfaces: HashMap[bytes32, bool]
3
4# @dev ERC165 的 ERC165 介面 ID
5ERC165_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000001ffc9a7
6
7# @dev ERC721 的 ERC165 介面 ID
8ERC721_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000080ac58cd

ERC-165 (opens in a new tab) 規定了一種機制,讓合約能夠揭露應用程式可以如何與其通訊,以及它遵循哪些 ERC 標準。 在此例中,合約遵循 ERC-165 和 ERC-721。

函數

這些是實際實作 ERC-721 的函式。

建構函式

1@external
2def __init__():

在 Vyper 中,與 Python 相同,建構函式稱為 __init__

1 """
2 @dev 合約建構函式。
3 """

在 Python 和 Vyper 中,你也可以透過指定一個多行字串 (以 """ 開頭和結尾) 且不以任何方式使用它來建立註解。 這些註解也可以包含 NatSpec (opens in a new tab)

1 self.supportedInterfaces[ERC165_INTERFACE_ID] = True
2 self.supportedInterfaces[ERC721_INTERFACE_ID] = True
3 self.minter = msg.sender

若要存取狀態變數,請使用 self.<變數名稱> (同樣,這和 Python 一樣)。

檢視函式

這些函式不會修改區塊鏈的狀態,因此如果從外部呼叫它們,可以免費執行。 如果檢視函式是由合約呼叫的,它們仍然必須在每個節點上執行,因此會產生 gas 費用。

1@view
2@external

在函式定義之前,以 at 符號 (@) 開頭的這些關鍵字稱為_裝飾器_。 它們指定了可以呼叫函式的條件。

  • @view 指定此函式為檢視函式。
  • @external 指定此特定函式可由交易和其他合約呼叫。
1def supportsInterface(_interfaceID: bytes32) -> bool:

與 Python 不同,Vyper 是一種靜態型別語言 (opens in a new tab)。 若不指定資料類型 (opens in a new tab),則無法宣告變數或函式參數。 在此例中,輸入參數為 bytes32,一個 256 位元的值 (256 位元是 以太坊虛擬機 的原生字組大小)。 輸出是一個布林值。 按照慣例,函式參數的名稱以下底線 (_) 開頭。

1 """
2 @dev 介面識別在 ERC-165 中指定。
3 @param _interfaceID 介面的 ID
4 """
5 return self.supportedInterfaces[_interfaceID]

self.supportedInterfaces HashMap 傳回值,該值在建構函式 (__init__) 中設定。

1### 檢視函式 ###
2

這些是讓使用者和其他合約能取得代幣相關資訊的檢視函式。

1@view
2@external
3def balanceOf(_owner: address) -> uint256:
4 """
5 @dev 傳回 `_owner` 擁有的 NFT 數量。
6 如果 `_owner` 是零地址,則會擲出錯誤。指派給零地址的 NFT 被視為無效。
7 @param _owner 要查詢餘額的地址。
8 """
9 assert _owner != ZERO_ADDRESS
顯示全部

此行斷言 (opens in a new tab)_owner 不為零。 如果為零,則會出現錯誤並且操作將被還原。

1 return self.ownerToNFTokenCount[_owner]
2
3@view
4@external
5def ownerOf(_tokenId: uint256) -> address:
6 """
7 @dev 傳回 NFT 擁有者的地址。
8 如果 `_tokenId` 不是有效的 NFT,則會擲出錯誤。
9 @param _tokenId NFT 的識別碼。
10 """
11 owner: address = self.idToOwner[_tokenId]
12 # 如果 `_tokenId` 不是有效的 NFT,則會擲出錯誤
13 assert owner != ZERO_ADDRESS
14 return owner
顯示全部

在以太坊虛擬機 (evm) 中,任何沒有儲存值的儲存空間都為零。 如果 _tokenId 沒有代幣,則 self.idToOwner[_tokenId] 的值為零。 在這種情況下,函式會還原。

1@view
2@external
3def getApproved(_tokenId: uint256) -> address:
4 """
5 @dev 取得單一 NFT 的核准地址。
6 如果 `_tokenId` 不是有效的 NFT,則會擲出錯誤。
7 @param _tokenId 要查詢核准的 NFT ID。
8 """
9 # 如果 `_tokenId` 不是有效的 NFT,則會擲出錯誤
10 assert self.idToOwner[_tokenId] != ZERO_ADDRESS
11 return self.idToApprovals[_tokenId]
顯示全部

注意,getApproved _可以_傳回零。 如果代幣有效,它會傳回 self.idToApprovals[_tokenId]。 如果沒有核准者,該值為零。

1@view
2@external
3def isApprovedForAll(_owner: address, _operator: address) -> bool:
4 """
5 @dev 檢查 `_operator` 是否為 `_owner` 的核准操作員。
6 @param _owner 擁有 NFT 的地址。
7 @param _operator 代表擁有者行事的地址。
8 """
9 return (self.ownerToOperators[_owner])[_operator]
顯示全部

此函式檢查 _operator 是否被允許管理此合約中 _owner 的所有代幣。 因為可以有多個操作員,這是一個兩層的 HashMap。

轉移輔助函式

這些函式實作了轉移或管理代幣的部分操作。

1
2### 轉移函式輔助工具 ###
3
4@view
5@internal

這個裝飾器 @internal 表示該函式只能在同一個合約內的其他函式中存取。 按照慣例,這些函式名稱也以下底線 (_) 開頭。

1def _isApprovedOrOwner(_spender: address, _tokenId: uint256) -> bool:
2 """
3 @dev 傳回給定的花費者是否可以轉移給定的代幣 ID
4 @param spender 要查詢的花費者地址
5 @param tokenId 要轉移的代幣的 uint256 ID
6 @return bool 表示 msg.sender 是否被核准用於給定的代幣 ID、
7 是否為擁有者的操作員,或是否為代幣的擁有者
8 """
9 owner: address = self.idToOwner[_tokenId]
10 spenderIsOwner: bool = owner == _spender
11 spenderIsApproved: bool = _spender == self.idToApprovals[_tokenId]
12 spenderIsApprovedForAll: bool = (self.ownerToOperators[owner])[_spender]
13 return (spenderIsOwner or spenderIsApproved) or spenderIsApprovedForAll
顯示全部

有三種方式可以讓一個地址被允許轉移代幣:

  1. 該地址是代幣的擁有者
  2. 該地址被核准花費該代幣
  3. 該地址是代幣擁有者的操作員

上面的函式可以是檢視函式,因為它不會改變狀態。 為了降低操作成本,任何_可以_成為檢視函式的函式都_應該_是檢視函式。

1@internal
2def _addTokenTo(_to: address, _tokenId: uint256):
3 """
4 @dev 將 NFT 新增到給定地址
5 如果 `_tokenId` 已被他人擁有,則會擲出錯誤。
6 """
7 # 如果 `_tokenId` 已被他人擁有,則會擲出錯誤
8 assert self.idToOwner[_tokenId] == ZERO_ADDRESS
9 # 變更擁有者
10 self.idToOwner[_tokenId] = _to
11 # 變更計數追蹤
12 self.ownerToNFTokenCount[_to] += 1
13
14
15@internal
16def _removeTokenFrom(_from: address, _tokenId: uint256):
17 """
18 @dev 從給定地址移除 NFT
19 如果 `_from` 不是目前的擁有者,則會擲出錯誤。
20 """
21 # 如果 `_from` 不是目前的擁有者,則會擲出錯誤
22 assert self.idToOwner[_tokenId] == _from
23 # 變更擁有者
24 self.idToOwner[_tokenId] = ZERO_ADDRESS
25 # 變更計數追蹤
26 self.ownerToNFTokenCount[_from] -= 1
顯示全部

當轉移出現問題時,我們會還原該呼叫。

1@internal
2def _clearApproval(_owner: address, _tokenId: uint256):
3 """
4 @dev 清除給定地址的核准
5 如果 `_owner` 不是目前的擁有者,則會擲出錯誤。
6 """
7 # 如果 `_owner` 不是目前的擁有者,則會擲出錯誤
8 assert self.idToOwner[_tokenId] == _owner
9 if self.idToApprovals[_tokenId] != ZERO_ADDRESS:
10 # 重設核准
11 self.idToApprovals[_tokenId] = ZERO_ADDRESS
顯示全部

只有在必要時才變更值。 狀態變數存在儲存空間中。 寫入儲存空間是 EVM (以太坊虛擬機) 執行成本最昂貴的操作之一 (以 gas 費用計算)。 因此,最好盡量減少寫入,即使寫入現有值也具有高成本。

1@internal
2def _transferFrom(_from: address, _to: address, _tokenId: uint256, _sender: address):
3 """
4 @dev 執行 NFT 的轉移。
5 除非 `msg.sender` 是目前的擁有者、授權的操作員或此 NFT 的核准
6 地址,否則會擲出錯誤。(注意:私有函式中不允許 `msg.sender`,所以傳入 `_sender`。)
7 如果 `_to` 是零地址,則會擲出錯誤。
8 如果 `_from` 不是目前的擁有者,則會擲出錯誤。
9 如果 `_tokenId` 不是有效的 NFT,則會擲出錯誤。
10 """
顯示全部

我們有這個內部函式,因為有兩種轉移代幣的方式 (常規和安全),但我們希望只在程式碼中的一個位置執行它,以便於審核。

1 # 檢查需求
2 assert self._isApprovedOrOwner(_sender, _tokenId)
3 # 如果 `_to` 是零地址,則會擲出錯誤
4 assert _to != ZERO_ADDRESS
5 # 清除核准。如果 `_from` 不是目前的擁有者,則會擲出錯誤
6 self._clearApproval(_from, _tokenId)
7 # 移除 NFT。如果 `_tokenId` 不是有效的 NFT,則會擲出錯誤
8 self._removeTokenFrom(_from, _tokenId)
9 # 新增 NFT
10 self._addTokenTo(_to, _tokenId)
11 # 記錄轉移
12 log Transfer(_from, _to, _tokenId)
顯示全部

若要在 Vyper 中發出事件,請使用 log 陳述式 (在此處查看更多詳細資訊 (opens in a new tab))。

轉移函式

1
2### 轉移函式 ###
3
4@external
5def transferFrom(_from: address, _to: address, _tokenId: uint256):
6 """
7 @dev 除非 `msg.sender` 是目前的擁有者、授權的操作員或此 NFT 的核准
8 地址,否則會擲出錯誤。
9 如果 `_from` 不是目前的擁有者,則會擲出錯誤。
10 如果 `_to` 是零地址,則會擲出錯誤。
11 如果 `_tokenId` 不是有效的 NFT,則會擲出錯誤。
12 @notice 呼叫者有責任確認 `_to` 能夠接收 NFT,否則
13 它們可能會永久遺失。
14 @param _from NFT 的目前擁有者。
15 @param _to 新的擁有者。
16 @param _tokenId 要轉移的 NFT。
17 """
18 self._transferFrom(_from, _to, _tokenId, msg.sender)
顯示全部

此函式可讓你轉移到任意地址。 除非該地址是使用者,或是知道如何轉移代幣的合約,否則你轉移的任何代幣都會卡在該地址中而無法使用。

1@external
2def safeTransferFrom(
3 _from: address,
4 _to: address,
5 _tokenId: uint256,
6 _data: Bytes[1024]=b""
7 ):
8 """
9 @dev 將 NFT 的所有權從一個地址轉移到另一個地址。
10 除非 `msg.sender` 是目前的擁有者、授權的操作員或此
11 NFT 的核准地址,否則會擲出錯誤。
12 如果 `_from` 不是目前的擁有者,則會擲出錯誤。
13 如果 `_to` 是零地址,則會擲出錯誤。
14 如果 `_tokenId` 不是有效的 NFT,則會擲出錯誤。
15 如果 `_to` 是智慧合約,它會在 `_to` 上呼叫 `onERC721Received`,如果
16 傳回值不是 `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`,則會擲出錯誤。
17 注意:bytes4 由帶有填充的 bytes32 表示
18 @param _from NFT 的目前擁有者。
19 @param _to 新的擁有者。
20 @param _tokenId 要轉移的 NFT。
21 @param _data 額外資料,無指定格式,在對 `_to` 的呼叫中傳送。
22 """
23 self._transferFrom(_from, _to, _tokenId, msg.sender)
顯示全部

先執行轉移是沒問題的,因為如果出現問題,我們無論如何都會還原,所以在呼叫中完成的所有事情都將被取消。

1 if _to.is_contract: # 檢查 `_to` 是否為合約地址

首先檢查該地址是否為合約 (如果它有程式碼)。 如果不是,則假設它是一個使用者地址,使用者將能夠使用或轉移該代幣。 但不要讓它讓你產生虛假的安全感。 即使使用 safeTransferFrom,如果你將代幣轉移到一個沒人知道私鑰的地址,你仍然可能會遺失代幣。

1 returnValue: bytes32 = ERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data)

呼叫目標合約,看它是否能接收 ERC-721 代幣。

1 # 如果轉移目標是不實作 'onERC721Received' 的合約,則會擲出錯誤
2 assert returnValue == method_id("onERC721Received(address,address,uint256,bytes)", output_type=bytes32)

如果目標是一個合約,但不接受 ERC-721 代幣 (或決定不接受此特定轉移),則還原。

1@external
2def approve(_approved: address, _tokenId: uint256):
3 """
4 @dev 設定或再次確認 NFT 的核准地址。零地址表示沒有核准地址。
5 除非 `msg.sender` 是目前的 NFT 擁有者,或目前擁有者的授權操作員,否則會擲出錯誤。
6 如果 `_tokenId` 不是有效的 NFT,則會擲出錯誤。(注意:這未在 EIP 中撰寫)
7 如果 `_approved` 是目前的擁有者,則會擲出錯誤。(注意:這未在 EIP 中撰寫)
8 @param _approved 要為給定 NFT ID 核准的地址。
9 @param _tokenId 要核准的代幣 ID。
10 """
11 owner: address = self.idToOwner[_tokenId]
12 # 如果 `_tokenId` 不是有效的 NFT,則會擲出錯誤
13 assert owner != ZERO_ADDRESS
14 # 如果 `_approved` 是目前的擁有者,則會擲出錯誤
15 assert _approved != owner
顯示全部

按照慣例,如果你不想有核准者,你應該指定零地址,而不是你自己。

1 # 檢查需求
2 senderIsOwner: bool = self.idToOwner[_tokenId] == msg.sender
3 senderIsApprovedForAll: bool = (self.ownerToOperators[owner])[msg.sender]
4 assert (senderIsOwner or senderIsApprovedForAll)

要設定核准,你可以是擁有者,也可以是擁有者授權的操作員。

1 # 設定核准
2 self.idToApprovals[_tokenId] = _approved
3 log Approval(owner, _approved, _tokenId)
4
5
6@external
7def setApprovalForAll(_operator: address, _approved: bool):
8 """
9 @dev 啟用或停用第三方 (「操作員」) 管理 `msg.sender`
10 所有資產的核准。它也會發出 ApprovalForAll 事件。
11 如果 `_operator` 是 `msg.sender`,則會擲出錯誤。(注意:這未在 EIP 中撰寫)
12 @notice 即使傳送方當時沒有任何代幣,此函式也有效。
13 @param _operator 要新增到授權操作員集合的地址。
14 @param _approved 如果操作員被核准則為 True,若要撤銷核准則為 false。
15 """
16 # 如果 `_operator` 是 `msg.sender`,則會擲出錯誤
17 assert _operator != msg.sender
18 self.ownerToOperators[msg.sender][_operator] = _approved
19 log ApprovalForAll(msg.sender, _operator, _approved)
顯示全部

鑄造新代幣和銷毀現有代幣

建立合約的帳戶是 minter (鑄幣者),即有權鑄造新 NFT 的超級使用者。 然而,即使是鑄幣者也不被允許銷毀現有的代幣。 只有擁有者或由擁有者授權的實體才能這麼做。

1### 鑄幣與銷毀函式 ###
2
3@external
4def mint(_to: address, _tokenId: uint256) -> bool:

此函式始終傳回 True,因為如果操作失敗,它將會被還原。

1 """
2 @dev 鑄造代幣的函式
3 如果 `msg.sender` 不是鑄幣者,則會擲出錯誤。
4 如果 `_to` 是零地址,則會擲出錯誤。
5 如果 `_tokenId` 已被他人擁有,則會擲出錯誤。
6 @param _to 將接收鑄造代幣的地址。
7 @param _tokenId 要鑄造的代幣 id。
8 @return 一個布林值,表示操作是否成功。
9 """
10 # 如果 `msg.sender` 不是鑄幣者,則會擲出錯誤
11 assert msg.sender == self.minter
顯示全部

只有鑄幣者 (建立 ERC-721 合約的帳戶) 才能鑄造新代幣。 如果我們未來想變更鑄幣者的身分,這可能會成為一個問題。 在一個生產合約中,你可能需要一個函式,允許鑄幣者將鑄幣權限轉移給其他人。

1 # 如果 `_to` 是零地址,則會擲出錯誤
2 assert _to != ZERO_ADDRESS
3 # 新增 NFT。如果 `_tokenId` 已被他人擁有,則會擲出錯誤
4 self._addTokenTo(_to, _tokenId)
5 log Transfer(ZERO_ADDRESS, _to, _tokenId)
6 return True

按照慣例,鑄造新代幣被視為從零地址轉出。

1
2@external
3def burn(_tokenId: uint256):
4 """
5 @dev 銷毀特定的 ERC721 代幣。
6 除非 `msg.sender` 是目前的擁有者、授權的操作員或此 NFT 的核准
7 地址,否則會擲出錯誤。
8 如果 `_tokenId` 不是有效的 NFT,則會擲出錯誤。
9 @param _tokenId 要銷毀的 ERC721 代幣的 uint256 id。
10 """
11 # 檢查需求
12 assert self._isApprovedOrOwner(msg.sender, _tokenId)
13 owner: address = self.idToOwner[_tokenId]
14 # 如果 `_tokenId` 不是有效的 NFT,則會擲出錯誤
15 assert owner != ZERO_ADDRESS
16 self._clearApproval(owner, _tokenId)
17 self._removeTokenFrom(owner, _tokenId)
18 log Transfer(owner, ZERO_ADDRESS, _tokenId)
顯示全部

任何被允許轉移代幣的人都可以銷毀它。 雖然銷毀看起來等同於轉移到零地址,但零地址實際上並不會收到代幣。 這讓我們可以釋放用於該代幣的所有儲存空間,這可以降低交易的 gas 成本。

使用此合約

與 Solidity 不同,Vyper 沒有繼承機制。 這是一項刻意的設計選擇,旨在讓程式碼更清晰,從而更容易確保其安全性。 因此,若要建立自己的 Vyper ERC-721 合約,你可以拿取此合約 (opens in a new tab)並修改它,以實作你想要的商業邏輯。

結論

為了複習,以下是此合約中一些最重要的概念:

  • 要透過安全轉移接收 ERC-721 代幣,合約必須實作 ERC721Receiver 介面。
  • 即使你使用安全轉移,如果你將代幣傳送到私鑰未知的地址,代幣仍然可能會卡住。
  • 當操作出現問題時,最好是 revert (還原) 該呼叫,而不是只傳回一個失敗值。
  • ERC-721 代幣在其有擁有者時存在。
  • 有三種方式可以獲得轉移 NFT 的授權。 你可以是擁有者、被核准用於特定代幣,或是擁有者所有代幣的操作員。
  • 過去的事件只有在區塊鏈外部才可見。 在區塊鏈內部執行的程式碼無法檢視它們。

現在去實作安全的 Vyper 合約吧。

在此查看我的更多作品 (opens in a new tab)

頁面最後更新時間: 2025年8月22日

這個使用教學對你有幫助嗎?