🎉

cryptozombiesでSolidityについて学ぶ part4

に公開

はじめに

今回も引き続きcryptozombiesでSolidityの基礎を学んでいきます。Solidityの基礎編は今回でラストになります。

前回の記事はこちら。

part1: https://zenn.dev/barabara/articles/a45c053f4a8eb8
part2: https://zenn.dev/barabara/articles/9a1597fbeb8bb7
part3: https://zenn.dev/barabara/articles/6fc9ad5d52bec5

payable修飾詞

ここまでで様々な関数修飾子をみてきました。

  • pure 状態の変更やアクセスを禁止する
  • view 状態の変更を不可にする
  • override 親の関数を上書きできる
  • virtual オーバーライド可能な関数になる
  • private / external / internal / public いつどこで関数を呼び出せるか制御する
  • modifier 関数修飾子をカスタムで設定できる (part3で触れたonlyOwnerなど)

他にも関数修飾子はあり、その一つがpayable関数になります。

payable関数は、ETH(または他のネイティブトークン)を受け取る関数を作るときに使います。

特徴

  • 送金を許可する特別な関数であり、payable修飾子がついていない関数にEtherを送ろうとするとトランザクションは自動的に拒否されます
  • 送られたETHはコントラクトの残高に蓄積される
  • msg.valueで送金されたETHの量を取得できる

この例は、setMessage関数に1ETHを払った場合のみ、メッセージの更新ができるようになっています。


contract PayableExample {
	function setMessage(string newMessage) public payable {
    require(msg.value == 1 ether);
    message = newMessage;
  }
}

コントラクトからEtherを引き出す

payable 関数でEtherを受け取る方法は説明しましたが、このままではコントラクトにEtherが貯められるだけで引き出したり送金することはできません。payable関数でEtherを受け取った後、それを送金する方法には主に3つあります。

address(this).balance でコントラクトに貯められた残高の総量を返しています。CryptoZombiesではthis.blanceで取得していますがこの方法はv0.5.0で既に禁止になっているみたいです🧟‍♀️(参照)

  • transfer
function sendFromTransfer(address payable _to) public payable {
    _to.transfer(address(this).balance);
}

transfer は、address に指定したアドレスに Ether を転送するために使われる関数です。これを使用する場合、Ether の送信先に指定されたアドレスが自動的に受け取る形となります。

特徴として、Etherを送信するときのガス制限が2300ガスに設定し、受け取り側がガスを使いすぎないよう保護したり、送金が失敗した場合、トランザクションがリバートされるため安全性が高いです。

  • send
function sendFromSend(address payable _to) public payable {
    bool sent = _to.send(address(this).balance);
    require(sent, "Failed to send");
}

sendは、transferと似ているところもありますが違いとしては、ガス制限の設定を手動で行えるところです。また、失敗時にはfalseを返すため、追加のエラーチェックが必要です。

  • call
function sendFromCall(address payable _to) public payable {
    (bool sent, ) = _to.call{value: address(this).balance}("");
    require(sent, "Failed to send");
}

call は、最も汎用的な方法で、Etherの送信だけでなくコントラクト間のメッセージ送信や、外部コントラクトとのインタラクションに使います。

transfersendは送金時に受け取り側に対して2300ガスの制限をかけるため受け取り側のコントラクトがガスを使いすぎて送金が失敗してしまう可能性がありますが、callは送金時のガス制限や失敗時の処理を柔軟にカスタマイズできるので、より強力に制御ができるため、現在ではcall関数を使用して送金処理を行うのが推奨されているみたいです

乱数生成

Solidityには組み込みの乱数生成関数は存在しません。スマートコントラクトは全ノードが同じ結果を出す必要があり、そのため内部でランダム性を作り出すことが難しいからです。

一応疑似乱数は生成することができます。あくまで疑似乱数でセキュリティのリスクはあるため、真に安全な乱数生成とは言えないです。

keccak256 を使用する方法

