20
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[ERC1967] コントラクトのアップグレードの仕組みについて理解しよう!

Last updated at Posted at 2023-08-08

はじめに

初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

代表的なゲームはクリプトスペルズというブロックチェーンゲームです。

今回は、コントラクトのアップグレードの標準規格を提案しているERC1967についてまとめていきます!

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

今回まとめているコントラクトのアップグレードについては以下の記事でもまとめています。

概要

デリゲートプロキシコントラクトは、スマートコントラクトのアップグレードとガスの節約に広く使用されている仕組みです。
これらのプロキシは、デリゲートコールと呼ばれる特殊な方法を使用して、ロジックコントラクト(または実装コントラクト、マスターコピーとも呼ばれる)を呼び出します。
デリゲートコールにより、プロキシはストレージデータや資金を保持しつつ、コードの実行をロジックコントラクトに委任することができます。

デリゲートコール(Delegatecall)については、以下を参照してください。

プロキシコントラクト
プロキシコントラクトは、データや資金を保持しているコントラクトで、デリゲートコールという特殊な方法を使用して、具体的な処理を実行するコントラクト(ロジックコントラクト)を呼び出すことができます。

ロジックコントラクト
一方、ロジックコントラクトは、データや資金を保持せずに具体的な処理を実行します。

プロキシとロジックコントラクトの間でストレージの使用に関する問題を避けるために、通常、ロジックコントラクトのアドレスはプロキシコントラクト内のストレージスロットに格納されます。

プロキシコントラクト内で、デリゲートコールと呼ばれる特殊な方法でロジックコントラクトを呼び出すときに、どのコントラクトを呼び出せば良いかというデータを保持しています。

たとえば、OpenZeppelinのコントラクトでは0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbcのようなアドレスが使用されます。
このアドレスは、コンパイラによって割り当てられることがない特別な場所であることが保証されています。

この提案(EIP)は、プロキシコントラクトに関する情報を管理するための新しい方法を導入するものです。
通常、プロキシコントラクトはロジックコントラクトを使用するため、そのプロキシがどのロジックコントラクトを利用しているかは大切な情報です。
しかし、現在の状態では、どのプロキシがどのロジックコントラクトに関連しているのかを一目で理解することは難しいです。

提案された新しい方法は、特定の場所(ストレージスロット)にプロキシと関連する情報を格納することです。
この情報を格納する場所は、コンパイラなどによって勝手に使われることのない特別な場所です。
この方法により、ブロックエクスプローラや他のクライアントは、プロキシの情報を簡単に取得し、ユーザーにわかりやすく表示できるようになります。
つまり、プロキシがどのコントラクトアドレスを呼び出しているのかを調べるのが簡単になります。

また、ロジックコントラクト自体も、自身がプロキシとして使用されていることを認識し、それに応じて動作することができます。
これは、ロジックコントラクトがプロキシと連携して特定の機能を提供したり、必要に応じてコードを更新したりするためのものです。

要するに、この提案は、デリゲートプロキシの取り扱いをより簡単にし、関連する情報を効果的に管理する方法を提供するものです。
これにより、プロキシとロジックコントラクトの関係が明確になり、プロキシの利用やアップグレードがスムーズに行えるようになります。

動機

プロキシがアップグレードのサポートとデプロイメントのガスコスト削減のために幅広く使用されている一方で、プロキシからロジックコントラクトのアドレスを取得する共通の方法が欠如しているため、この情報を活用するためのツールが不足していることです。

具体的な例として、ブロックエクスプローラが挙げられます。
ユーザーは通常、プロキシ自体ではなく、その裏にあるロジックコントラクトと対話したいと考えます。

具体的な処理がロジックコントラクトに書かれているためです。

しかし、現状ではプロキシからロジックコントラクトのアドレスを取得する共通の方法がないため、エクスプローラはロジックコントラクトのABI(Application Binary Interface)を正しく表示することが難しくなります。
提案された標準スロットを使用することで、ブロックエクスプローラなどのツールは、あるプロキシがどのロジックコントラクトを利用しているかを特定できるようになります。
これは、プロキシとそのバックエンドのロジックコントラクトとの関係性を明確にする手段です。

