📖

Chapter6: Writing Smart Contracts | Solidity Programming Essentialsを読む

2022/08/04に公開

イーサリアムの知識を整理するために2022年6月発売のSolidity Programming Essentials 2nd Editionを読み進める試みです。この記事ではChapter6「Writing Smart Contracts」を読み進めます。

Solidity Programming Essentials: A guide to building smart contracts and tokens using the widely used Solidity language, 2nd Edition (English Edition)

読書ログは以下のスクラップで逐次更新していきます。
https://zenn.dev/mah/scraps/ea8c79961ae8c8

コントラクトの生成

Solidityでコントラクトを生成し利用するには二つの方法がある。

  • newキーワードを使う
  • 既にデプロイされているコントラクトのアドレスを利用する

newキーワードを使う

Solidityのnewキーワードは新しいコントラクトインスタンスをデプロイして作成する。コントラクトをデプロイし、ステート変数を初期化し、コンストラクタを実行し、nonce値を1に設定し、最終的には呼び出し元にインスタンスのアドレスを返すことでコントラクトインスタンスを初期化する。

コントラクトをデプロイするためには、送信者がデプロイを完了するのに十分なガスを提供したかどうかを確認し、要求者のアドレスとnonce値を使用して、コントラクトデプロイ用のアカウント、アドレスを作成し、それと共に送信されるEtherを渡す。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract HelloWorld {
  uint private simpleInt;

  function getValue() public view returns (uint) {
    return simpleInt;
  }

  function setValue(uint _value) public {
    simpleInt = _value;
  }
}

contract Client {
  function UseNewKeyword() public returns (uint) {
    // HelloWorldコントラクトをデプロイし、アドレスを取得
    HelloWorld myObj = new HelloWorld();
    myObj.setValue(10);
    return myObj.getValue();
  }
}

newキーワードによって生成されるコントラクトのアドレスは呼び出し元コントラクトのアドレスとnonce値に基づくため予測することが可能である。予測ができないようにランダム性を持たせたい場合はソルト値を与える。

contract Client {
  function UseNewKeyword(bytes32 _salt) public returns (uint) {
    HelloWorld myObj = (new HelloWorld){salt: _salt}();
    myObj.setValue(10);
    return myObj.getValue();
  }
}

newキーワードによって子コントラクトを作成しながら、同時にEtherを送ることも可能。ただし呼び出し側のコントラクトがEther残高を持っており、かつ作成するコントラクトがpayableなコンストラクタを持っている場合に限る。

contract Client {
  function UseNewKeyword(bytes32 _salt) public returns (uint) {
    HelloWorld myObj = (new HelloWorld){value: 1000000000000000000, salt: _salt}();
    myObj.setValue(10);
    return myObj.getValue();
  }
}

コントラクトのアドレスを利用する

コントラクトが既にデプロイされインスタンス化されている場合は、そのアドレスを利用してそのコントラクトを参照することができる。

下記のコード例ではsetObjectでHelloWorldコントラクトのアドレスをClientに保存し、HelloWorldコントラクトを呼び出している。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract HelloWorld {
  uint private simpleInt;

  function GetValue() public view returns (uint) {
    return simpleInt;
  }

  function SetValue(uint _value) public {
    simpleInt = _value;
  }
}

contract Client {
  address obj ;

  function setObject(address _obj) external {
    obj = _obj;
  }

  function UseExistingAddress() public returns (uint) {
    HelloWorld myObj = HelloWorld(obj);
    myObj.SetValue(10);
    return myObj.GetValue();
  }
}

コントラクトのコンストラクタ

Solidityではコントラクト内でコンストラクタを宣言することができる。宣言されていない場合は、コンパイラによってデフォルトのコンストラクタが作成される。

コンストラクタはデプロイ時に一度だけ実行される。他の言語では新しいオブジェクトのインスタンスが作成されるたびにコンストラクタが実行されるが、Solidityの場合はデプロイ時に一度だけ実行されるということに注意が必要である。

コントラクタは主にステート変数の初期化とコンテキストのセットアップに利用する。Solidityでは一つのコントラクトに対して一つのコンストラクタしか存在できない。コントラクタに引数を設定する場合は、デプロイ時にパラメータを渡す必要がある。

