ブロックチェーン技術ブログ

ブロックチェーンと英語、数学に関するブログ

Solidity入門1 <Pragmas, Contracts, Types>

Solidityは、スマートコントラクトを実装するのに利用されるオブジェクト指向言語です.
Solidityの文法や基本的な機能についてみていきます.

Pragmas

Solidityのソースコードは、ファイルの先頭に Version Pragma を書く必要があります.
Version Pragmaは、コンパイラーのバージョンを指定します.

コンパイラーのバージョンが 0.6.0以上 かつ 0.7.0未満 であること

pragma solidity >=0.6.0 <0.7.0;

コンパイラーのバージョンが 0.6.2以上 かつ 0.7.0未満 であること

pragma solidity ^0.6.2;

Contracts

Contract は、オブジェクト指向言語におけるクラスに似た仕組みです.

オブジェクト指向言語と同様に継承の機能を持ち、他のContractを継承することができます.

Wallet を継承した SimpleWallet を定義する

contract Wallet {
}

contract SimpleWallet is Wallet {
}

Contractの中には、State Variables、Functions、Function Modifiers, Events, Errors, Struct Types, Enum Typesを定義することができます.

Constructor

Solidityでは、Contract内にただ一つコンストラクタを宣言することができます.
コンストラクタを宣言しない場合、デフォルトのコンストラクタ(何もしないコンストラクタ)があるとみなせます.

コンストラクタはコントラクト生成(デプロイ)時に、実行されます.

Walletの生成時にコンストラクタで minimumBalance に初期値をセットする

contract Wallet {
   uint public minimumBalance;

   constructor(uint _minimumBalance) public {
       minimumBalance = _minimumBalance;
   }
}

SimpleWallet から Wallet のコンストラクタを呼び出す

contract SimpleWallet is Wallet {
   constructor(uint _minimumBalance) Wallet(_minimumBalance + 10) public {}
}

Value Types

Solidityで使用するValue型をいくつか抜粋してみてみましょう.

Booleans

true, false のどちらかの値をとります.
キーワード bool を使用して、宣言します.

bool approved; 

Integers

符号付き整数(int)と符号なし整数(uint)の2種類があります.
キーワード int, uint の後に、数値のビット数を示す数を付けて宣言します.
この値は、8の倍数となる 8 ~ 256 の範囲で選ぶことができます.

int64 rate;
uint128 balance; 
uint score;

数値のビット数を省略することもできます.
int, uint はそれぞれ int256, uint256エイリアス(同じデータ型)です.

Addresses

Addressには、address, address payable の2種類があります.
どちらもEthereumのアドレスと同じ長さである20バイトの値を保持します.

Addressはメンバを持っており、アカウントの残高を取得できます.

  • balance
    address のアカウントの残高を返す(単位: wei)

コントラクトの残高を取得する

contract Wallet {
    function getBalance() external view returns (uint) {
        return address(this).balance;
    }
}
  • call(bytes memory) returns (bool, bytes memory)
    ペイロードを引数に CALLを実行し、 処理の成否を表す boolean値 と データ を返す.
    全てのガスを転送する(転送するガスの量を指定することも可能).

address payable のみ、以下のメンバを持っています.

  • transfer(uint256 amount)
    address のアカウントに、引数で指定した ETH (単位: wei) を送金する.
    定量のガスを転送する.
    コントラクトアカウントの残高が不足していた場合、処理が中断され、変更がリバートされる.

  • send(uint256 amount) returns (bool)
    address のアカウントに、引数で指定した ETH (単位: wei) を送金する.
    定量のガスを転送する.
    コントラクトアカウントの残高が不足していた場合、送金処理が拒否され、falseを返す(処理が中断されない).

call, transfer, send を使用した送金の例
(コントラクトアカウントから to のアカウントに、100wei を送金する)
call を使用した送金が推奨されています.

