ブロックチェーン学習ロードマップ

スマートコントラクトの不変性を乗り越える技術:アップグレード可能な設計パターン

Tags: スマートコントラクト, Solidity, 開発パターン, アップグレード, Proxyパターン, DELEGATECALL, Ethereum

はじめに:スマートコントラクトの不変性と課題

スマートコントラクトは一度ブロックチェーン上にデプロイされると、そのコードを変更することが非常に困難であるという「不変性」の特性を持っています。これはブロックチェーンの信頼性や透明性を支える重要な性質の一つです。例えば、イーサリアムのようなブロックチェーンでは、デプロイされたスマートコントラクトのアドレスに紐づくコードは基本的に変更できません。

この不変性のおかげで、ユーザーはコントラクトの実行結果が予測可能であり、特定の第三者による不正なコード変更のリスクを心配する必要がありません。しかし、現実の開発プロセスにおいては、この不変性が課題となる場合があります。

なぜスマートコントラクトのアップグレードが必要なのか?

プログラム開発において、バグは避けられないものです。スマートコントラクトも例外ではなく、デプロイ後に致命的なバグが発見される可能性があります。金融資産を扱うコントラクトであれば、その影響は計り知れません。また、サービスを継続的に提供していく上で、機能の追加や改善が必要になることもあります。

Webアプリケーションであれば、サーバー上のコードを更新することでこれらの課題に対応できます。しかし、不変なスマートコントラクトでは、デプロイ後にコードを直接修正することはできません。従来の不変なコントラクトの場合、これらの課題に対応するには、新たなコントラクトをデプロイし、ユーザーや関連するシステムに新しいコントラクトアドレスへの移行を促すという、非常に手間のかかる、そしてユーザー体験を損なう作業が必要になります。場合によっては、旧コントラクトにロックされた資産の移行なども考慮しなければなりません。

このような背景から、「不変性」というブロックチェーンの根本的な性質を維持しつつ、デプロイ後のコード変更(アップグレード)を技術的に可能にするための様々な設計パターンが考案されてきました。

アップグレード可能なコントラクトの基本的な考え方

スマートコントラクトの不変性を保ちながらアップグレードを実現するための基本的なアプローチは、ユーザーがやり取りするコントラクト(エントリーポイント)と、実際のロジックを実行するコントラクトを分離するという考え方です。

この構造により、ユーザーは常に同じエントリーポイントとなるコントラクトアドレスを通じてサービスを利用できますが、その裏側で実行されるロジックは最新のものに切り替わっている、という状態を作り出します。

技術的アプローチ:Proxyパターン

上記で述べた基本的な考え方を実装するための最も一般的な技術的アプローチがProxyパターンです。Proxyパターンは、スマートコントラクト開発で広く採用されており、特にERC-20やERC-721といったトークンのアップグレード可能な実装によく利用されています。

Proxyパターンとは?

Proxyパターンでは、主に以下の3つの要素が登場します。

  1. Proxyコントラクト: これがユーザーや他のコントラクトが最初にやり取りするエントリーポイントです。非常にシンプルで、自身にはほとんどロジックを持ちません。その主な役割は、受け取った関数呼び出しとデータを、実際のロジックを持つImplementation(またはLogic)コントラクトに「委譲(delegate)」することです。Proxyコントラクトは、現在有効なImplementationコントラクトのアドレスを内部に保持しており、管理者によってこのアドレスが更新可能になっています。
  2. Implementation (Logic) コントラクト: こちらがアプリケーションの実際のロジックが記述されているコントラクトです。アップグレードが必要になった場合は、このImplementationコントラクトの新しいバージョンをデプロイします。
  3. Client (ユーザー/他のコントラクト): Proxyコントラクトとやり取りを行う主体です。常に同じProxyコントラクトのアドレスを知っていればよく、背後でImplementationコントラクトが切り替わっても意識する必要はありません。

Proxyパターンの仕組み:DELEGATECALL

Proxyパターンの中核となる技術は、イーサリアム仮想マシン(EVM)の特別なオペコードであるDELEGATECALLです。

通常のコントラクト呼び出し(CALL)では、呼び出された側のコントラクトのコンテキスト(ストレージやmsg.senderなど)でロジックが実行されます。しかし、DELEGATECALLを使用すると、呼び出された側のコードが、呼び出し元のコントラクトのコンテキストで実行されます。

ProxyコントラクトがDELEGATECALLを使ってImplementationコントラクトを呼び出す場合、以下のようになります。