具体的に言うと、プロキシにはそのロジックコントラクトのアドレスが特定の場所(提案された標準スロット)に保存されます。
エクスプローラは、プロキシのアドレスをチェックして、その特定のスロットに保存されているアドレスを読み取ります。
このアドレスは、プロキシがどのロジックコントラクトを利用しているかを示しています。

ストレージデータおよび、スロットについては以下を参照してください。

Sample-proxy-on-etherscan.png
引用: https://eips.ethereum.org/EIPS/eip-1967

さらに、ロジックコントラクト側もプロキシコントラクトを認識し、特定のアクションを起こすことができるようになります。
この共通のストレージスロットは、プロキシの実装に依存せずに、このようなユースケースを実現するための基盤となります。

仕様

プロキシの監視は多くのアプリケーションのセキュリティにとって重要です。
そのため、実装スロットと管理者スロットへの変更を追跡できる機能を持つことが非常に重要です。
残念ながら、ストレージスロットの変更を追跡することは簡単ではありません。
そのため、これらのスロットのいずれかを変更する任意の関数は、対応するイベントを発行するべきです。

イベントとはログのことです。

これには、初期化や0x0から最初の非ゼロ値への変更も含まれます。

提案されたプロキシ固有情報のためのストレージスロットは以下の通りです。
追加の情報に必要に応じて、後続のERCでさらにスロットを追加することができます。

ロジックコントラクトアドレス

0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc」というストレージスロット(このアドレスは、eip1967.proxy.implementationkeccak256ハッシュから導き出されたものです)は、このプロキシが委任するロジックコントラクトのアドレスを保持します。
もしビーコンが代わりに使用されている場合は、このスロットは空であるべきです。
このスロットへの変更は、以下のイベントを介して通知されます。

event Upgraded(address indexed implementation);

このイベントは、ロジックコントラクトのアドレスが変更されたときに発行され、変更内容をトラッキングするためのものです。
このようにして、プロキシを通して新しいロジックコントラクトにアップグレードされた時に、プロキシの状態変更が他のコントラクトや外部ツールに通知されるようになります。

ビーコンコントラクトアドレス

0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50」というストレージスロット(このアドレスは、eip1967.proxy.beaconkeccak256ハッシュから導き出されたものです)は、このプロキシが依存するビーコンコントラクトのアドレスを保持します(フォールバック)。
もし直接的にロジックアドレスが使用される場合は、このスロットは空であるべきです。
また、ロジックコントラクトスロットが空である場合のみ、このビーコンコントラクトスロットが考慮されます。

event BeaconUpgraded(address indexed beacon);

このイベントは、ビーコンコントラクトのアドレスが変更された時に発行され、変更内容をトラッキングするためのものです。
ビーコンは、複数のプロキシのためにロジックアドレスを1つの場所に保持するために使用され、単一のストレージスロットを変更することで複数のプロキシをアップグレードできるようにします。
ビーコンコントラクトは、次の関数を実装する必要があります。

function implementation() returns (address)

ビーコンベースのプロキシコントラクトはロジックコントラクトスロットを使用せず、代わりにビーコンコントラクトスロットを使用して、接続されているビーコンのアドレスを保持します。
ビーコンプロキシが使用しているロジックコントラクトを知るために、クライアントは次の手順を取ります。

  1. ビーコンロジックのストレージスロットからビーコンのアドレスを読み取る。
  2. ビーコンコントラクト上のimplementation()関数を呼び出す。

また、ビーコンコントラクトのimplementation()関数の結果は、呼び出し元(msg.sender)に依存しないようにします。

アドミンアドレス

0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103」というストレージスロット(このアドレスは、eip1967.proxy.adminkeccak256ハッシュから導き出されたものです)は、このプロキシがロジックコントラクトのアドレスをアップグレードする権限を持つアドレスを保持します(オプション)。
このアドレスは、ロジックコントラクトのアップグレードを行うために必要なアドレスを指します。
このスロットへの変更は、以下のイベントを介して通知されます。