contract RandomExample {
    function generateRandomNumber(uint256 seed) public view returns (uint256) {
      // abi.encodePackedは複数の値を連結してバイナリ形式にエンコードする
       return uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender, seed))) % 100;
    }
}

詳しくは触れないですが、安全な乱数を必要とする場合は、Chainlink VRF (Verifiable Random Function) という外部コントラクトを利用すれば作成できます。

Chainlink VRFの使い方はこちらの記事が参考になりました。

https://tech-lab.sios.jp/archives/40428

イーサリアムトークン

イーサリアムのトークン規格(トークンの共通ルールや仕様)であるERC20ERC721ERC1155は、それぞれ異なる目的と特性を持つトークンの代表的な標準規格です。以下にその概要と違いを説明します。

ERC20(代替可能トークン)

ERC20は、代替可能なトークン(Fungible Token)の標準規格です。この規格は、同一の価値を持つトークンを扱うために設計されており、主に暗号通貨や資産として利用されます。

特徴

  • すべてのERC20トークンは同じ価値を持ち、互換性がある(例: 1 USDT = 1 USDT)。
  • トークンを小数点以下の単位まで分割して送金可能

主な機能

  • totalSupply() トークンの総供給量を返す
  • balanceOf(address) 指定アドレスの残高を返す
  • transfer(address, uint256) トークンを送信する
  • approve(address, uint256) 第三者によるトークン使用を承認する
  • transferFrom(address, address, uint256) 承認された第三者がトークンを送信する

ERC721(非代替性トークン / NFT)

ERC721は、非代替可能なトークン(Non-Fungible Token, NFT)の標準規格です。この規格は、ユニークなデジタルアイテムやコレクションアイテムを表現するために使用されます。

特徴

  • 各トークンが一意であり、他のトークンと交換できません(例: デジタルアートやゲーム内アイテム)。
  • トークンは分割できず、1単位でのみ取引可能

主な機能

  • balanceOf(address) 所有するトークン数を返す
  • ownerOf(uint256) 特定のトークンの所有者を返す
  • safeTransferFrom(address, address, uint256) トークンを安全に送信する
  • approve(address, uint256) 特定のトークンの使用を承認する
  • getApproved(uint256) 承認されたアドレスを返す

ERC1155(非代替性トークン / NFT)

ERC1155は、代替可能と非代替性トークンの両方をサポートする多目的な規格です。なので、代替可能トークン(ゲーム内通貨やETH)と非代替性トークン(レアアイテムやNFT)が混在しているため、効率的に管理するためにブロックチェーンゲームやNFTのマーケットプレースに使用されます。

特徴

  • 複数トークン型:1つのコントラクトで複数種類のトークンを管理
  • バッチ操作:複数のトークンを1回のトランザクションで送信可能
  • ガス効率:ストレージの最適化によりガスコストを削減

主な機能

  • balanceOf(address, uint256) 特定のトークンIDの残高を返す
  • balanceOfBatch(address[], uint256[]) 複数アドレスの複数トークンIDの残高を一括で返す
  • safeTransferFrom(address, address, uint256, uint256, bytes) トークンを安全に送信する
  • safeBatchTransferFrom(address, address, uint256[], uint256[], bytes) 複数のトークンを一括で送信する

オーバーフローとアンダーフロー

オーバーフローとは、数値がそのデータ型が表現できる最大値を超えてしまう現象です。例えば、uint8は0から255までの範囲ですが、256を代入しようとすると0に戻ってしまいます。逆に、アンダーフローは最小値を下回る場合です。

v0.8.0以前まで、オーバーフローとアンダーフローの対策にOpenZeppelinのSafeMathライブラリを用いて数値計算を安全にできるかチェックしていました。(SafeMathライブラリは最新のOpenZeppelinのリポジトリやドキュメントからはもう消えています)

v0.8.x以降では、オーバーフローやアンダーフローのチェックがデフォルトで有効となったため、特に手動で対策はせず数値計算を行えるようになっています。

contract MyContract {
    uint256 public totalSupply;

    function add(uint256 amount) public {
		    // オーバーフローとアンダーフローを自動チェック
        totalSupply = totalSupply + amount; 
    }
}

