iT邦幫忙

2022 iThome 鐵人賽

DAY 13
2
Web 3

從以太坊白皮書理解 web 3 概念系列 第 14

從以太坊白皮書理解 web 3 概念 - Day13

  • 分享至 

  • xImage
  •  

從以太坊白皮書理解 web 3 概念 - Day13

Learn Solidity - day 5 ERC721 & Crypto Collectibles

今天將會透過 Lession 5: ERC721 & Crypto Collectibles 來理解 Ethereum 上 token 系統。

在 Ethereum , 最常聽到的 token 就是所謂 ERC20 tokens。

在 Ethereum 上的 token ,代表的是一種 Smart Contract。這種 Smart Contract 符合某種標準介面,必須實作某些特定功能。比如說: transferFrom(address _from, address _to, uint256 _tokenId) 以及 balanceOf(address _owner)

在這些 Smart Contract 內部通常會有一個 mapping(address => uint256) balance ,用來紀錄每個 address 擁有多少 token 。

簡單來說, token 就是一個 Smart Contract 用來紀錄每個使用者具有這個 token ,以及一些轉移 token 的功能。

ERC20 tokens 代表是符合 ERC20 標準介面的 Smart Contract。

token 的作用

透過這些標準介面,每個 Contract 的互動性具有一個標準定義

因此在功能擴充,以及實作上能夠更加的便利

有了 ERC20 標準介面,對想要使用這類 token 的周邊就可以透過這些標準接口去實作功能。

除了 ERC20 之外的標準

對於 Zombie 物件來說

ERC20 標準雖然可以讓 Zombie 像一般貨幣一樣可以交易

然而,卻有一些特性不同

  1. Zombie 無法部份給出,只能整個 Zombie 一次給出
    舉例來說: 可以轉移 0.02 ETH 給別人 ,但無法轉移 0.02 Zombie
  2. 每個 Zombie 都是唯一產出的,非同質性
    我的 level 6 Zombie "json", 與別人的 level 40 Zombie "eddie" 不會相同。

這兩個特性用來做 token 資產剛好可以使用 ERC721 標準

ERC721 標準做出來的 token 是彼此之間是無法取代的

因為每個都是唯一的。你只能以單個買入。

因此只要實作 ERC721 標準介面,就可以保證上面兩個特性。

具體實作細節

  1. 宣告一個 ZombieOwnership Contract
  2. import "./zombieattack.sol";
  3. 讓 ZombieOwnership 繼承 ZombieAttack
pragma solidity >=0.5.0 <0.6.0;

import "./zombieattack.sol";

contract ZombieOwnership is ZombieAttack {

}

ERC721 標準與多重繼承

ERC721 標準

以下是 ERC721 標準介面

contract ERC721 {
  event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
  event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);

  function balanceOf(address _owner) external view returns (uint256);
  function ownerOf(uint256 _tokenId) external view returns (address);
  function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
  function approve(address _approved, uint256 _tokenId) external payable;
}

實作繼承的 token contract

接下來為了讓 ZombieOwnership 有 ERC721 的特性

必須要透過引入 ERC721 的介面

然後必須讓 ZombieOwnership 去繼承 ERC721

但是 ZombieOwnership 原本已經繼承了 ZombieAttack 了

那要如何再繼承 ERC721 呢?

很幸運的是,在 solidity 語言,支援多重繼承語法如下:

contract SatoshiNakamoto is NickSzabo, HalFinney {
    
}

實作步驟

  1. import "./erc721.sol";
  2. 加入繼承 ERC721
pragma solidity >=0.5.0 <0.6.0;

import "./zombieattack.sol";
// Import file here
import "./erc721.sol";
// Declare ERC721 inheritance here
contract ZombieOwnership is ZombieAttack, ERC721 {

}

BalanceOf 與 ownerOf

BalanceOf

 function balanceOf(address _owner) external view returns (uint256 _balance);

這個 function 只會查詢 _owner 的具有多少 token

ownerOf

function ownerOf(uint256 _tokenId) external view returns (address _owner);