event AdminChanged(address previousAdmin, address newAdmin);

このイベントは、このプロキシのアップグレード権限を持つアドレスが変更された際に発生し、変更内容をトラッキングするためのものです。
これにより、アップグレード権限が変更された際の履歴が記録されます。

ロジックコントラクトのアドレスをアップグレードするとき、アップグレード権限を持つアドレス(アドミンアドレス)が指定されます。
これにより、特定のアドミンアドレスだけがロジックコントラクトのアップグレードを行うことができるようになります。
アドミンアドレスの変更が行われると、「AdminChanged」イベントが発行され、前のアドミンアドレスと新しいアドミンアドレスが記録されます。

この仕組みにより、プロキシとロジックコントラクトのセキュリティを強化し、アップグレードが適切な権限を持つ者によって行われることが確保されます。

補足

このEIPは、プロキシコントラクトの公開メソッドを使用せず、代わりにロジックコントラクトアドレスのストレージスロットを標準化することを提案しています。
その背景には、プロキシがロジックコントラクトの関数と衝突する可能性のある関数をエンドユーザーに公開すべきではないという考えがあります。

関数セレクターはABIで使用される際にわずかな4バイトしか使用されないため、異なる名前を持つ関数間でも衝突が発生する可能性があります。
このような衝突が起きると、予期しないエラーや悪意あるいは攻撃といった問題が発生する可能性があります。
例えば、プロキシコントラクトが関数呼び出しを傍受して独自の値を返すことで、本来の意図と異なる結果が返されることがあります。

関数セレクターの衝突については以下を参照してください。

Nomic Labsの「Malicious backdoors in Ethereum proxies」によれば、プロキシコントラクト内のセレクターが実装コントラクト内のセレクターと一致する場合、その関数は実装コードを完全にスキップして直接呼び出されます。
これにより、ロジックコントラクトの実装が無視され、予期しない動作が起こる可能性があります。

つまり、ユーザーはロジックコントラクト内の関数を実行しようとしたが、実際はプロキシコントラクト内の同じ関数セレクタの関数を実行してしまったということです。

このような問題を回避するために、プロキシコントラクトはロジックコントラクトの関数と衝突する可能性のある公開関数を持たず、代わりにロジックコントラクトのアドレスをストレージスロットに格納することが標準化されています。
これにより、エンドユーザーが意図せぬ問題に遭遇する可能性が減少し、プロキシとロジックコントラクトの間で予期せぬ挙動が起こるリスクが低減されます。

Any function in the Proxy contract whose selector matches with one in the implementation contract will be called directly, completely skipping the implementation code.
Because the function selectors use a fixed amount of bytes, there will always be the possibility of a clash. This isn’t an issue for day to day development, given that the Solidity compiler will detect a selector clash within a contract, but this becomes exploitable when selectors are used for cross-contract interaction. Clashes can be abused to create a seemingly well-behaved contract that’s actually concealing a backdoor.

プロキシの公開関数が潜在的に悪用される可能性があることから、ロジックコントラクトのアドレスを別の方法で標準化する必要性が生じました。

選ばれるストレージスロットの主要な要件は、コンパイラがコントラクトの状態変数を格納するために使用しないようにすることです。
そうしないと、ロジックコントラクトが自身の変数を書き込む際に、誤ってプロキシに保存されている情報を上書きしてしまう可能性があります。
つまり、プロキシが保持している重要な情報が失われてしまう可能性があります。

Solidityは、コントラクトの継承チェーンが線形化された後に変数が宣言された順序に基づいて、変数をストレージにマップします。
最初に宣言された変数は最初のスロットに、2番目に宣言された変数は2番目のスロットに割り当てられ、以後の変数も同様です。
ただし、動的配列やマッピングの値は、キーとストレージスロットの連結に基づいてハッシュされて保存されます。
このレイアウトにより、変数がどのスロットに格納されるかが決まります。