また、コントラクタにはpayable属性をつけることができる。payable属性がついている場合は、デプロイ時やコントラクトインスタンス作成時にEtherを受け取ることができるようになる。

以下はコントラクタインスタンス作成時にコンストラクタでステート変数simpleIntに値を割り当てる例である。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract HelloWorld {
  uint private simpleInt;

  constructor() {
    simpleInt = 5;
  }

  function GetValue() public view returns (uint) {
    return simpleInt;
  }

  function SetValue(uint _value) public {
    simpleInt = _value;
  }
}

コントラクトの合成

これまでの例では、ClientコントラクトとHelloWorldコントラクトは、ClientコントラクトがHelloWorldコントラクトのインスタンスを保持するhas-a関係だった。その他にも継承を利用したis-a関係を表現することもできる。

継承

Solidityではスマートコントラクト間の継承をサポートしている。親と子のコントラクトの間にはis-a関係があり、publicexternalinternalの全ての関数とステート変数が派生コントラクトで使用できる。内部的には、Solidityコンパイラは基底コントラクトのバイトコードを派生コントラクトのバイトコードにコピーする。

Solidityでは単一継承、マルチレベル、多重継承など複数のタイプの継承をサポートしている。

単一継承

継承はisキーワードで表現する。コントラクトAを継承するコントラクトBを表現する場合は以下のように書く。

contract A {
  // ....
}
contract B is A {
  // ....
}

マルチレベル継承

複数に連なる継承も表現することができる。

contract A {
  // ....
}
contract B is A {
  // ....
}
contract C is B {
  // ....
}

ヒエラルキー継承

基底コントラクトを複数のコントラクトから継承することもできる。

contract A {
  // ....
}
contract B is A {
  // ....
}
contract C is A {
  // ....
}

多重継承

基底コントラクトAを継承したコントラクトB、コントラクトCを継承したコントラクトDを表現する場合は以下のように書く。コントラクトB、コントラクトCだけを指定するのではなく、コントラクトAも指定する。

contract A {
  // ....
}
contract B is A {
  // ....
}
contract C is A {
  // ....
}
contract D is A, B, C {
  // ....
}

カプセル化

クライアントが直接アクセスできないステート変数を宣言し、関数からのみアクセスできるようにする、といった設計も可能である。Solidityではexternalpublicinternalprivateなどの複数の可視性修飾子が用意されており、ステート変数が定義されているコントラクト内、継承する子コントラクト、外部コントラクトでの可視性を制御することができる。

ポリモーフィズム

Solidityにおけるポリモーフィズムには以下の種類がある。

  • 関数ポリモーフィズム(Function polymorphism)
  • コントラクトポリモーフィズム(Contract polymorphism)

関数ポリモーフィズム

型によって関数をオーバーロードすることができる。以下がコード例。int8型の引数が与えられた場合とint16型の引数が与えられた場合とで別々の関数が実行される。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract FunctionPolymorphism {
  function GetVariableData(int8 data) public pure returns(int8) {
    return data;
  }
  function GetVariableData(int16 data) public pure returns(int16) {
    return data;
  }
}

コントラクトポリモーフィズム

コントラクトポリモーフィズムとは、コントラクトが継承によって互いに関連している場合に、複数のコントラクトインスタンスを入れ替えて使用することである。具体的には親コントラクト型変数に子コントラクトのインスタンスを設定できる、ということ。

ParentContact pc = new ChildContract();

メソッドオーバーライド

親コントラクトで定義している関数を、派生コントラクトで同じ名前とシグネチャで再定義することをオーバーライドと呼ぶ。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

contract ParentContract {
  uint internal simpleInteger;

  function SetInteger(uint _value) public {
    simpleInteger = _value;
  }

  function GetInteger() virtual public view returns (uint) {
    return 10;
  }
}

contract ChildContract is ParentContract {
  function GetInteger() override public view returns (uint) {
    return simpleInteger;
  }
}

contract Client {
  ParentContract pc = new ChildContract();

  function WorkWithInheritance() public returns (uint) {
    pc.SetInteger(100);
    return pc.GetInteger(); // ChildContractのGetIntegerが呼び出され、100が返る
  }
}

抽象コントラクト

