🕌

cryptozombiesでSolidityについて学ぶ part3

2025/03/10に公開

はじめに

今回も引き続きcryptozombiesでSolidityの基礎を学んでいきます。ガス代やコントラクトの不変性はスマートコントラクトを実装していく上で非常に重要な概念だと思います。

前回の記事はこちら。

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

DAppと普通のアプリケーションの違い

イーサリアムのDApp(ブロックチェーン上で動作するアプリケーション)は普通のアプリケーション(Webアプリやモバイルアプリなど)とは違うところがいくつかあります。

  • 中央管理者がいない

DAppはブロックチェーン上で動作し、特定の企業や組織による管理者はいません。普通のアプリケーションでは、一般的に企業や個人がサーバーやデータを管理しています。

例を挙げると、TwitterではTwitter社が管理しており、アカウントの凍結や投稿の削除が可能ですがDAppではスマートコントラクト上で動作し、管理者がいないのでアカウントの凍結や投稿の削除はできません。

  • データの改竄ができない

コントラクトにデプロイしたコードは永久にブロックチェーン上に残るため、コードの編集や削除ができなくなります。コントラクトに何かしら不具合があっても修正する手段がないため、不具合を直した新しいコントラクトを使うしかなくなるのです。

このことからSolidityではセキュリティが大変重要になっています。

  • 取引にガス代が必要

普通のアプリケーションではデータベースの書き込みに費用は発生することはほとんどありませんが、DAppではブロックチェーンにデータを記録するたびに、ガスと呼ばれる通貨を支払うことになっています。ユーザーはEther(イーサと呼び、イーサリアムの通貨)でガスを買い、アプリの関数を実行します。

アクセス制御について

part1externalの関数はコントラクトの外部から呼び出すことができると説明しましたが、何も制限をかけていないと外部から誰でも呼べるようになっています。😇

流石にこれではセキュリティ的によろしくないのでアクセス制御を入れるのが一般的です。

その制御方法としてSolidityのライブラリにあるOpenZeppelinOwnableコントラクトです。OpenZeppelinを使用すると、必要最小限の実装で済む他、監査済みのコードになっているので、セキュリティ的にも安全性を確保できます。

実際OpenZeppelinのドキュメントを確認しましたが、関数の内部の処理がどうなっているか把握できなかったので、実際にコードを見た方が早いかもしれません。(CryptoZombiesにもOwnableの実装が載っていますがv0.4.19でかなり古く、今の書き方とは違うところがあるのでこちらを載せています)

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol

part1, part2でも触れていなかったものがあるのでこちらで説明します。

  • constructor: constructorはコンストラクトです。コントラクトが最初に作成された時に、1度だけ実行される特別な関数です。 ※v0.4.22より前のバージョンでは、コンストラクタはコントラクトと同じ名前の関数として定義されていました。 この構文は非推奨で、バージョン0.5.0以降ではもう認められていません。(参照)
  • modifier:Solidityの関数に対して事前条件を追加できる機能です。

このコードだとonlyOwnerは最初オーナーだけが関数を実行できるかチェックし、_;に辿り着いたらrenounceOwnership関数に戻ってコードを実行するようになります。

// modifierの定義
modifier onlyOwner() {
  _checkOwner();
  // ここで関数の本体を実行
  _;
 }

// onlyOwnerを適用
function renounceOwnership() public virtual onlyOwner {
  // オーナーだけが実行できる
}

時間の単位

Solidityには、block.timestamap変数があり、現在のunixタイムスタンプ(1970年1月1日から経過した秒数)を返します。ブロックチェーンの性質上、完全に正確というわけではないため数秒の誤差は許容する必要があります。

※ v0.7.0まではblock.timestampではなく現在の時間はnow変数で取得していましたがnowという名前がその値の意味や制限を誤解を招くということで非推奨となり使えなくなっています。(参照)

また seconds、 minutes、 hours、 daysweeks という時間を扱いやすくするためのエイリアスも用意されています。

これらのエイリアスは以下の通りです。

  • seconds: 1秒 (基本単位)
  • minutes: 60秒
  • hours: 3,600秒 (60分)
  • days: 86,400秒 (24時間)
  • weeks: 604,800秒 (7日)

注意点として、 yearsはうるう年の複雑さのためにv0.5.0で削除されていたり(参照)、小数点は使うことができないです。(1.5 hoursという書き方はできないということです)

ガス代についてさらに詳しく

なぜSolidityがガス代を必要なのか

Solidityにはガス代という関数を実行するための手数料がかかるというのはここまで何回か説明してきましたが、実際なぜSolidityがガス代を必要とするのでしょうか?

理由はイーサリアムの計算リソースは有限だからです。イーサリアムのブロックチェーンは世界中のノード(コンピュータ)によって分散管理されています。スマートコントラクトを実行すると、各ノードが計算を実行する必要があるため、リソースが消費されます。