ストレージデータおよび、スロットについては以下を参照してください。

Solidityの開発チームは、新しいバージョン間でストレージのレイアウトを保持する方針を確認しています。
このため、プロキシコントラクトの情報を保存するために選ばれるストレージスロットは、将来のSolidityのアップデートに影響を受けることなく、安定して使用できるように考慮されています。

このようなアプローチにより、プロキシの公開関数が衝突や悪用のリスクから保護され、プロキシとロジックコントラクト間のデータの整合性とセキュリティが確保されることになります。

The layout of state variables in storage is considered to be part of the external interface of Solidity due to the fact that storage pointers can be passed to libraries. This means that any change to the rules outlined in this section is considered a breaking change of the language and due to its critical nature should be considered very carefully before being executed. In the event of such a breaking change, we would want to release a compatibility mode in which the compiler would generate bytecode supporting the old layout.

VyperもSolidityと同じ方針を取っており、プロキシのストレージスロットの選定はコンパイラがコントラクトの状態変数を格納するために使用するスロットと衝突しないように慎重に行われています。
このアプローチは、コントラクトのセキュリティを向上させるための重要な手段となっています。

これらのストレージスロットは、コンパイラによって割り当てられるコントラクトの状態変数との衝突を避けるために慎重に選ばれています。
この選択は、ハッシュ関数を使用して、文字列がストレージのインデックスから始まるハッシュに変換されることに依存しています。
このようなハッシュによるアプローチは、特定のパターンや文字列を使用して、一意のスロットを割り当てるための方法です。

さらに、-1のオフセットがハッシュに追加されているため、ハッシュの元の値を推測することが困難になっています。
これにより、攻撃者がストレージスロットの選定方法を逆算して攻撃を試みるリスクを低減しています。

このアプローチにより、VyperやSolidityなどのコンパイラが安全なストレージスロットを選定することで、コントラクトの挙動が予期しない問題や攻撃から守られることが保証されています。
ただし、他の言語で書かれたコントラクトやアセンブリで書かれたコントラクトは、同じルールに従っていない場合があり、衝突のリスクが残る可能性があるため、その点に注意が必要です。

ストレージデータの衝突については以下を参照してください。

参考実装

contract ERC1967Proxy is Proxy, ERC1967Upgrade {
    constructor(address _logic, bytes memory _data) payable {
        assert(_IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1));
        _upgradeToAndCall(_logic, _data, false);
    }

    function _implementation() internal view virtual override returns (address impl) {
        return ERC1967Upgrade._getImplementation();
    }
}

