第4回に引き続きSolidity入門基礎編を進めていきます。
今回はERC721についての学習。
ERC721は話題になってるNFT(NON-FUNGIBLE TOKEN)のトークン規格ですね。
前回のはこちら。
Solidity入門基礎編【4】 – CryptoZombiesで学習 –
初めての方はこちらから。
Solidity入門基礎編【1】 – CryptoZombiesで学習 –
始めるまえにこちらへアクセスしてください。
https://cryptozombies.io/jp/lesson/5/chapter/1
トークン規格について少し触れておくと、
ERC20というのが有名なトークン規格で、イーサリアムとかはこの規格で作られている。
しかし、NFTの規格がERC721と別になっている。
例えばイーサリアムの場合は、0.01ethとか1以下の値でも機能するけど、NFTのアート1点を分割するわけにいかない。
”1点モノ”を表す専用のトークン規格がERC721というわけですね。
ではチャプターに入っていきましょう。
目次
- 1 チャプター 1: イーサリアム上トークン
- 2 チャプター 2: ERC721規格と多重継承
- 3 チャプター 3: balanceOf と ownerOf
- 4 チャプター 4: リファクタリング
- 5 チャプター 5: ERC721: トランスファーのロジック
- 6 チャプター 6: ERC721: トランスファーの続き
- 7 チャプター 7: ERC721: Approve
- 8 チャプター 8: ERC721: takeOwnership
- 9 チャプター 9: オーバーフロー対策
- 10 チャプター 10: SafeMathパート2
- 11 チャプター 11: SafeMathパート3
- 12 チャプター 12: SafeMathパート4
- 13 チャプター 13: コメント
チャプター 1: イーサリアム上トークン
バージョンとコントラクトの記述までなのでさくっと。
pragma solidity ^0.4.19; import "./zombieattack.sol"; contract ZombieOwnership is ZombieAttack { }
チャプター 2: ERC721規格と多重継承
今まで継承は1つのコントラクトのみでしたが、今回は複数のコントラクトを継承するというところを学びます。
書き方は簡単で、こんな感じでカンマ区切りにして追加していくだけで良い。
contract コントラクト名 継承コントラクトA, 継承コントラクトB, 継承コントラクトC {}
pragma solidity ^0.4.19; import "./zombieattack.sol"; // ァイルをインポート import "./erc721.sol"; // ERC721の継承を宣言 // ZombieAttackの右隣にカンマで区切って追加 contract ZombieOwnership is ZombieAttack, ERC721 { }
チャプター 3: balanceOf と ownerOf
balanceOf と ownerOfを実装する。
balanceOfは、オーナーのトークン保有量を返す関数(つまり所有しているゾンビ数)
function balanceOf(address _owner) public view returns (uint256 _balance);
ownerOfは、balanceOfの逆で対象のトークン(ゾンビ)はどのオーナーが持っているかを返す関数
function ownerOf(uint256 _tokenId) public view returns (address _owner);
※uint256はuintと同じ
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) { // `_owner`がもつゾンビの数を返す return ownerZombieCount[_owner]; } function ownerOf(uint256 _tokenId) public view returns (address _owner) { // `_tokenId`の所有者をここで返す return zombieToOwner[_tokenId]; } function transfer(address _to, uint256 _tokenId) public { } function approve(address _to, uint256 _tokenId) public { } function takeOwnership(uint256 _tokenId) public { } }
チャプター 4: リファクタリング
リファクタリングとは内部を整理することを指します。
このチャプターのお題として出されているのは、ownerOfという関数が重複しているという問題を解決せよというもの。
ownerOfという関数なんてあるっけ?と思うかと思いますが、少し前のレッスンでmodifierにてownerOfというのを使ってしまってるんですね。
それぞれ、今までのコントラクトは継承してきているので全て繋がっています。
なので、同じ関数名は使えません。
で、新たにつくるERC721の関数名を変えればいいじゃん!と思うかと思いますが、(思うかと思いますがって変ですね)
トークンの規格なのでホイホイ変えてはダメなんです。ルールに則ってつくろうというのが規格ですから。
ということは、modifierの方を変えましょうということで、リファクタリング(内部整理)を今回はします。
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; // 修飾詞名をownerOf⇒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); } // 修飾詞名をownerOf⇒onlyOwnerOfへ変更 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"); } }
チャプター 5: ERC721: トランスファーのロジック
今回のチャプターではNFTを受け渡しする関数を実装する。
流れとしては、売り手から買い手へ送るので
買い手の所有数がプラス1→売り手の所有数がマイナス1→イベントを送信(記録)
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 { // 買い手の所有数が+1 ownerZombieCount[_to] ++; // 売り手の所有数が-1 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 { } }
チャプター 6: ERC721: トランスファーの続き
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); } // publicのためオーナーのみ実行可能としたいのでonlyOwnerOf修飾詞を加える function transfer(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) { // 呼び出す関数を書く // fromにはこのコントラクトを実行したコントラクトアドレスmsg.senderを記述 _transfer(msg.sender, _to, _tokenId); } function approve(address _to, uint256 _tokenId) public { } function takeOwnership(uint256 _tokenId) public { } }
チャプター 7: ERC721: Approve
前回のチャプターではトークン(ゾンビ)をtransfer関数を用いて送るという内容でしたが、
今回と次回のチャプターはその逆で、対象のトークン(ゾンビ)を買い手が受け取るという内容です。
そして、今回はApproveということで受け渡しの許可を取るための関数を学んでいきます。
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); } // 関数修飾詞を追加する // onlyOwnerOf オーナーしか実行不可 function approve(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) { // 買い手に許可を与えた zombieApprovals[_tokenId] = _to; Approval(msg.sender, _to, _tokenId); } function takeOwnership(uint256 _tokenId) public { } }
チャプター 8: ERC721: takeOwnership
前回に引き続き買い手が受け取るための関数です。なので、こちらも買い手が実行する関数です。
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); // ownerOf関数の中身を変数へ格納する(現オーナーのアドレス) address owner = ownerOf(_tokenId); // トークン(ゾンビ)を送る _transfer(owner, msg.sender, _tokenId); } }
チャプター 9: オーバーフロー対策
例えばuint8に256といれると桁があふれてしまう。つまりオーバーフローというものです。
予めオーバーフロー対策をしたライブラリが用意されており、それがOpenZeppelinのSafeMathというコントラクトです。
今回はこれをimportして、使えるようにしていきます。
pragma solidity ^0.4.19; import "./ownable.sol"; // まずはライブラリをインポート import "./safemath.sol"; contract ZombieFactory is Ownable { // SafeMathコントラクトを使えるようにする 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 _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]++; NewZombie(id, _name, _dna); } function _generateRandomDna(string _str) private view returns (uint) { uint rand = uint(keccak256(_str)); return rand % dnaModulus; } function createRandomZombie(string _name) public { require(ownerZombieCount[msg.sender] == 0); uint randDna = _generateRandomDna(_name); randDna = randDna - randDna % 100; _createZombie(_name, randDna); } }
チャプター 10: SafeMathパート2
前回のチャプターではSafeMathを使えるようにするまででしたが、今回のチャプターで実際に使っていきます。
ちなみにSafeMath.solの中身はこんな感じ。
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; } }
英語なので分かりづらいが、こうゆうこと。
ADD:加算
SUB:減算
MULT:乗算
DIV:除算
library SafeMath {~~~~~~}
using SafeMath for uint256;
という構文は初めてだと思いますが、ほとんどコントラクトと同じとのこと。
違いとしては、SafeMath内の関数を使用したときにuint256で値を返せるということ。
あとは以下のように変数へあらかじめ数字をいれておき、第一引数にセットできる。
第二引数だけセットすれば加減乗除算できる。
using SafeMath for uint; // now we can use these methods on any uint uint test = 2; test = test.mul(3); // test now equals 6 test = test.add(5); // test now equals 11
では、テスト問題を下記へ。
pragma solidity ^0.4.19; 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) 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 { // SafeMathの`add`で置き換え ownerZombieCount[_to] = ownerZombieCount[_to].add(1); // SafeMathの`sub`で置き換え ownerZombieCount[_from] = ownerZombieCount[_from].sub(1); 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); } }
チャプター 11: SafeMathパート3
uint32とuint16バージョンも使ってみようという回。
pragma solidity ^0.4.19; import "./ownable.sol"; import "./safemath.sol"; contract ZombieFactory is Ownable { using SafeMath for uint256; // 1. uint32にSafeMath32を使用することを宣言せよ using SafeMath32 for uint32; // 2. uint16にSafeMath16を使用することを宣言せよ 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 _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. ここでSafeMathの`add`を使うのだ: ownerZombieCount[msg.sender] = ownerZombieCount[msg.sender].add(1); NewZombie(id, _name, _dna); } function _generateRandomDna(string _str) private view returns (uint) { uint rand = uint(keccak256(_str)); return rand % dnaModulus; } function createRandomZombie(string _name) public { require(ownerZombieCount[msg.sender] == 0); uint randDna = _generateRandomDna(_name); randDna = randDna - randDna % 100; _createZombie(_name, randDna); } }
チャプター 12: SafeMathパート4
その他、++しているところをSafeMathへ置き換えていく
pragma solidity ^0.4.19; import "./zombiehelper.sol"; contract ZombieBattle is ZombieHelper { uint randNonce = 0; uint attackVictoryProbability = 70; function randMod(uint _modulus) internal returns(uint) { // 修正箇所 randNonce = randNonce.add(1); return uint(keccak256(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) { // 修正箇所 myZombie.winCount = myZombie.winCount.add(1); myZombie.level = myZombie.level.add(1); enemyZombie.lossCount = enemyZombie.lossCount.add(1); feedAndMultiply(_zombieId, enemyZombie.dna, "zombie"); } else { // 修正箇所 myZombie.lossCount = myZombie.lossCount.add(1); enemyZombie.winCount = enemyZombie.winCount.add(1); _triggerCooldown(myZombie); } } }
チャプター 13: コメント
コメントの書き方についての説明です。例をそのまま載せてみます。
/// @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:ユーザー にコントラクトや関数全体で何を行うか説明
@param:パラメータに設定しているのは何か(引数)
@return:どんな値を返すのか(返り値)
@dev:関数に対してつける。どんな動きをする関数か
pragma solidity ^0.4.19; import "./zombieattack.sol"; import "./erc721.sol"; import "./safemath.sol"; /// @title タイトルです /// @author 自分の名前 /// @dev NFTを送ったり受け取ったりするコントラクトです contract ZombieOwnership is ZombieAttack, ERC721 { using SafeMath for uint256; 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[_to].add(1); ownerZombieCount[msg.sender] = ownerZombieCount[msg.sender].sub(1); 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); } }
ここでレッスン5は終わりです。
お疲れ様でした。
次回はいよいよweb3.jsでフロントエンドとの繋ぎ込みです。
次の講座はこちら。
Solidity入門基礎編【6】 – CryptoZombiesで学習 –