cryptozombiesでSolidityについて学ぶ part4
2025/03/22 公開
はじめに
今回も引き続きcryptozombiesでSolidityの基礎を学んでいきます。Solidityの基礎編は今回でラストになります。
前回の記事はこちら。
part1: https://www.br-to.dev/blog/solidity/
part2: https://www.br-to.dev/blog/solidity_2/
part3: https://www.br-to.dev/blog/solidity_3/
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の送信だけでなくコントラクト間のメッセージ送信や、外部コントラクトとのインタラクションに使います。
transfer
やsend
は送金時に受け取り側に対して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
イーサリアムトークン
イーサリアムのトークン規格(トークンの共通ルールや仕様)であるERC20
、ERC721
、ERC1155
は、それぞれ異なる目的と特性を持つトークンの代表的な標準規格です。以下にその概要と違いを説明します。
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作成においては別サイトを使って取り組んでいこうと思います。
ここまで見ていただき、ありがとうございました。🙇🏽♀️