abstract contract ERC1967Upgrade {
    // This is the keccak-256 hash of "eip1967.proxy.rollback" subtracted by 1
    bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143;

    bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

    event Upgraded(address indexed implementation);

    function _getImplementation() internal view returns (address) {
        return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value;
    }

    function _setImplementation(address newImplementation) private {
        require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract");
        StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;
    }

    function _upgradeTo(address newImplementation) internal {
        _setImplementation(newImplementation);
        emit Upgraded(newImplementation);
    }

    function _upgradeToAndCall(
        address newImplementation,
        bytes memory data,
        bool forceCall
    ) internal {
        _upgradeTo(newImplementation);
        if (data.length > 0 || forceCall) {
            Address.functionDelegateCall(newImplementation, data);
        }
    }

    function _upgradeToAndCallSecure(
        address newImplementation,
        bytes memory data,
        bool forceCall
    ) internal {
        address oldImplementation = _getImplementation();

        // Initial upgrade and setup call
        _setImplementation(newImplementation);
        if (data.length > 0 || forceCall) {
            Address.functionDelegateCall(newImplementation, data);
        }

        // Perform rollback test if not already in progress
        StorageSlot.BooleanSlot storage rollbackTesting = StorageSlot.getBooleanSlot(_ROLLBACK_SLOT);
        if (!rollbackTesting.value) {
            // Trigger rollback using upgradeTo from the new implementation
            rollbackTesting.value = true;
            Address.functionDelegateCall(
                newImplementation,
                abi.encodeWithSignature("upgradeTo(address)", oldImplementation)
            );
            rollbackTesting.value = false;
            // Check rollback was effective
            require(oldImplementation == _getImplementation(), "ERC1967Upgrade: upgrade breaks further upgrades");
            // Finally reset to the new implementation and log the upgrade
            _upgradeTo(newImplementation);
        }
    }

    bytes32 internal constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;

    event AdminChanged(address previousAdmin, address newAdmin);

    function _getAdmin() internal view returns (address) {
        return StorageSlot.getAddressSlot(_ADMIN_SLOT).value;
    }

    function _setAdmin(address newAdmin) private {
        require(newAdmin != address(0), "ERC1967: new admin is the zero address");
        StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin;
    }

    function _changeAdmin(address newAdmin) internal {
        emit AdminChanged(_getAdmin(), newAdmin);
        _setAdmin(newAdmin);
    }

    bytes32 internal constant _BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50;

    event BeaconUpgraded(address indexed beacon);

    function _getBeacon() internal view returns (address) {
        return StorageSlot.getAddressSlot(_BEACON_SLOT).value;
    }

    function _setBeacon(address newBeacon) private {
        require(Address.isContract(newBeacon), "ERC1967: new beacon is not a contract");
        require(
            Address.isContract(IBeacon(newBeacon).implementation()),
            "ERC1967: beacon implementation is not a contract"
        );
        StorageSlot.getAddressSlot(_BEACON_SLOT).value = newBeacon;
    }

    function _upgradeBeaconToAndCall(
        address newBeacon,
        bytes memory data,
        bool forceCall
    ) internal {
        _setBeacon(newBeacon);
        emit BeaconUpgraded(newBeacon);
        if (data.length > 0 || forceCall) {
            Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data);
        }
    }
}

abstract contract Proxy {
    function _delegate(address implementation) internal virtual {
        assembly {
            // Copy msg.data. We take full control of memory in this inline assembly
            // block because it will not return to Solidity code. We overwrite the
            // Solidity scratch pad at memory position 0.
            calldatacopy(0, 0, calldatasize())

            // Call the implementation.
            // out and outsize are 0 because we don't know the size yet.
            let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)

            // Copy the returned data.
            returndatacopy(0, 0, returndatasize())

            switch result
            // delegatecall returns 0 on error.
            case 0 {
                revert(0, returndatasize())
            }
            default {
                return(0, returndatasize())
            }
        }
    }

    function _implementation() internal view virtual returns (address);

    function _fallback() internal virtual {
        _beforeFallback();
        _delegate(_implementation());
    }

    fallback() external payable virtual {
        _fallback();
    }

    receive() external payable virtual {
        _fallback();
    }

    function _beforeFallback() internal virtual {}
}

library StorageSlot {
    struct AddressSlot {
        address value;
    }

    struct BooleanSlot {
        bool value;
    }

    struct Bytes32Slot {
        bytes32 value;
    }

    struct Uint256Slot {
        uint256 value;
    }

    function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {
        assembly {
            r.slot := slot
        }
    }

    function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) {
        assembly {
            r.slot := slot
        }
    }

    function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) {
        assembly {
            r.slot := slot
        }
    }

    function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) {
        assembly {
            r.slot := slot
        }
    }
}

長すぎるので、分割して解説していきます。

ERC1967Proxy

contract ERC1967Proxy is Proxy, ERC1967Upgrade {
    constructor(address _logic, bytes memory _data) payable {
        assert(_IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1));
        _upgradeToAndCall(_logic, _data, false);
    }

    function _implementation() internal view virtual override returns (address impl) {
        return ERC1967Upgrade._getImplementation();
    }
}

EIP1967に基づいてアップグレード可能なプロキシコントラクトの実装です。
このプロキシコントラクトは、特定の実装アドレスに対する呼び出しをデリゲートし、実装アドレスを変更できる仕組みを提供します。
プロキシコントラクトは、スマートコントラクトのアップグレードによる変更を容易にするための設計です。

