從這節開始,我們將學習
代幣
,ERC721
標準, 以及加密收集資產
等知識。
一、代幣
代幣
讓我們來聊聊以太坊上的代幣
。
如果你對以太坊的世界有一些瞭解,你很可能聽過人們聊到代幣——尤其是 ERC20
代幣。
一個 代幣
在以太坊基本上就是一個遵循一些共同規則的智慧合約——即它實現了所有其他代幣合約共享的一組標準函式,例如 transfer(address _to, uint256 _value)
和 balanceOf(address _owner)
.
在智慧合約內部,通常有一個對映, mapping(address => uint256) balances
,用於追蹤每個地址還有多少餘額。
所以基本上一個代幣只是一個追蹤誰擁有多少該代幣的合約,和一些可以讓那些使用者將他們的代幣轉移到其他地址的函式
。
它為什麼重要呢?
由於所有 ERC20 代幣共享具有相同名稱的同一組函式,它們都可以以相同的方式進行互動。
這意味著如果你構建的應用程式能夠與一個 ERC20 代幣進行互動,那麼它就也能夠與任何 ERC20 代幣進行互動。 這樣一來,將來你就可以輕鬆地將更多的代幣新增到你的應用中,而無需進行自定義編碼。 你可以簡單地插入新的代幣合約地址,然後嘩啦,你的應用程式有另一個它可以使用的代幣了。
其中一個例子就是交易所
。 當交易所新增一個新的 ERC20 代幣時,實際上它只需要新增與之對話的另一個智慧合約。 使用者可以讓那個合約將代幣傳送到交易所的錢包地址,然後交易所可以讓合約在使用者要求取款時將代幣傳送回給他們。
交易所只需要實現這種轉移邏輯一次,然後當它想要新增一個新的 ERC20 代幣時,只需將新的合約地址新增到它的資料庫即可。
其他代幣標準
對於像貨幣一樣的代幣來說,ERC20 代幣非常酷。 但是要在我們殭屍遊戲中代表殭屍就並不是特別有用。
首先,殭屍不像貨幣可以分割 —— 我可以發給你 0.237 以太,但是轉移給你 0.237 的殭屍聽起來就有些搞笑。
其次,並不是所有殭屍都是平等的。 你的2級殭屍”Steve”完全不能等同於我732級的殭屍”H4XF13LD MORRIS”。(你差得遠呢,Steve)。
有另一個代幣標準更適合如 CryptoZombies 這樣的加密收藏品——它們被稱為ERC721
代幣.
ERC721代幣
是不能互換的,因為每個代幣都被認為是唯一且不可分割的。 你只能以整個單位交易它們,並且每個單位都有唯一的 ID。 這些特性正好讓我們的殭屍可以用來交易。
請注意,使用像 ERC721 這樣的標準的優勢就是,我們不必在我們的合約中實現拍賣或託管邏輯,這決定了玩家能夠如何交易/出售我們的殭屍。 如果我們符合規範,其他人可以為加密可交易的 ERC721 資產搭建一個交易所平臺,我們的 ERC721 殭屍將可以在該平臺上使用。 所以使用代幣標準相較於使用你自己的交易邏輯有明顯的好處。
實戰演練
我們將在下一章深入討論ERC721的實現。 但首先,讓我們為本課設定我們的檔案結構。
我們將把所有ERC721邏輯儲存在一個叫ZombieOwnership
的合約中。
- 1、在檔案頂部宣告我們pragma的版本(格式參考之前的課程)。
- 2、將
zombieattack.sol import
進來。 - 3、宣告一個繼承
ZombieAttack
的新合約, 命名為ZombieOwnership
。合約的其他部分先留空。
zombieownership.sol
// 從這裡開始
pragma solidity ^0.4.19;
import "./zombieattack.sol";
contract ZombieOwnership is ZombieAttack {
}
二、ERC721標準與多重繼承
讓我們來看一看 ERC721 標準:
contract ERC721 {
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId);
function balanceOf(address _owner) public view returns (uint256 _balance);
function ownerOf(uint256 _tokenId) public view returns (address _owner);
function transfer(address _to, uint256 _tokenId) public;
function approve(address _to, uint256 _tokenId) public;
function takeOwnership(uint256 _tokenId) public;
}
這是我們需要實現的方法列表,我們將在接下來的章節中逐個學習。
雖然看起來很多,但不要被嚇到了!我們在這裡就是準備帶著你一步一步瞭解它們的。
注意: ERC721目前是一個 草稿,還沒有正式商定的實現。在本教程中,我們使用的是 OpenZeppelin 庫中的當前版本,但在未來正式釋出之前它可能會有更改。 所以把這 一個 可能的實現當作考慮,但不要把它作為 ERC721 代幣的官方標準。
實現一個代幣合約
在實現一個代幣合約的時候,我們首先要做的是將介面複製到它自己的 Solidity 檔案並匯入它,import ./erc721.sol
。 接著,讓我們的合約繼承它,然後我們用一個函式定義來重寫每個方法。
但等一下—— ZombieOwnership
已經繼承自 ZombieAttack
了 —— 它如何能夠也繼承於 ERC721
呢?
幸運的是在Solidity,你的合約可以繼承自多個合約,參考如下:
contract SatoshiNakamoto is NickSzabo, HalFinney {
// 嘖嘖嘖,宇宙的奧祕洩露了
}
正如你所見,當使用多重繼承的時候,你只需要用逗號 , 來隔開幾個你想要繼承的合約。在上面的例子中,我們的合約繼承自 NickSzabo 和 HalFinney。
來試試吧。
實戰演練
我們已經在上面為你建立了帶著介面的 erc721.sol 。
- 1、將
erc721.sol
匯入到zombieownership.sol
- 2、宣告
ZombieOwnership
繼承自ZombieAttack
和ERC721
zombieownership.sol
pragma solidity ^0.4.19;
import "./zombieattack.sol";
// 在這裡引入檔案
import "./erc721.sol";
// 在這裡宣告 ERC721 的繼承
contract ZombieOwnership is ZombieAttack, ERC721 {
}
三、 balanceOf和ownerOf
現在,我們來深入討論一下 ERC721
的實現。
我們已經把所有你需要在本課中實現的函式的空殼複製好了。
在本章節,我們將實現頭兩個方法: balanceOf
和 ownerOf
。
balanceOf
function balanceOf(address _owner) public view returns (uint256 _balance);
這個函式只需要一個傳入 address
引數,然後返回這個 address
擁有多少代幣。
在我們的例子中,我們的“代幣”是殭屍。你還記得在我們 DApp 的哪裡儲存了一個主人擁有多少隻殭屍嗎?
ownerOf
function ownerOf(uint256 _tokenId) public view returns (address _owner);
這個函式需要傳入一個代幣 ID
作為引數 (我們的情況就是一個殭屍 ID),然後返回該代幣擁有者的 address
。
同樣的,因為在我們的 DApp 裡已經有一個 mapping
(對映) 儲存了這個資訊,所以對我們來說這個實現非常直接清晰。我們可以只用一行 return
語句來實現這個函式。
注意:要記得, uint256 等同於uint。我們從課程的開始一直在程式碼中使用 uint,但從現在開始我們將在這裡用 uint256,因為我們直接從規範中複製貼上。
實戰演練
我將讓你來決定如何實現這兩個函式。
每個函式的程式碼都應該只有1行 return
語句。看看我們在之前課程中寫的程式碼,想想我們都把這個資料儲存在哪。如果你覺得有困難,你可以點“我要看答案”的按鈕來獲得幫助。
- 1、實現
balanceOf
來返回_owner
擁有的殭屍數量。 - 2、實現
ownerOf
來返回擁有 ID 為_tokenId
殭屍的所有者的地址。
zombieownership.sol
pragma solidity ^0.4.19;
import "./zombieattack.sol";
import "./erc721.sol";
contract ZombieOwnership is ZombieAttack, ERC721 {
function balanceOf(address _owner) public view returns (uint256 _balance) {
// 1. 在這裡返回 `_owner` 擁有的殭屍數
return ownerZombieCount[_owner];
}
function ownerOf(uint256 _tokenId) public view returns (address _owner) {
// 2. 在這裡返回 `_tokenId` 的所有者
return zombieToOwner[_tokenId];
}
function transfer(address _to, uint256 _tokenId) public {
}
function approve(address _to, uint256 _tokenId) public {
}
function takeOwnership(uint256 _tokenId) public {
}
}
四、重構
Hey!我們剛剛的程式碼中其實有個錯誤,以至於其根本無法通過編譯,你發現了沒?
在前一個章節我們定義了一個叫 ownerOf
的函式。但如果你還記得第4課的內容,我們同樣在zombiefeeding.sol
裡以 ownerOf
命名建立了一個 modifier
(修飾符)。
如果你嘗試編譯這段程式碼,編譯器會給你一個錯誤說你不能有相同名稱的修飾符和函式。
所以我們應該把在 ZombieOwnership
裡的函式名稱改成別的嗎?
不,我們不能那樣做!!!要記得,我們正在用 ERC721
代幣標準,意味著其他合約將期望我們的合約以這些確切的名稱來定義函式。這就是這些標準實用的原因——如果另一個合約知道我們的合約符合 ERC721 標準,它可以直接與我們互動,而無需瞭解任何關於我們內部如何實現的細節。
所以,那意味著我們將必須重構我們第4課中的程式碼,將 modifier
的名稱換成別的。
實戰演練
我們回到了 zombiefeeding.sol
。我們將把 modifier
的名稱從 ownerOf
改成 onlyOwnerOf
。
- 1、把修飾符定義中的名稱改成
onlyOwnerOf
- 2、往下滑到使用此修飾符的函式
feedAndMultiply
。我們也需要改這裡的名稱。
注意:我們在 zombiehelper.sol 和 zombieattack.sol 裡也使用了這個修飾符,所以這兩個檔案也必須把名字改了。
zombiefeeding.sol
pragma solidity ^0.4.19;
import "./zombiefactory.sol";
contract KittyInterface {
function getKitty(uint256 _id) external view returns (
bool isGestating,
bool isReady,
uint256 cooldownIndex,
uint256 nextActionAt,
uint256 siringWithId,
uint256 birthTime,
uint256 matronId,
uint256 sireId,
uint256 generation,
uint256 genes
);
}
contract ZombieFeeding is ZombieFactory {
KittyInterface kittyContract;
// 1. 把修飾符名稱改成 `onlyOwnerOf`
modifier onlyOwnerOf(uint _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
_;
}
function setKittyContractAddress(address _address) external onlyOwner {
kittyContract = KittyInterface(_address);
}
function _triggerCooldown(Zombie storage _zombie) internal {
_zombie.readyTime = uint32(now + cooldownTime);
}
function _isReady(Zombie storage _zombie) internal view returns (bool) {
return (_zombie.readyTime <= now);
}
// 2. 這裡也要修改修飾符的名稱
function feedAndMultiply(uint _zombieId, uint _targetDna, string _species) internal onlyOwnerOf(_zombieId) {
Zombie storage myZombie = zombies[_zombieId];
require(_isReady(myZombie));
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
if (keccak256(_species) == keccak256("kitty")) {
newDna = newDna - newDna % 100 + 99;
}
_createZombie("NoName", newDna);
_triggerCooldown(myZombie);
}
function feedOnKitty(uint _zombieId, uint _kittyId) public {
uint kittyDna;
(,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
feedAndMultiply(_zombieId, kittyDna, "kitty");
}
}
五、ERC721轉移標準
現在我們將通過學習把所有權從一個人轉移給另一個人來繼續我們的 ERC721 規範的實現。
注意 ERC721 規範有兩種不同的方法來轉移代幣:
function transfer(address _to, uint256 _tokenId) public;
function approve(address _to, uint256 _tokenId) public;
function takeOwnership(uint256 _tokenId) public;
- 1、第一種方法是代幣的擁有者呼叫
transfer
方法,傳入他想轉移到的address
和他想轉移的代幣的_tokenId
。 - 2、第二種方法是代幣擁有者首先呼叫
approve
,然後傳入與以上相同的引數。接著,該合約會儲存誰被允許提取代幣,通常儲存到一個mapping (uint256 => address)
裡。然後,當有人呼叫takeOwnership
時,合約會檢查msg.sender
是否得到擁有者的批准來提取代幣,如果是,則將代幣轉移給他。
你注意到了嗎,
transfer
和takeOwnership
都將包含相同的轉移邏輯,只是以相反的順序。 (一種情況是代幣的傳送者呼叫函式;另一種情況是代幣的接收者呼叫它)。
所以我們把這個邏輯抽象成它自己的私有函式 _transfer
,然後由這兩個函式來呼叫它。 這樣我們就不用寫重複的程式碼了。
實戰演練
讓我們來定義 _transfer
的邏輯。
- 1、定義一個名為
_transfer
的函式。它會需要3個引數:address _from
、address _to
和uint256 _tokenId
。它應該是一個私有
函式。 - 2、我們有2個對映會在所有權改變的時候改變:
ownerZombieCount
(記錄一個所有者有多少隻殭屍)和zombieToOwner
(記錄什麼人擁有什麼)。 - 我們的函式需要做的第一件事是為 接收 殭屍的人(
address _to
)增 加ownerZombieCount
。使用++
來增加。 - 3、接下來,我們將需要為 傳送 殭屍的人(
address _from
)減少ownerZombieCount
。使用 — 來扣減。 - 4、最後,我們將改變這個
_tokenId
的zombieToOwner
對映,這樣它現在就會指向_to
。 - 5、騙你的,那不是最後一步。我們還需要再做一件事情。
ERC721規範包含了一個 Transfer 事件。這個函式的最後一行應該用正確的引數觸發Transfer ——檢視 erc721.sol 看它期望傳入的引數並在這裡實現。
zombieownership.zol
pragma solidity ^0.4.19;
import "./zombieattack.sol";
import "./erc721.sol";
contract ZombieOwnership is ZombieAttack, ERC721 {
function balanceOf(address _owner) public view returns (uint256 _balance) {
return ownerZombieCount[_owner];
}
function ownerOf(uint256 _tokenId) public view returns (address _owner) {
return zombieToOwner[_tokenId];
}
// 在這裡定義 _transfer()
function _transfer(address _from, address _to, uint256 _tokenId) private {
/*錯誤的寫法
balanceOf(_to)++;
balanceOf(_from)--;
ownerOf(_tokenId);
*/
ownerZombieCount[_to]++;
ownerZombieCount[_from]--;
zombieToOwner[_tokenId] = _to;
Transfer(_from, _to, _tokenId);
}
function transfer(address _to, uint256 _tokenId) public {
}
function approve(address _to, uint256 _tokenId) public {
}
function takeOwnership(uint256 _tokenId) public {
}
}
剛才那是最難的部分——現在實現公共的 transfer
函式應該十分容易,因為我們的 _transfer
函式幾乎已經把所有的重活都幹完了。
實戰演練
- 1、我們想確保只有代幣或殭屍的所有者可以轉移它。還記得我們如何限制只有所有者才能訪問某個功能嗎?
- 沒錯,我們已經有一個修飾符能夠完成這個任務了。所以將修飾符
onlyOwnerOf
新增到這個函式中。 - 2、現在該函式的正文只需要一行程式碼。它只需要呼叫 _transfer。
- 記得把
msg.sender
作為引數傳遞進address _from
。
zombieownership.zol
pragma solidity ^0.4.19;
import "./zombieattack.sol";
import "./erc721.sol";
contract ZombieOwnership is ZombieAttack, ERC721 {
function balanceOf(address _owner) public view returns (uint256 _balance) {
return ownerZombieCount[_owner];
}
function ownerOf(uint256 _tokenId) public view returns (address _owner) {
return zombieToOwner[_tokenId];
}
function _transfer(address _from, address _to, uint256 _tokenId) private {
ownerZombieCount[_to]++;
ownerZombieCount[_from]--;
zombieToOwner[_tokenId] = _to;
Transfer(_from, _to, _tokenId);
}
// 1. 在這裡新增修飾符
function transfer(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
// 2. 在這裡定義方法
_transfer(msg.sender, _to, _tokenId);
}
function approve(address _to, uint256 _tokenId) public {
}
function takeOwnership(uint256 _tokenId) public {
}
}
六、ERC721之批准
現在,讓我們來實現 approve
。
記住,使用 approve
或者 takeOwnership
的時候,轉移有2個步驟:
- 1、你,作為所有者,用新主人的
address
和你希望他獲取的_tokenId
來呼叫approve
- 2、新主人用
_tokenId
來呼叫takeOwnership
,合約會檢查確保他獲得了批准,然後把代幣轉移給他。
因為這發生在2個函式的呼叫中,所以在函式呼叫之間,我們需要一個資料結構來儲存什麼人被批准獲取什麼。
實戰演練
- 1、首先,讓我們來定義一個對映
zombieApprovals
。它應該將一個uint
對映到一個address
。 - 這樣一來,當有人用一個 _tokenId 呼叫 takeOwnership 時,我們可以用這個對映來快速查詢誰被批准獲取那個代幣。
- 2、在函式 approve 上, 我們想要確保只有代幣所有者可以批准某人來獲取代幣。所以我們需要新增修飾符 onlyOwnerOf 到 approve。
- 3、函式的正文部分,將
_tokenId
的zombieApprovals
設定為和_to
相等。 - 4、最後,在 ERC721 規範裡有一個
Approval
事件。所以我們應該在這個函式的最後觸發這個事件。(參考 erc721.sol 來確認傳入的引數,並確保 _owner 是 msg.sender)
zombieownership.zol
pragma solidity ^0.4.19;
import "./zombieattack.sol";
import "./erc721.sol";
contract ZombieOwnership is ZombieAttack, ERC721 {
// 1. 在這裡定義對映
mapping (uint => address) zombieApprovals;
function balanceOf(address _owner) public view returns (uint256 _balance) {
return ownerZombieCount[_owner];
}
function ownerOf(uint256 _tokenId) public view returns (address _owner) {
return zombieToOwner[_tokenId];
}
function _transfer(address _from, address _to, uint256 _tokenId) private {
ownerZombieCount[_to]++;
ownerZombieCount[_from]--;
zombieToOwner[_tokenId] = _to;
Transfer(_from, _to, _tokenId);
}
function transfer(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
_transfer(msg.sender, _to, _tokenId);
}
// 2. 在這裡新增方法修飾符
function approve(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
// 3. 在這裡定義方法
zombieApprovals[_tokenId] = _to;
Approval(msg.sender, _to, _tokenId); // 協議事件
}
function takeOwnership(uint256 _tokenId) public {
}
}
七、ERC721之takeOwnership
現在讓我們完成最後一個函式來結束 ERC721 的實現。
最後一個函式 takeOwnership
, 應該只是簡單地檢查以確保 msg.sender
已經被批准來提取這個代幣或者殭屍。若確認,就呼叫 _transfer
;
實戰演練
- 1、首先,我們要用一個
require
句式來檢查_tokenId
的zombieApprovals
和msg.sender
相等。 - 這樣如果
msg.sender
未被授權來提取這個代幣,將丟擲一個錯誤。 - 2、為了呼叫 _transfer,我們需要知道代幣所有者的地址(它需要一個 _from 來作為引數)。幸運的是我們可以在我們的 ownerOf 函式中來找到這個引數。
- 所以,定義一個名為 owner 的 address 變數,並使其等於 ownerOf(_tokenId)。
- 3、最後,呼叫 _transfer, 並傳入所有必須的引數。(在這裡你可以用 msg.sender 作為 _to, 因為代幣正是要傳送給呼叫這個函式的人)。
注意: 我們完全可以用一行程式碼來實現第2、3兩步。但是分開寫會讓程式碼更易讀。一點個人建議 ?
zombieownership.zol
pragma solidity ^0.4.19;
import "./zombieattack.sol";
import "./erc721.sol";
contract ZombieOwnership is ZombieAttack, ERC721 {
mapping (uint => address) zombieApprovals;
function balanceOf(address _owner) public view returns (uint256 _balance) {
return ownerZombieCount[_owner];
}
function ownerOf(uint256 _tokenId) public view returns (address _owner) {
return zombieToOwner[_tokenId];
}
function _transfer(address _from, address _to, uint256 _tokenId) private {
ownerZombieCount[_to]++;
ownerZombieCount[_from]--;
zombieToOwner[_tokenId] = _to;
Transfer(_from, _to, _tokenId);
}
function transfer(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
_transfer(msg.sender, _to, _tokenId);
}
function approve(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
zombieApprovals[_tokenId] = _to;
Approval(msg.sender, _to, _tokenId);
}
function takeOwnership(uint256 _tokenId) public {
// 從這裡開始
require(zombieApprovals[_tokenId] == msg.sender);
address owner = ownerOf(_tokenId);
_transfer(owner, msg.sender, _tokenId);
}
}