contract SendEther {
    address payable to = payable(0x123);

    function sendViaCall() external payable {
        (bool sent, bytes memory data) = to.call{value: 100}("");
        require(sent, "Failed to send Ether");
    }

    function sendViaTransfer() external payable {
        to.transfer(100);
    }

    function sendViaSend() external payable {
        bool sent = to.send(100);
        require(sent, "Failed to send Ether");
    }
}

他にもメンバを保持しており、以下のリンクから確認できます.
Units and Globally Available Variables — Solidity 0.8.17 documentation

Strings

文字列型は、ダブルクウォートもしくはシングルクオートで文字列を囲って記述します.
また、文字列リテラルは、bytes型に割り当てることも可能です.

string memory text1 = 'abc';
string memory text2 = "def";
bytes32 stringLiteral = 'ghq';

Solidityでは、他の言語と異なり、文字列を比較する機能がサポートされていません.
そのため、2つの文字列を比較したいときは、以下の手順を踏みます.

  1. abi.encodePacked を使用して、bytes型に変換する
  2. 1で取得した値に keccak256 を使用して、ハッシュ値を取得する
  3. 2で取得した値を比較する

文字列の一致を判定する処理

function compare(string memory a, string memory b) public pure returns(bool) {
    return keccak256(abi.encodePacked(a)) == keccak256(encodePacked(b));
} 

Enums

Enumsを使用して、独自のデータ型を定義できます.
Enumsは一つ以上のメンバーを含みます.

enum Seasons を定義して、その変数に Winter を代入

enum Seasons { Spring, Summer, Autumn, Winter };
Seasons season;

season = Seasons.Winter;

Enumsのデフォルト値は、一番の左のメンバーとなります.

Reference Types

参照型は、Value型と異なり、異なる変数から同じデータを参照できます.
意図せず他の変数の値も変更してしまう恐れがあるので、注意深く扱う必要があります.

参照型には、Arrays, Structs, Mappingsがあります.
参照型の変数を宣言するときは、データの格納場所(Data location)も一緒に指定する必要があります.

Data locationには、以下の3種類があります

  • memory
    外部関数(External function)を実行してる間、データが保持される

  • storage
    コントラクトが存在する間、データが保持される
    コントラクトの直下に定義する変数は、Data locationを省略可能で、その場合 storage となる

  • calldata
    関数の引数を保持する (変更が不可能)

Arrays

配列には、固定長のものと可変長のものがあります.
あるデータ型 T に対して、T[] のように定義します.

素数が3の uint の配列を生成

uint[] memory numbers = mew uint[](3);

Data locationがmemoryの場合、配列の生成後に、動的に要素を追加することができません.

配列の操作

uint[] storage numbers = new uint[]();

// 配列に要素を追加 (配列が可変長の場合のみ、呼び出し可能)
numbers.push(1);

// 配列の末尾の要素を削除 (配列が可変長の場合のみ、呼び出し可能)
// 返り値はなし
numbers.pop();

// 配列の要素数を取得 (配列が固定長の場合のみ、固定値を返す)
numbers.length;

Structs

任意のデータ型のメンバからなる、新しいデータ構造を定義できます.
配列やMappingsのデータ型にすることも可能です.

struct Person {
    string name;
    uint age;
    address addr;
}

// Personの配列を宣言
Person[] people;

Mappings

Mappingsは、キーとバリューのペアからなるデータ型です.
JavaでのMap, PythonでのDictに相当するものです.

mapping(KeyType => ValueType) という書式で、宣言します.

キーがaddress、バリューがuintのmappingを宣言

mapping(address => uint) balances;
address addr = payable(0x123);

// キー・バリューのペアを追加
balances[addr] = 123;

// キーからバリューを取得
balances[addr];

Mappingsを宣言するときは、Data locationにstorageを指定する必要があります.

存在しないキーで、バリューを取得しようとした場合、バリューのデータ型の初期値が返り値になります.

Mappingsは、他の言語と同様に、要素数を取得したり、キー・バリューのペアをイテレートすることができません.
なので、別途配列を用意して、キーをその配列に格納するなどの工夫が必要となります.