constructor

初期実装アドレス _logic と初期データ _data を受け取り、プロキシの初期化を行う関数。
_dataが空でないの場合、デリゲートコール内で _logic に対して実行されます。

_implementation

ERC1967Upgradeコントラクトから継承された関数をオーバーライドする関数。
現在の実装アドレスを返し、プロキシコントラクトはをデリゲートコールでこのアドレスを指定することで、ロジックコントラクト内の処理を呼び出せます。

ERC1967Upgrade

abstract contract ERC1967Upgrade {
    // This is the keccak-256 hash of "eip1967.proxy.rollback" subtracted by 1
    bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143;

    bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

    event Upgraded(address indexed implementation);

    function _getImplementation() internal view returns (address) {
        return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value;
    }

    function _setImplementation(address newImplementation) private {
        require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract");
        StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;
    }

    function _upgradeTo(address newImplementation) internal {
        _setImplementation(newImplementation);
        emit Upgraded(newImplementation);
    }

    function _upgradeToAndCall(
        address newImplementation,
        bytes memory data,
        bool forceCall
    ) internal {
        _upgradeTo(newImplementation);
        if (data.length > 0 || forceCall) {
            Address.functionDelegateCall(newImplementation, data);
        }
    }

    function _upgradeToAndCallSecure(
        address newImplementation,
        bytes memory data,
        bool forceCall
    ) internal {
        address oldImplementation = _getImplementation();

        // Initial upgrade and setup call
        _setImplementation(newImplementation);
        if (data.length > 0 || forceCall) {
            Address.functionDelegateCall(newImplementation, data);
        }

        // Perform rollback test if not already in progress
        StorageSlot.BooleanSlot storage rollbackTesting = StorageSlot.getBooleanSlot(_ROLLBACK_SLOT);
        if (!rollbackTesting.value) {
            // Trigger rollback using upgradeTo from the new implementation
            rollbackTesting.value = true;
            Address.functionDelegateCall(
                newImplementation,
                abi.encodeWithSignature("upgradeTo(address)", oldImplementation)
            );
            rollbackTesting.value = false;
            // Check rollback was effective
            require(oldImplementation == _getImplementation(), "ERC1967Upgrade: upgrade breaks further upgrades");
            // Finally reset to the new implementation and log the upgrade
            _upgradeTo(newImplementation);
        }
    }

    bytes32 internal constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;

    event AdminChanged(address previousAdmin, address newAdmin);

    function _getAdmin() internal view returns (address) {
        return StorageSlot.getAddressSlot(_ADMIN_SLOT).value;
    }

    function _setAdmin(address newAdmin) private {
        require(newAdmin != address(0), "ERC1967: new admin is the zero address");
        StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin;
    }

    function _changeAdmin(address newAdmin) internal {
        emit AdminChanged(_getAdmin(), newAdmin);
        _setAdmin(newAdmin);
    }

    bytes32 internal constant _BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50;

    event BeaconUpgraded(address indexed beacon);

    function _getBeacon() internal view returns (address) {
        return StorageSlot.getAddressSlot(_BEACON_SLOT).value;
    }

    function _setBeacon(address newBeacon) private {
        require(Address.isContract(newBeacon), "ERC1967: new beacon is not a contract");
        require(
            Address.isContract(IBeacon(newBeacon).implementation()),
            "ERC1967: beacon implementation is not a contract"
        );
        StorageSlot.getAddressSlot(_BEACON_SLOT).value = newBeacon;
    }

    function _upgradeBeaconToAndCall(
        address newBeacon,
        bytes memory data,
        bool forceCall
    ) internal {
        _setBeacon(newBeacon);
        emit BeaconUpgraded(newBeacon);
        if (data.length > 0 || forceCall) {
            Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data);
        }
    }
}

EIP1967に基づいてアップグレード可能なスマートコントラクトの実装に関する抽象的なコントラクです。
このコントラクトは、EIP1967のスロットに関連する情報を取得し、イベントを発行するための関数を提供しています。