這個 function 是查詢 _tokenId 對應到的 owner

具體實作

  1. 實作 balanceOf 回傳 _owner 具有的 zombie 個數
  2. 實作 ownerOf 回傳 _tokenId 對應的 owner
pragma solidity >=0.5.0 <0.6.0;

import "./zombieattack.sol";
import "./erc721.sol";

contract ZombieOwnership is ZombieAttack, ERC721 {

  function balanceOf(address _owner) external view returns (uint256) {
    // 1. Return the number of zombies `_owner` has here
    return ownerZombieCount[_owner];  
  }

  function ownerOf(uint256 _tokenId) external view returns (address) {
    // 2. Return the owner of `_tokenId` here
    return zombieToOwner[_tokenId];
  }

  function transferFrom(address _from, address _to, uint256 _tokenId) external payable {

  }

  function approve(address _approved, uint256 _tokenId) external payable {

  }
}

重構

修正之前寫在 zombiefeeding.sol 寫的 modifier

因為剛好更現在 ERC721 介面的 function ownerOf 同名衝突

所以只能修改 zombiefeeding.sol 寫的 modifier ownerOf 變成 onlyOwnerOf

pragma solidity >=0.5.0 <0.6.0;

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. Change modifier name to `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. Change modifier name here as well
  function feedAndMultiply(uint _zombieId, uint _targetDna, string memory _species) internal onlyOwnerOf(_zombieId) {
    Zombie storage myZombie = zombies[_zombieId];
    require(_isReady(myZombie));
    _targetDna = _targetDna % dnaModulus;
    uint newDna = (myZombie.dna + _targetDna) / 2;
    if (keccak256(abi.encodePacked(_species)) == keccak256(abi.encodePacked("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: Transfer 邏輯

在 ERC721 介面裡,具有兩種不同方法來轉換 token:

  1. transferFrom
  function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

透過 token 持有者呼叫 transferFrom 從自己的 address _from 轉移 _tokenId 到 address _to
2. approve

function approve(address _approved, uint256 _tokenId) external payable;

透過 token 持有者呼叫 approve ,把要轉移的 _tokenId 與要轉給的 address _approved 傳入。此時,contract 會紀錄下誰被允許拿一個 token ,通常是使用 mapping(uint256=> address) 。然後,當接收者 _approved 呼叫了 transferFrom , contract 會去檢查這個呼叫者有沒有在剛剛紀錄的清單內。

具體實作 _transfer

  1. 定義一個 function _transfer,
    需要3個參數: _from(address), _to(address), _tokenId(uint256)
    設定讀取權限是 private
  2. 把 ownerZombieCount[_to]++;
  3. 把 ownerZombieCount[_from]--;
  4. 更新 zombieToOwner[_tokenId] = _to;
  5. emit Transfer 事件。
pragma solidity >=0.5.0 <0.6.0;

import "./zombieattack.sol";
import "./erc721.sol";

contract ZombieOwnership is ZombieAttack, ERC721 {

  function balanceOf(address _owner) external view returns (uint256) {
    return ownerZombieCount[_owner];
  }

  function ownerOf(uint256 _tokenId) external view returns (address) {
    return zombieToOwner[_tokenId];
  }

  // Define _transfer() here
  function _transfer(address _from, address _to, uint256 _tokenId) private {
    ownerZombieCount[_to]++;
    ownerZombieCount[_from]--;
    zombieToOwner[_tokenId] = _to;
    emit Transfer(_from, _to, _tokenId);
  }
  function transferFrom(address _from, address _to, uint256 _tokenId) external payable {

  }

  function approve(address _approved, uint256 _tokenId) external payable {

  }
}

實作 transferFrom

  1. 宣告一個 mapping(uint => address) zombieApprovals
    用來紀錄 approved 過的 address
  2. 新增 require(zombieToOwner[_tokenId]== msg.sender || zombieApprovals[_tokenId] == msg.sender);
    到 transforFrom
  3. 呼叫 _transfer
pragma solidity >=0.5.0 <0.6.0;

import "./zombieattack.sol";
import "./erc721.sol";

contract ZombieOwnership is ZombieAttack, ERC721 {

  // 1. Define mapping here
  mapping (uint => address) zombieApprovals;
  function balanceOf(address _owner) external view returns (uint256) {
    return ownerZombieCount[_owner];
  }

  function ownerOf(uint256 _tokenId) external view returns (address) {
    return zombieToOwner[_tokenId];
  }

  function _transfer(address _from, address _to, uint256 _tokenId) private {
    ownerZombieCount[_to]++;
    ownerZombieCount[_from]--;
    zombieToOwner[_tokenId] = _to;
    emit Transfer(_from, _to, _tokenId);
  }

  function transferFrom(address _from, address _to, uint256 _tokenId) external payable {
    // 2. Add the require statement here
    require(zombieToOwner[_tokenId]== msg.sender || zombieApprovals[_tokenId] == msg.sender);
    // 3. Call _transfer
      _transfer(_from, _to, _tokenId);  
  }

  function approve(address _approved, uint256 _tokenId) external payable {

  }

}

ERC721: Approve

實作 approve

  1. 在 approve function 新增 onlyOwnerOf 的 modifier
  2. 在 approve function ,設定 zombieApprovals[_tokenId] = _approved;
pragma solidity >=0.5.0 <0.6.0;

import "./zombieattack.sol";
import "./erc721.sol";

contract ZombieOwnership is ZombieAttack, ERC721 {

  mapping (uint => address) zombieApprovals;

  function balanceOf(address _owner) external view returns (uint256) {
    return ownerZombieCount[_owner];
  }

  function ownerOf(uint256 _tokenId) external view returns (address) {
    return zombieToOwner[_tokenId];
  }

  function _transfer(address _from, address _to, uint256 _tokenId) private {
    ownerZombieCount[_to]++;
    ownerZombieCount[_from]--;
    zombieToOwner[_tokenId] = _to;
    emit Transfer(_from, _to, _tokenId);
  }

  function transferFrom(address _from, address _to, uint256 _tokenId) external payable {
    require (zombieToOwner[_tokenId] == msg.sender || zombieApprovals[_tokenId] == msg.sender);
    _transfer(_from, _to, _tokenId);
  }

  // 1. Add function modifier here
    function approve(address _approved, uint256 _tokenId) external payable onlyOwnerOf(_tokenId) {
    // 2. Define function here
    zombieApprovals[_tokenId] = _approved;    
  }
}

觸發 Approve Event

  1. 在 approve function , 加入以下
    emit Approval(msg.sender, _approved, _tokenId);
pragma solidity >=0.5.0 <0.6.0;

import "./zombieattack.sol";
import "./erc721.sol";

contract ZombieOwnership is ZombieAttack, ERC721 {

  mapping (uint => address) zombieApprovals;

  function balanceOf(address _owner) external view returns (uint256) {
    return ownerZombieCount[_owner];
  }

  function ownerOf(uint256 _tokenId) external view returns (address) {
    return zombieToOwner[_tokenId];
  }

  function _transfer(address _from, address _to, uint256 _tokenId) private {
    ownerZombieCount[_to]++;
    ownerZombieCount[_from]--;
    zombieToOwner[_tokenId] = _to;
    emit Transfer(_from, _to, _tokenId);
  }

  function transferFrom(address _from, address _to, uint256 _tokenId) external payable {
    require (zombieToOwner[_tokenId] == msg.sender || zombieApprovals[_tokenId] == msg.sender);
    _transfer(_from, _to, _tokenId);
  }

  function approve(address _approved, uint256 _tokenId) external payable onlyOwnerOf(_tokenId) {
    zombieApprovals[_tokenId] = _approved;
    //Fire the Approval event here
    emit Approval(msg.sender, _approved, _tokenId);
  }


}

實作避免 overflow

overflow

在數值儲存超過其位數變數所能代表數值時 就是所謂 overflow

舉例來說: uint8 儲存的最大數值 = 2^8 -1 = 255

uint8 number = 255; // max number value could store
number++; // 超過最大位數就會造成 overflow
// 原本 255 + 1 應該儲存 256 , 但當數值超過最大值時會進位造成變成 0 變成錯誤的數值

使用 SafeMath

為了避免 overflow

使用一個叫作 SafeMath 的 solidity library 可以解決這個問題。

SafeMath 針對 add, sub, mul, div 這些操作做封裝避免 overflow 問題

範例如下:

using SafeMath for uint256;
uint256 a = 5;
uint256 b = a.add(3); // 5+3 = 8
uint256 c = a.mult(2); // 5*2 = 10

更新 Contract

  1. 在 zombiefactory.sol 引用 safemath.sol
  2. 加入宣告 using SafeMath for uint256
pragma solidity >=0.5.0 <0.6.0;

import "./ownable.sol";
// 1. Import here
import "./safemath.sol";
contract ZombieFactory is Ownable {

  // 2. Declare using safemath here
  using SafeMath for uint256;
  event NewZombie(uint zombieId, string name, uint dna);

  uint dnaDigits = 16;
  uint dnaModulus = 10 ** dnaDigits;
  uint cooldownTime = 1 days;

  struct Zombie {
    string name;
    uint dna;
    uint32 level;
    uint32 readyTime;
    uint16 winCount;
    uint16 lossCount;
  }

  Zombie[] public zombies;

  mapping (uint => address) public zombieToOwner;
  mapping (address => uint) ownerZombieCount;

  function _createZombie(string memory _name, uint _dna) internal {
    uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime), 0, 0)) - 1;
    zombieToOwner[id] = msg.sender;
    ownerZombieCount[msg.sender]++;
    emit NewZombie(id, _name, _dna);
  }

  function _generateRandomDna(string memory _str) private view returns (uint) {
    uint rand = uint(keccak256(abi.encodePacked(_str)));
    return rand % dnaModulus;
  }

  function createRandomZombie(string memory _name) public {
    require(ownerZombieCount[msg.sender] == 0);
    uint randDna = _generateRandomDna(_name);
    randDna = randDna - randDna % 100;
    _createZombie(_name, randDna);
  }

}

SafeMath library

SafeMath 的實作內容如下

library SafeMath {

  function mul(uint256 a, uint256 b) internal pure returns (uint256) {
    if (a == 0) {
      return 0;
    }
    uint256 c = a * b;
    assert(c / a == b);
    return c;
  }

  function div(uint256 a, uint256 b) internal pure returns (uint256) {
    // assert(b > 0); // Solidity automatically throws when dividing by 0
    uint256 c = a / b;
    // assert(a == b * c + a % b); // There is no case in which this doesn't hold
    return c;
  }

  function sub(uint256 a, uint256 b) internal pure returns (uint256) {
    assert(b <= a);
    return a - b;
  }

  function add(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a + b;
    assert(c >= a);
    return c;
  }
}

library 是一個特別保留字用來宣告函式庫的。
而 using 則是要使用 library 的關鍵字,透過 using 會自動把型別帶入 library。

範例如下:

using SafeMath for uint;
uint test = 2;
test = test.mul(3); // 6
test = test.add(5); // 6+5 = 11

原本 add 與 mul 需要兩個參數

然而透過 using Safe for uint;

uint 會自動在使用會自動把自己當成第1個參數帶入

而可以看到 add

function add(uint256 a, uint256 b) internal pure returns (uint256) {
  uint256 c = a + b;
  assert(c >= a);
  return c;
}

裏面 assert 類似於 require

當 assert 內判斷式是 false 則會拋出 error

更改 ZombieOwnership 使用 SafeMath

  1. 修改 ++ 為使用 SafeMath add
  2. 修改 ++ 為使用 SafeMath sub
pragma solidity >=0.5.0 <0.6.0;

import "./zombieattack.sol";
import "./erc721.sol";
import "./safemath.sol";

contract ZombieOwnership is ZombieAttack, ERC721 {

  using SafeMath for uint256;

  mapping (uint => address) zombieApprovals;

  function balanceOf(address _owner) external view returns (uint256) {
    return ownerZombieCount[_owner];
  }

  function ownerOf(uint256 _tokenId) external view returns (address) {
    return zombieToOwner[_tokenId];
  }

  function _transfer(address _from, address _to, uint256 _tokenId) private {
    // 1. Replace with SafeMath's `add`
    ownerZombieCount[_to] = ownerZombieCount[_to].add(1);
    // 2. Replace with SafeMath's `sub`
    ownerZombieCount[_from] = ownerZombieCount[_from].sub(1);
    zombieToOwner[_tokenId] = _to;
    emit Transfer(_from, _to, _tokenId);
  }

  function transferFrom(address _from, address _to, uint256 _tokenId) external payable {
    require (zombieToOwner[_tokenId] == msg.sender || zombieApprovals[_tokenId] == msg.sender);
    _transfer(_from, _to, _tokenId);
  }

  function approve(address _approved, uint256 _tokenId) external payable onlyOwnerOf(_tokenId) {
    zombieApprovals[_tokenId] = _approved;
    emit Approval(msg.sender, _approved, _tokenId);
  }

}

修改 zombiefactory.sol 使用 SafeMath

  1. 宣告 using SafeMath32 for uint32;
  2. 宣告 using SafeMath16 for uint16;
  3. 把對應的運算都修正為 SafeMath 運算子
pragma solidity >=0.5.0 <0.6.0;

import "./ownable.sol";
import "./safemath.sol";

contract ZombieFactory is Ownable {

  using SafeMath for uint256;
  // 1. Declare using SafeMath32 for uint32
  using SafeMath32 for uint32;
  // 2. Declare using SafeMath16 for uint16
  using SafeMath16 for uint16;
  event NewZombie(uint zombieId, string name, uint dna);

  uint dnaDigits = 16;
  uint dnaModulus = 10 ** dnaDigits;
  uint cooldownTime = 1 days;

  struct Zombie {
    string name;
    uint dna;
    uint32 level;
    uint32 readyTime;
    uint16 winCount;
    uint16 lossCount;
  }

  Zombie[] public zombies;

  mapping (uint => address) public zombieToOwner;
  mapping (address => uint) ownerZombieCount;

  function _createZombie(string memory _name, uint _dna) internal {
    // Note: We chose not to prevent the year 2038 problem... So don't need
    // worry about overflows on readyTime. Our app is screwed in 2038 anyway ;)
    uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime), 0, 0)) - 1;
    zombieToOwner[id] = msg.sender;
    // 3. Let's use SafeMath's `add` here:
      ownerZombieCount[msg.sender] = ownerZombieCount[msg.sender].add(1);
    emit NewZombie(id, _name, _dna);
  }

  function _generateRandomDna(string memory _str) private view returns (uint) {
    uint rand = uint(keccak256(abi.encodePacked(_str)));
    return rand % dnaModulus;
  }

  function createRandomZombie(string memory _name) public {
    require(ownerZombieCount[msg.sender] == 0);
    uint randDna = _generateRandomDna(_name);
    randDna = randDna - randDna % 100;
    _createZombie(_name, randDna);
  }

}

修正 zombieattack.sol 使用 SafeMath

  1. 把所有使用數值運算子改成 SafeMath
pragma solidity >=0.5.0 <0.6.0;

import "./zombiehelper.sol";

contract ZombieAttack is ZombieHelper {
  uint randNonce = 0;
  uint attackVictoryProbability = 70;

  function randMod(uint _modulus) internal returns(uint) {
    // Here's one!
    randNonce = randNonce.add(1);
    return uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % _modulus;
  }

  function attack(uint _zombieId, uint _targetId) external onlyOwnerOf(_zombieId) {
    Zombie storage myZombie = zombies[_zombieId];
    Zombie storage enemyZombie = zombies[_targetId];
    uint rand = randMod(100);
    if (rand <= attackVictoryProbability) {
      // Here's 3 more!
      myZombie.winCount = myZombie.winCount.add(1);
      myZombie.level = myZombie.level.add(1);
      enemyZombie.lossCount = enemyZombie.lossCount.add(1);
      feedAndMultiply(_zombieId, enemyZombie.dna, "zombie");
    } else {
      // ...annnnd another 2!
       myZombie.lossCount = myZombie.lossCount.add(1);
      enemyZombie.winCount = enemyZombie.winCount.add(1);
      _triggerCooldown(myZombie);
    }
  }
}

註解

在 solidity 中,使用註解的語法跟 javascript 一樣。

單行註解如下

// This is a single-line comment. It's kind of like a note to self (or to others)

多行註解如下

contract CryptoZombies {
  /* This is a multi-lined comment. I'd like to thank all of you
    who have taken your time to try this programming course.
    I know it's free to all of you, and it will stay free
    forever, but we still put our heart and soul into making
    this as good as it can be.

    Know that this is still the beginning of Blockchain development.
    We've come very far but there are so many ways to make this
    community better. If we made a mistake somewhere, you can
    help us out and open a pull request here:
    https://github.com/loomnetwork/cryptozombie-lessons

    Or if you have some ideas, comments, or just want to say
    hi - drop by our Telegram community at https://t.me/loomnetworkdev
  */
}

特別的是,有一種 Solidity 的標準格式註解叫作 natspec

如下:

// @title A contract for basic math operations
/// @author H4XF13LD MORRIS ?????
/// @notice For now, this contract just adds a multiply function
contract Math {
  /// @notice Multiplies 2 numbers together
  /// @param x the first uint.
  /// @param y the second uint.
  /// @return z the product of (x * y)
  /// @dev This function does not currently check for overflows
  function multiply(uint x, uint y) returns (uint z) {
    // This is just a normal comment, and won't get picked up by natspec
    z = x * y;
  }
}

@title 與 @author 個別註解標題與作者
@notice 用來解釋 contract 功能
@dev 解釋實作細節給開發者
@param 註解參數,@return 註解回傳值

幫 ZombieOwnership 加入註解

  1. 新增 @title A contract that manages transfering zombie ownership
  2. 新增 @author 你的名字
  3. 新增 @dev Compliant with OpenZeppelin's implementation of the ERC721 spec draft
pragma solidity >=0.5.0 <0.6.0;

import "./zombieattack.sol";
import "./erc721.sol";
import "./safemath.sol";

/// @title A contract that manages transfering zombie ownership
/// @author gson
/// @dev Compliant with OpenZeppelin's implementation of the ERC721 spec draft
contract ZombieOwnership is ZombieAttack, ERC721 {

  using SafeMath for uint256;

  mapping (uint => address) zombieApprovals;

  function balanceOf(address _owner) external view returns (uint256) {
    return ownerZombieCount[_owner];
  }

  function ownerOf(uint256 _tokenId) external view returns (address) {
    return zombieToOwner[_tokenId];
  }

  function _transfer(address _from, address _to, uint256 _tokenId) private {
    ownerZombieCount[_to] = ownerZombieCount[_to].add(1);
    ownerZombieCount[msg.sender] = ownerZombieCount[msg.sender].sub(1);
    zombieToOwner[_tokenId] = _to;
    emit Transfer(_from, _to, _tokenId);
  }

  function transferFrom(address _from, address _to, uint256 _tokenId) external payable {
    require (zombieToOwner[_tokenId] == msg.sender || zombieApprovals[_tokenId] == msg.sender);
    _transfer(_from, _to, _tokenId);
  }

  function approve(address _approved, uint256 _tokenId) external payable onlyOwnerOf(_tokenId) {
    zombieApprovals[_tokenId] = _approved;
    emit Approval(msg.sender, _approved, _tokenId);
  }

}


上一篇
從以太坊白皮書理解 web 3 概念 - Day12
下一篇
從以太坊白皮書理解 web 3 概念 - Day14
系列文
從以太坊白皮書理解 web 3 概念32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言