2021/10/10
Solidity 勉強記録
一度イーサリアム上にデプロイしたコントラクトはイミュータブルになる.
バージョン指定
pragma solidity ^x.x.x;
コントラクト
contract MyContract {
}
contract MyChildContract is MyContract {
// MyContractを継承
// MyContract内のpublic関数にアクセスできる
}
複数継承する場合は,is
の後にカンマ区切りで列挙する.
ファイル分割
import "./mycontract.sol";
if
if (condition) {
// do
}
for
for (uint i=0; i<10; i++) {
// 処理
}
記憶領域
storage
ブロックチェーン上に格納される変数.
書き込むためにはガスが必要.ガスとは,ブロックチェーンにデータを書き込むため通行料となるもの.
T storage var;
memory
関数の呼び出し時に準備され,終了と共に破棄される変数.ガス代は不要.
T memory var;
変数
基本型
uint hoge = 0; // uint8, uint16, uint32, uint256
string str = "myname";
また,アカウントに割り当てられるaddress
型もある.
uint
の種別について,uint
はuint256
へのエイリアスである.また,基本的にはどのuintX
を選んでも,uint256
文のスペースが確保されるが,struct
内部ではその限りではない.つまり,ガスの節約のため,struct
内部のuint
は,必要最低限のものを選ぶ.
配列
uint[] arr; // 配列
arr.push(0); // 末尾に追加
uint len = arr.push(1); // push()は追加後の要素数を返す
// memoryの配列生成
uint[] memory arr = new uint[](n); // 要素数を予め決める必要がある
連想配列
mapping (address => uint) public accountData; // アドレスにデータを格納できる.
時間の扱い方
seconds
, minutes
, hours
, days
, weeks
, years
という単位が用意されており,uint
の足し算で時間を扱うことができる.また,現在時刻はnow
で利用可能.また,now
はuint256
を返すため,時間単位にuint256
以外を使用する場合,明示的にキャストする必要がある.
if (now >= (lastTime + 1 days + 30 minutes))) {
// lastTimeから1日30分経過していると入れる
}
関数
書式
function <function-name>(T1 _<arg1>, T2 _<arg2>) <visibility modifier> <state modifier1> <state modifier2> returns (<RT1> <ret1>, <RT2> <ret2>) {
// <function-name>: 関数名
// <T1> <T2>: 引数の型
// <arg1> <arg2>: 引数
// <visibility modifier>: 可視性修飾子(optional),public/private/internal/external
// <state modifier1> <state modifier2>: 状態修飾子(optional),view/pure,自作可能
// <RT1> <RT2>: 戻り値の型
// <ret1> <ret2>: 戻り値の変数名(一つであればつけないほうが楽)
}
ベーシック
function myfunc(uint _a, string _s) {
// 引数はアンダースコアから開始するのが通例とのこと
} // これはパブリック関数
可視性修飾子
明示的に指定しない限りはpublic
として呼び出されるが,以下のように指定できる.
function _myPrivateFunc(uint _a, uint _s) private {
} // プライベート関数
// アンダースコアから始めるのがよいらしい
function myRetFunc() public returns(string) {
return "hello";
} // 戻り値を指定
function myInFunc() internal returns (string) {
// 継承先から呼び出せるprivate
}
function myExFunc() external returns (string) {
// コントラクト外のみから呼び出せるpublic
}
状態修飾子
view
にすると閲覧のみ,pure
にすると内部のみで処理といったように,関数内からのコントラクトへのアクセスを制限できる.view
関数は,ブロックチェーン上のデータを変更しないため,ガス代が不要となる.
function myViewFunc() public view returns (string) {
return "viewing";
} // 読み取り専用(C++でいうconst修飾子)
function myPureFunc() public pure returns (string) {
return "pure";
} // アプリ内のデータにもアクセス不可能(引数からのみ戻り値を決定)
以下のようにすると,関数修飾子を作ることができる.引数を使うこともできるので,require()
を関数内に直書きするのではなく,修飾子にしておくと再利用可能となり,便利.
modifier isOK(uint id) {
require(id == 123);
_; // 続行を意味する(?)
}
function callWithIsOK(uint id) isOK(id) {
// isOKの時に処理可能
}
payable 修飾子
Eth を受け取ることができる特別な修飾子.Eth の支払いを伴う関数にはこれをつけなくてはならない.
function myPayaFunc() external payable {
// Ethを受け取ることができる
// コントラクトにEthがどのくらい送られてきたかは`msg.value`で参照可能
}
上記でコントラクトが Eth を受け取ったが,これだけではコントラクト内に Eth がとどまってしまうため,コントラクトからどこかに移動する必要がある.
owner.transfer(this.balance); // コントラクトオーナーのアドレスに残高すべてを送金
msg.sender.transfer(0.1 ether);
seller.transfer(msg.value);
// this.balanceはコントラクト内のEth残高の総量を指す.
// 0.1 ether のようにEthを指定できる
// msg.valueは上述の通りの意味を持ち,コントラクトを介してEthをやり取り可能
複数戻り値
function myMultiFunc(uint _a, uint _b) private returns(uint a, uint b) {
return (_a*2, _b*2);
}
uint a;
uint b;
(a, b) = myMultiFunc(1, 2);
(, b) = myMultiFunc(1, 2);
Event
ブロックチェーン上の動きを,コントラクトがアプリのフロントエンドに伝えることができる.
フロントエンドをlistening
状態にし,ブロックチェーンの動きに対し発火することができる.
event MyEvent(uint x, uint result);
function func(uint _x) public {
uint resut = _x * 2;
MyEvent(_x, result);
return result;
}
require
特定の条件を与え,それを満たさない場合は関数とエラー終了する.
function myFunc(uint _x) public returns (uint) {
require(_x == 0);
return _x;
}
標準関数
Keccak256
ハッシュ関数の一種.文字列をハッシュする.余談だが,こいつの発音をカタカナで表すとケチャックみたいになるという説が強いらしい.
keccak256(s);
uint hs = uint(keccak256(s));
こいつを使うと,簡易的なランダム値を生成することもできる.
グローバル変数
msg.sender
address
型を持ち,関数を呼び出したユーザのaddress
への参照.第三者を騙ってコントラクトを呼び出すためには,秘密鍵を盗むしかない.
扱い方としては,通常の変数としても扱え,mapping
の Key としても Val としても使える.
インターフェース
他のブロックチェーン上からデータを読み取ることができる.
以下のように書くと,Solidity がインターフェースだと理解してくれる.
contract HogeInterface {
getHoge(address _addr) public view returns(uint hoge);
}
インターフェースを作ったら,以下のようにして呼び出すことができる.
address addr = 0x...;
HogeInterface hogeContract = HogeInterface(addr);
(フロントエンドで関数を呼び出すのと同じ感じ?)
フロントエンド
web3.js
やethers.js
などを使うと,Contract を利用することができる.
Solidity のビルド時に ABI が生成され,そこからコントラクトを持ってくる(?)ことができる.
ABI
Application Binary Interface の事で,コントラクトコードへのインタフェースが記されている.
myContract = new web3.eth.Contract(app_abi, app_address);
Call
view
関数およびpure
関数を呼び出すために末尾に付ける.ガス不要.
then(() => {})
をつなげる.
myContract.methods.myFunc(1).call();
Send
view
/pure
ではない関数を呼び出す,つまり,ユーザへ署名を要求する関数を呼び出す時に使う.これにはガス代も含まれる.こいつには秘密鍵が必要で,Metamask による管理を受けるとのこと.
イベントリスナーを連ねていくが,それが,receipt
/error
である.
myContract.methods.buyCat(1).send();
myContract.methods.buyCat(1)
.send({from: myAccount})
.on("receipt", (receipt) => {
// トランザクション成功
})
.on("error", (error) => {
// トランザクション失敗
})
myContract.methods.buyCat(1)
.send({from: myAccount, gas: 300}) // ガスを指定可能,非指定時はユーザ選択
call()
およびsend()
は,フロントエンド開発において,JavsScript 側でわかりやすい名前をつけた関数を再定義しておくと,開発が楽になる.
Wei
1ether = 10^18wei
となる Ether の最小単位.web3.js では,Ether 単位ではなく Wei 単位で指定する必要がある.
web3js.utils.toWei("0.1", "ether");
myContract.methods.buyCat(1)
.send({ from: myAccount, value: web3js.utils.toWei("0.1", "ether")})
event
フロントエンドで,コントラクトのイベントをサブスクリライブできる.
myContract.events.SoldCat()
.on("data", (event) => {
let cat = event.returnValues;
console.log('cat was sold, ', cat.id, cat.name);
}) // コントラクト内のデータが変更される度に情報が送られてくる
.on("error", (error) => {
console.log("Error: ", error);
})
// ERC721において,フィルタしてイベントを受け取る
myContract.events.Transfer({ filter: { _to: myAccount } })
.on("data", (event) => {
let cat = event.returnValues;
})
トークン
イーサリアム上のトークンとは,特定のルールに基づいたスマートコントラクトのこと.
クリプト収集物にはERC721が適しているらしく,用途によって使い分ける必要がある.クリプトキティーズも ERC721 だった.
SafeMath
オーバフロー・アンダフローを防ぐ.
using SafeMath for uint256;
uint i = 1;
i = i.add(1);
i = i.sub(1);
i = i.mul(10);
i = i.div(10);
なんか普通のプログラミングと違うよね
Solidity はブロックチェーン上のデータであるstorage
とローカル(?)上のmemory
の2種類のデータ保存方法がある.後者は通常のプログラミングと一緒であるが,storage
は考え方そのものが違う.
通常のプログラミングであれば,本来なら計算量を削減するためにデータ構造やアルゴリズムを組み合わせて,より走査回数や計算回数が減少するようなプログラムを書く.オーダ記法で考えることで,スケールしたときの処理速度の増大を防いだりもする.
しかし,storage
を扱うことはコストが高く,特に,storage
への書き込みには莫大なコストがかかる.そのため,走査回数を減らすのではなく,storage
への書き込みができるだけ少なく済むように設計する必要がある.
例えば,配列に ID 0<=id<=100
が割り当てられているとする.id=x
である配列内の要素を繰り返し探す必要があるという場合のことを考える.配列の全要素を確認してid=x
となる要素を探すと,O(N)
となってしまう.
そこで通常,id
から配列要素への参照を用意しておき,id
の変更が行われる度に参照を書き換えることで,O(logN)
やO(1)
へと計算量を削減することができる.これはいわば,メモリとそのデータ更新を犠牲にして計算量を節約しているとも言える.
しかし,Solidity においてstorage
を書き換えることは高コストなため,O(N)
の走査時間をとってでも,storage
のデータを書き換える回数を抑えるという方法を取ることがある.