_ROLLBACK_SLOT

アップグレード時のロールバックテストの進行状況を格納する変数。
ロールバックテストは、新しい実装によるアップグレードが失敗した場合に、以前の実装に戻すためのものです。

_IMPLEMENTATION_SLOT

現在の実装アドレスを格納する変数。
プロキシコントラクトは、このスロットに格納された実装アドレスに対する呼び出しをデリゲートすることで機能します。

_ADMIN_SLOT

プロキシコントラクトの管理者アカウントのアドレスを格納する変数。
アップグレード可能なコントラクトの管理者は、コントラクトのアップグレードなどの重要な操作を行うための権限を持ちます。

_BEACON_SLOT

]Beaconパターンを使用したアップグレード可能なコントラクトの場合に、現在のBeaconアドレスを格納する変数。
Beaconパターンは、プロキシコントrかうとがアップグレード可能なコントラクトを指し示すための方法です。

_getImplementation

現在の実装アドレスを取得する関数。

_setImplementation

新しい実装アドレスを指定して、プロキシコントラクトの実装アドレスを更新する関数。

_upgradeTo

新しい実装アドレスにアップグレードする関数。
アップグレードが行われると、Upgradedイベントが発行されます。

_upgradeToAndCall

新しい実装アドレスにアップグレードし、データを使用して追加のセットアップ呼び出す関数。
必要に応じて、forceCalltrueの場合にデリゲートコールも実行されます。

_upgradeToAndCallSecure

新しい実装アドレスにアップグレードし、セキュリティチェックを含む追加のセットアップ呼び出す関数。
ロールバックテストも行い、アップグレードが正常に行われたか確認します。

_getAdmin

現在の管理者アカウントのアドレスを取得する関数。

_setAdmin

新しい管理者アカウントのアドレスを指定して、管理者アカウントを更新する関数。

_changeAdmin

プロキシコントラクトの管理者を変更する関数。
変更が行われると、AdminChangedイベントが発行されます。

_upgradeBeaconToAndCall

Beaconアップグレードを実行し、新しいBeaconアドレスに対してセットアップ呼び出しを行う関数。

Proxy

abstract contract Proxy {
    function _delegate(address implementation) internal virtual {
        assembly {
            // Copy msg.data. We take full control of memory in this inline assembly
            // block because it will not return to Solidity code. We overwrite the
            // Solidity scratch pad at memory position 0.
            calldatacopy(0, 0, calldatasize())

            // Call the implementation.
            // out and outsize are 0 because we don't know the size yet.
            let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)

            // Copy the returned data.
            returndatacopy(0, 0, returndatasize())

            switch result
            // delegatecall returns 0 on error.
            case 0 {
                revert(0, returndatasize())
            }
            default {
                return(0, returndatasize())
            }
        }
    }

    function _implementation() internal view virtual returns (address);

    function _fallback() internal virtual {
        _beforeFallback();
        _delegate(_implementation());
    }

    fallback() external payable virtual {
        _fallback();
    }

    receive() external payable virtual {
        _fallback();
    }

    function _beforeFallback() internal virtual {}
}

フォールバック関数を提供し、EVMのdelegatecall命令を使用して他のコントラクトに呼び出しを委譲する抽象的なコントラクトです。
このコントラクトを使用することで、プロキシを介して別の実装コントラクトに対する呼び出しを行うことができます。

_delegate

現在の呼び出しを指定された実装アドレスに対してdelegatecall命令を使用して委譲する関数。
delegatecallの成功やエラーに応じて、戻り値を処理します。

_implementation

フォールバック関数や委譲関数が呼び出しを委譲する対象の実装アドレスを返すための関数。
この関数はオーバーライドして実装アドレスを指定する必要があります。

_fallback

フォールバック関数として機能し、呼び出しを指定された実装アドレスに対して委譲する関数。
_beforeFallback関数が呼び出されます。

fallback

フォールバック関数と同様の機能を提供しますが、外部呼び出しのエントリポイントとして使用される関数。