そこでガス代の役割としては、

  • 不要な計算の実行を防ぐ(スパム対策)
  • ネットワークの混雑対策や維持
  • セキュリティの維持

になります。

どういった処理にガス代がかかるのか

Solidityでは処理内容によってガス代の消費量が大きく変わります。特にストレージへの書き換え処理はガス代が多くかかります。

なぜかというと、データを書き込んだり、変更するたびに、それがすべてブロックチェーンに永久に書き込まれるからです。世界中の何千個というノードがすべてそのデータをハードドライブに書き込む必要があり、そのデータ容量はブロックチェーンが成長すればするほど大きくなるためどうしてもコストが高くなります。

ストレージのガス代を節約する方法

  • ストレージの更新回数を減らす

Solidity では、storage の代わりに memory を使うことで、ガス代を節約 できます。

ガス代が高くなってしまう例

このコードでは2回ストレージに更新を行っています。

contract StorageExample {
    struct User {
        string name;
        uint256 age;
    }
    
    // マッピング型はストレージに格納する
    mapping(address => User) public users;

    function setUser(string memory _name, uint256 _age) public {
		    // ストレージの書き換え
        users[msg.sender].name = _name;
        users[msg.sender].age = _age;    
    }
}

改善例

ストレージの更新回数を減らします。

contract MemoryExample {
    struct User {
        string name;
        uint256 age;
    }
    
    mapping(address => User) public users;

    function setUser(string memory _name, uint256 _age) public {
		    // 1回だけstorageからmemoryへ
        User memory user = users[msg.sender];
        user.name = _name;
        user.age = _age;
        // ストレージへの書き込みを1回だけに最適化
        users[msg.sender] = user;  
    }
}
  • メモリーに一時的に配列を作成し必要ならストレージに書き込む

ガス代が高くなってしまう例

ストレージに配列を直接書き込んでいます。

contract ExpensiveStorage {
  uint256[] public numbers;
  
  function addNumbers(uint256[] memory newNumbers) public {
    for (uint256 i = 0; i < newNumbers.length; i++) {
     // ストレージに直接書き込むためガス代が高い
     numbers.push(newNumbers[i]); 
    }
  }
}

改善例

配列の操作をメモリーで完結させて、ストレージの書き込みを最小限にしています。

contract OptimizedStorage {

	uint256[] public numbers;
	function addNumbers(uint[] memory newNumbers) public {
    uint256 length = numbers.length;
    uint256[] memory tempNumbers = new uint[](length + newNumbers.length);

    // 既存のデータをメモリーにコピー
    for (uint i = 0; i < length; i++) {
        tempNumbers[i] = numbers[i];
    }

    // 新しいデータをメモリーに追加
    for (uint i = 0; i < newNumbers.length; i++) {
        tempNumbers[length + i] = newNumbers[i];
    }

    // まとめてストレージに書き込み(ガス代節約)
    numbers = tempNumbers;
  }
}

View 関数はガスコストが不要

ただ全ての処理でガス代がかかるわけではありません。

view関数を外部から呼び出す場合、ガスは一切かかりません。なぜかというと、view 関数がただデータ参照をし、ブロックチェーン上でなにも変更しないからです。(ただしトランザクションを生成すると全てのノードで実行する必要があり、ガスが必要になります。)

可能な場合は読み取り専用のexternal view関数を使うことで、DAppのガス使用量を最適にすることが出来ます。

contract Example {
    mapping(uint256 => string) private data;
    
		// 外部からのみ呼び出されるならexternal viewが良い
    function getData(uint256 id) external view returns (string memory) {
        return data[id];
    }
}

ループ処理

Solidityのループ処理はforwhiledo...while の3つの方法があります。記述方法もJavascriptとほとんど同じです。

ただ、ループを使用する際もガス代のコストを気をつける必要があります。大量のデータを処理するとガス代が高くなり、トランザクションが失敗する可能性があります。

  • for文
// 配列の合計を計算
contract ForLoopExample {
    function sumArray(uint[] memory numbers) public pure returns (uint) {
        uint total = 0;
        for (uint i = 0; i < numbers.length; i++) {
            total += numbers[i];
        }
        return total;
    }
}
  • while文
// 偶数の数を数える
contract WhileLoopExample {
    function countEvenNumbers(uint[] memory numbers) public pure returns (uint) {
        uint count = 0;
        uint i = 0;
        while (i < numbers.length) {
            if (numbers[i] % 2 == 0) {
                count++;
            }
            i++;
        }
        return count;
    }
}
  • do-while文
// カウントダウン
contract DoWhileLoopExample {
    function countdown(uint start) public pure returns (uint) {
        do {
            start--;
        } while (start > 0);
        return start;
    }
}

まとめ

今回でSolidityの基礎は大体学べたと思います!今後もDApp作成のためしっかり学んでいきます。Solidityはユーザーの負担を減らすためにもガス代を考慮した実装をするのがとても大切だということを実感しました。

ここまで見ていただき、ありがとうございました。👏

Discussion