ライブラリの定義

SafeMathライブラリは不要になりましたがライブラリそのものは重要です。

用途

  • コードの再利用 複数のコントラクトで同じロジックを使いたい場合に便利
  • 関数の集約 複雑な処理やユーティリティ関数を切り出して、コードをシンプルに保てる
  • ガス効率 ライブラリのデプロイは1回だけなので、複数のコントラクトで同じコードを複製する場合と比較して、ガスコストが削減になる

定義方法

**
 * @dev Standard math utilities missing in the Solidity language.
 */
library Math {
    /**
     * @dev Returns the largest of two numbers.
     */
    function max(uint256 a, uint256 b) internal pure returns (uint256) {
        return ternary(a > b, a, b);
    }

    /**
     * @dev Returns the smallest of two numbers.
     */
    function min(uint256 a, uint256 b) internal pure returns (uint256) {
        return ternary(a < b, a, b);
    }
}

ライブラリはlibraryキーワードを使って定義します。

呼び出し方

ライブラリの呼び出し方は2つ方法があります。

  • usingを使用する方法
contract MathExample {
    using Math for uint256;

    function findMax(uint256 num1, uint256 num2) public pure returns (uint256) {
        return num1.max(num2);
    }
}

メソッドチェーンのように書けたり、型ごとの操作が明確になるのは良いと思いますが、引数が複数あったりするとあまり直感的でないですね。

  • 直接呼び出す方法
import "@openzeppelin/contracts/utils/math/Math.sol";

contract Example {
    function findMax(uint256 num1, uint256 num2) public pure returns (uint256) {
        return Math.max(num1, num2);
    }
}

using使うよりは多少コードが長くなってしまいますが、個人的には関数名が明示的でわかりやすいのでこっちの方が良いなと思いました。

コメント

ここまで//でコメントを書いてきましたが、コメントの書き方は一般的なコメントとNatSpec(Natural Specification)と呼ばれる特殊なコメントの2種類があります。

一般的なコメント

書き方はJavaScriptと同様です。

  • 単一行コメント // から行末まで
  • 複数行コメント /* と *\ で囲む

NatSpecコメント

NatSpecは、Solidityのスマートコントラクトのためのドキュメンテーションフォーマットです。

NatSpecのコメントは以下の形式です。

タグ 内容 コンテキスト
@title コントラクトの名前 contract, library, interface
@author 作成者の名前 contract, library, interface
@notice これがどういうことを行うのか、エンドユーザー向けの説明 contract, library, interface, function, public state variable, event
@dev 開発者向けの追加の説明 contract, library, interface, function, state variable, event
@param 関数の引数に対しての説明 function, event
@return 関数の戻り値に対しての説明 function, public state variable
@inheritdoc 親コントラクトからドキュメントを継承する function, public state variable
@custom:... 独自のタグを作成して自由にコメントできる everywhere

contract CommentExample {
    // この変数はユーザーの残高を保存します
    uint256 public balance;

    /**
     * @notice この関数はユーザーの残高を取得します
     * @dev balanceは状態変数で、ユーザーの残高が格納されています
     * @return ユーザーの残高を返します
     */
    function getBalance() public view returns (uint256) {
        return balance;
    }

    /**
     * @notice ユーザーの残高を更新します
     * @param amount 新しい残高の値
     */
    function updateBalance(uint256 amount) public {
        balance = amount;
    }
}

全てのタグを毎回使わないといけないわけではありません、ただコードを読みやすくわかりやすくするために@devだけでも使っていこうと思います。

まとめ

今回でSolidityの基礎編は終わりです。ここまでCryptoZombiesをやってわからなかったところは公式ドキュメントなどを見て学んできましたが、パートが進むにつれCryptoZombiesの情報が古すぎて間違えた知識を覚えてしまいそうな箇所が多々ありました。今後のEther.jsや実際のDApp作成においては別サイトを使って取り組んでいこうと思います。

ここまで見ていただき、ありがとうございました。🙇🏽‍♀️

Discussion