コントラクト内に呼び出された関数がない時に呼び出される関数です。
この関数の中でdelegatecallを呼び出します。

receive

fallback関数と同様に、フォールバック関数の機能を提供しますが、コールデータが空の場合に使用される関数。

_beforeFallback

フォールバック前に呼び出されるフック関数。
オーバーライドすることで、特定のアクションを実行できます。

StorageSlot

library StorageSlot {
    struct AddressSlot {
        address value;
    }

    struct BooleanSlot {
        bool value;
    }

    struct Bytes32Slot {
        bytes32 value;
    }

    struct Uint256Slot {
        uint256 value;
    }

    function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {
        assembly {
            r.slot := slot
        }
    }

    function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) {
        assembly {
            r.slot := slot
        }
    }

    function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) {
        assembly {
            r.slot := slot
        }
    }

    function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) {
        assembly {
            r.slot := slot
        }
    }
}

プリミティブなデータ型の読み書きを特定のストレージスロットに行うためのライブラリです。
アップグレード可能なコントラクトでストレージの競合を避けるために使用されることがあります。
このライブラリは、インラインアセンブリを使用せずに、このようなスロットへの読み書きを行うのに役立ちます。

AddressSlot

valueメンバーを持つaddress型のストレージスロット。

BooleanSlot

valueメンバーを持つbool型のストレージスロット。

Bytes32Slot

valueメンバーを持つbytes32型のストレージスロット。

Uint256Slot

valueメンバーを持つuint256型のストレージスロット。

getAddressSlot

指定されたスロットに位置するAddressSlotを返す関数。

getBooleanSlot

指定されたスロットに位置するBooleanSlotを返す関数。

getBytes32Slot

指定されたスロットに位置するBytes32Slotを返す関数。

getUint256Slot

指定されたスロットに位置するUint256Slotを返す関数。

セキュリティ

このERCは、アップグレード可能なプロキシの実装を提供するものです。
プロキシは、外部の呼び出しを別の実装アドレスに委任することで、アップグレード可能な機能を提供します。
このプロキシには、アップグレードされる実装アドレスが格納されており、その格納場所はERC1967というイーサリアムの改善提案によって指定されています。
このようにすることで、プロキシの背後にある実装のストレージレイアウトと競合することがありません。

セキュリティの観点から、この実装はソリディティコンパイラが割り当てることのないストレージスロットを利用しています。
これにより、プロキシの正常な動作に必要な情報が誤って上書きされるリスクを回避しています。
高いスロット番号が選択されている理由は、コンパイラによって割り当てられるスロットとの競合を最小限に抑えるためです。
また、事前のイメージが存在しないスロットが選ばれていることで、悪意のあるキーを使ったマッピングの書き込みによってもプロキシの情報が上書きされることがありません。

プロキシに関連する情報を変更するためには、実装コントラクトが特定のストレージスロットに対して意図的に書き込む必要があります。
これはアップグレード可能なプロキシをサポートするための一般的なアプローチであり、UUPS(Universal Upgradeable Proxy Standard)と呼ばれるアップグレード方法でも同様です。
これにより、プロキシの安全な運用が確保され、予期しないアップグレードや情報の上書きを防ぎます。

引用

Santiago Palladino (@spalladino), Francisco Giordano (@frangio), Hadrien Croubois (@Amxx), "ERC-1967: Proxy Storage Slots," Ethereum Improvement Proposals, no. 1967, April 2019. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-1967.

最後に

今回は「コントラクトのアップグレードの標準規格を提案しているERC1967」についてまとめてきました!
いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

Twitter @cardene777

採用強化中!

CryptoGamesでは一緒に働く仲間を大募集中です。

この記事で書いた自分の経験からもわかるように、裁量権を持って働くことができて一気に成長できる環境です。
「ブロックチェーンやWeb3、NFTに興味がある」、「スマートコントラクトの開発に携わりたい」など、少しでも興味を持っている方はまずはお話ししましょう!

20
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?