これにより、ユーザーはProxyコントラクトを呼び出しているにも関わらず、Proxyコントラクトのストレージにデータを保存したり、Proxyコントラクトの残高を操作したりすることが、Implementationコントラクトのロジックを通じて可能になります。そして、Implementationコントラクトのアドレスを別の新しいコントラクトに差し替えることで、Proxyコントラクトのストレージ(状態)を維持したまま、実行されるロジックだけを更新できるのです。

概念的なコードのイメージは以下のようになります。(Solidityの擬似コード)

// Proxyコントラクト (シンプルな例)
contract Proxy {
    address currentImplementation; // 現在のロジックコントラクトのアドレス

    // アドレスを更新する関数 (管理者のみ実行可能)
    function upgradeTo(address newImplementation) public onlyOwner {
        currentImplementation = newImplementation;
    }

    // fallback関数: どの関数が呼び出されてもここに到達
    // この関数内でDELEGATECALLを使ってロジックコントラクトに処理を委譲
    fallback(bytes calldata data) external payable returns (bytes memory) {
        // currentImplementationのアドレスにDELEGATECALLで処理を委譲
        // gas, value, data をそのまま渡す
        (bool success, bytes memory result) = currentImplementation.delegatecall(data);

        // DELEGATECALLの結果を返す
        require(success, "DELEGATECALL failed");
        return result;
    }

    // receive関数: Etherを受け取った場合に実行
    receive() external payable {
        fallback(msg.data);
    }
}

// Implementationコントラクト (実際のロジック)
contract MyLogic {
    uint256 public value; // Proxyのストレージに保存される

    function setValue(uint256 _value) public {
        value = _value; // 実際にはProxyのストレージを変更
    }

    function getValue() public view returns (uint256) {
        return value; // 実際にはProxyのストレージを読み込む
    }

    // 注意: このコントラクト自身はデプロイされるが、直接呼び出されることは想定されていない
    // 呼び出しは必ずProxy経由で行われる
}

上記の例では、ProxyコントラクトはcurrentImplementationというアドレスを保持し、受け取った全ての呼び出し(fallback関数)をそのアドレスにDELEGATECALLしています。MyLogicコントラクトはsetValuegetValueといった実際のビジネスロジックを持ちますが、DELEGATECALLによってProxyコントラクトのvalueというストレージ変数を操作します。

Proxyパターンの派生

Proxyパターンにはいくつか主要な派生形があり、それぞれストレージ管理や関数の衝突回避といった課題に対するアプローチが異なります。

どのパターンを採用するかは、プロジェクトの要件やリスク許容度によって決定されます。

Proxyパターン以外のアップグレード手法

Proxyパターン以外にも、アップグレードを実現するためのアイデアはいくつか存在します。

これらの手法と比較して、Proxyパターンは既存の状態を維持したままシームレスにロジックだけを切り替えやすいという利点から、最も広く採用されています。

アップグレード可能なコントラクト開発の考慮事項とリスク

アップグレード可能なコントラクトは柔軟性を提供しますが、同時にいくつかの技術的な考慮事項とリスクを伴います。

開発ツールとライブラリ

アップグレード可能なコントラクトを安全かつ効率的に開発するために、OpenZeppelinなどの標準的なライブラリがProxyパターンの実装を提供しています。これらのライブラリを使用することで、複雑なProxyロジックやストレージの互換性に関するベストプラクティスを容易に採用できます。OpenZeppelin Contracts Upgradeableライブラリは、Transparent ProxyやUUPSなど、様々なパターンの実装を提供しており、開発者はこれらのライブラリを継承して自身のロジックコントラクトを作成するのが一般的です。

まとめ

スマートコントラクトの不変性はブロックチェーンの信頼性の基盤ですが、バグ修正や機能追加といった開発上の要件から、デプロイ後のアップグレードが必要になることがあります。Proxyパターンをはじめとするアップグレード可能な設計パターンは、DELEGATECALLのようなEVMの低レベルな仕組みを利用することで、エントリーポイントとなるアドレスを固定したまま、背後で実行されるロジックを切り替える技術を提供します。

しかし、これらのパターンはストレージの互換性や権限管理、初期化処理といった複雑な課題を伴います。安全なアップグレード可能なコントラクトを開発するためには、これらの技術的な詳細を理解し、OpenZeppelinのような信頼できるライブラリを利用し、厳格なテストと検証プロセスを経ることが不可欠です。

ブロックチェーン開発において、アップグレード戦略はプロジェクトの継続性やセキュリティに深く関わる重要な要素です。この技術的な理解を深めることは、より実践的なコントラクト開発へ進むための重要なステップとなるでしょう。