抽象コントラクトとは部分的な関数定義を持つコントラクトであり、インスタンスを作成することはできない。抽象コントラクトの機能を利用するには子コントラクトに継承させる必要がある。抽象コントラクトであると明示するためにはabstractキーワードを利用する。

下記のコード例では抽象コントラクトAbstractHelloWorldを実装したHelloWorldをインスタンス化して利用している。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

abstract contract AbstractHelloWorld {
  function GetValue() virtual public view returns (uint);
  function SetValue(uint _value) virtual public;
  function AddNumber(uint _value) virtual public returns(uint) {
    return _value;
  }
}

contract HelloWorld is AbstractHelloWorld {
  uint private simpleInteger;

  function GetValue() override public view returns (uint) {
    return simpleInteger;
  }
  function SetValue(uint _value) override public {
    simpleInteger = _value;
  }
  function AddNumber(uint _value) override public view returns (uint){
    return (simpleInteger + _value);
  }
}

contract Client {
  AbstractHelloWorld myObj;

  constructor() {
    myObj = new HelloWorld();
  }
  function GetIntegerValue() public returns (uint) {
    myObj.SetValue(100);
    return myObj.AddNumber(200) + 10;
  }
}

インターフェース

インターフェースは抽象コントラクトに似ているが、関数の定義を含めることができず、関数の宣言のみを含むことができる。ステート変数を含むこともできない。インターフェースは他のインターフェースを継承することができる。

下記のコード例はインターフェースIHelloWorldを実装したHelloWorldコントラクトの実装を示している。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

interface IHelloWorld {
  function GetValue() external view returns (uint);
  function SetValue(uint _value) external;
}

contract HelloWorld is IHelloWorld{
  uint private simpleInteger;

  function GetValue() public view returns (uint) {
    return simpleInteger;
  }
  function SetValue(uint _value) public {
    simpleInteger = _value;
  }
}

contract Client {
  function GetSetIntegerValue() public returns (uint) {
    IHelloWorld myObj = new HelloWorld();
    myObj.SetValue(100);
    return myObj.GetValue() + 10;
  }
}

高度なインターフェースの使い方

コントラクトは既にデプロイされており、アドレスも取得可能な状態だが、コントラクトの定義がないためにコードからアクセスできないシチュエーションを思い浮かべて欲しい。そのような場合、そのコントラクトが実装しているインターフェースが分かれば、その定義を利用してコントラクトを利用することができる。

例えばIMathというインターフェースがあったとしよう:

interface IMaths {
  function GetSquare(uint256 value) external returns(uint256);
}

その実装コントラクトはこのような形になっている。

contract Mathematics is IMaths {
  function GetSquare(uint256 value) external pure returns(uint256) {
    return value ** 2;
  }
}

Mathematicsコントラクトは既にイーサリアムネットワークにデプロイされており、そのアドレスも分かっていて、インターフェースも分かっている場合は次のようにアクセスすることができる。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

import "./interfaces.sol";

contract Client {
  function CallSquare(address targetContract, uint256 inputValue) public returns (uint256) {
    IMaths targetMathematics = IMaths(targetContract);
    return targetMathematics.GetSquare(inputValue);
  }
}

ライブラリ

Solidityには一度書いたコードを複数のスマートコントラクトで再利用することができるライブラリという機能がある。ライブラリはlibraryキーワードによって宣言する。

library {
  // ...
}

ライブラリもコントラクトと同様イーサリアムネットワーク上にデプロイ可能だが、ライブラリは状態を維持・管理せず、再利用可能なコードとして利用可能な一連の関数を持つ。

ライブラリのインポート

次のようなライブラリがあるとき

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

library MyMathLibrary {
  function sum( uint256 a, uint256 b) public returns(uint256) {
    return a + b ;
  }
  function exponential( uint256 a,uint256 b) public returns(uint256){
    return a ** b ;
  }
}

このようにライブラリファイルをインポートしてコントラクト内でライブラリ関数を利用できる。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

import "./mylib.sol";

contract LibraryClient {
  function GetExponential(uint256 firstVal, uint256 secondVal) public returns(uint256) {
    return MyMathlibrary.exponential(firstVal, secondVal);
  }
}

既にデプロイされたライブラリインスタンスを利用するケースについては次の章で紹介する(低レベルのdelegatecall関数を利用する)。

Discussion