見出し画像

【完全保存版】EIP1967についてしっかり学ぼう

こちらは、EIP1967の翻訳・編集をした記事です。

0 はじめに

代理コントラクト(以下、プロキシコントラクト)の委任は、更新可能性(以下、アップグレーダブル)とガス消費量の節約の両方のために広く使用されています。

翻訳者注
いきなり難しいですが、下の図がイメージしやすいと思います。
プロキシコントラクトは、その実装部分ロジックコントラクト任せています(委任しています。)
また、後ほど出てくる、ロジックコントラクトが既に存在している場合、ロジックコントラクトの内容を再度デプロイする必要がなくなるため、ガス代の節約につながります。

これらのプロキシは、delegatecallを使用して呼び出されるロジックコントラクトインプリメンテーションコントラクトまたはマスターコピーとも呼ばれる)に依存しています。

これにより、プロキシ永続的な状態(ストレージとバランス)を保持しつつ、コードはロジックコントラクトに委任されます。

プロキシとロジックコントラクトの間でストレージの使用が衝突しないように、ロジックコントラクトのアドレスは通常、コンパイラによって割り当てられることがないことが保証された特定のストレージスロット(例えば、OpenZeppelin契約の0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc)に保存されます。

このEIPは、プロキシ情報を保存するための標準スロットのセットを提案します。

これにより、ブロックエクスプローラーのようなクライアントは、この情報を適切に抽出し、エンドユーザーに表示することが可能となり、ロジックコントラクトは必要に応じてこの情報に基づいて行動することが可能となります。

1 動機

アップグレードをサポートし、デプロイメントのガスコストを削減する手段として、委任プロキシが広く使用されています。

OpenZeppelin Contracts, Gnosis, AragonOS, Melonport, Limechain, WindingTree, Decentralandなど、これらのプロキシの例が見られます。

しかし、プロキシからロジックアドレスを取得するための共通インターフェースの欠如により、この情報に基づいて行動する共通ツールを構築することは不可能です。

典型的な例はブロックエクスプローラーです。

ここでは、エンドユーザーはプロキシ自体ではなく、下層のロジックコントラクトと対話したいと考えています。

翻訳者注
ユーザーが直接やり取りをするのは、プロキシコントラクトであるものの、実際にやり取りをしたいのは、その背後にあるロジックコントラクトです。

プロキシからロジックコントラクトアドレスを取得する共通の方法があれば、ブロックエクスプローラーはプロキシコントラクトではなく、ロジックコントラクトのABIを表示できます。

エクスプローラーは、コントラクトの区別されたスロットのストレージをチェックして、それが実際にプロキシであるかどうかを判断し、その場合はプロキシとロジックコントラクトの両方の情報を表示します。

翻訳者注
ロジックコントラクトの格納場所が決まっていれば、そこを見ることで、プロキシコントラクトなのか、通常のコントラクトなのかがわかります。

例として、0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48Etherscan上でどのように表示されるか見てみましょう。

https://etherscan.io/address/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48#readProxyContract

別の例としては、自身がプロキシ化されていることを明確に認識し、それに基づいて行動するロジックコントラクトがあります。

これにより、ロジックの一部としてコードの更新を可能にすることができます。

一般的なストレージスロットを使用することで、これらのユースケースは使用されている具体的なプロキシ実装に独立して可能となります。

翻訳者注
つまり、プロキシはそのままで、実装だけ変更ができます。

2 仕様

プロキシのモニタリング(監視)は多くのアプリケーションのセキュリティにとって重要です。

したがって、実装スロット管理スロット変更を追跡する能力は必要不可欠です。

残念ながら、ストレージスロットの変更を追跡するのは容易ではありません。

したがって、これらのスロットのいずれかを変更する関数は、対応するイベントを発行すべき(SHOULD)です。

これには、初期化(0x0から、最初の非ゼロ値まで)のすべてが含まれます。

翻訳者注
つまり、0x0から別の値に変更となる、初期化の場合もイベントを発行しましょうという話です。

プロキシ固有の情報のための提案されたストレージスロットは次のとおりです。

追加の情報については、必要に応じて後続のERCでさらにスロットを追加できます。

翻訳者注
追加で標準化が必要になれば、同様の手順で追加ができます。

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

ストレージスロット0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc(bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)として取得)。

このプロキシが委任するロジックコントラクトのアドレスを保持します。

ビーコンが代わりに使用される場合、空であるべき(SHOULD)です。

翻訳者注
ビーコンは後ほど出てきます。
ロジックコントラクトか、ビーコンコントラクトはどちらか一つのみ実装されます。
そのため、ビーコンコントラクトが使用される場合は、ロジックコントラクトは空にする必要があります。

このスロットの変更は、以下のイベントによって通知されるべき(SHOULD)です。

event Upgraded(address indexed implementation);

翻訳者注
つまり、ロジックコントラクトが変更になった場合は、上のイベントが発生します。

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

ストレージスロット0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50
(bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)として取得)。

このプロキシが依存するビーコンコントラクトのアドレス(フォールバック)を保持します。

直接ロジックアドレスが使用される場合は空であるべき(SHOULD)であり、ロジックコントラクトスロットが空の場合にのみ考慮されるべきです。

このスロットの変更は以下のイベントによって通知されるべき(SHOULD)です。

event BeaconUpgraded(address indexed beacon);

ビーコンは、複数のプロキシのロジックアドレスを一か所で保持するために使用され、単一のストレージスロットを変更することで複数のプロキシをアップグレードすることを可能にします。

ビーコンコントラクトは以下の関数を実装する必要があります(MUST)

function implementation() returns (address) 

ビーコンベースのプロキシコントラクトロジックコントラクトスロットを使用しません

代わりに、彼らはビーコンコントラクトスロットを使用して、それらが接続されているビーコンのアドレスを保存します。

ビーコンプロキシが使用するロジックコントラクトを知るために、クライアントは以下を行うべき(SHOULD)です。

  1. ビーコンロジックストレージスロットビーコンのアドレスを読み取る

  2. ビーコンコントラクト上でのimplementation()関数を呼び出す。

ビーコンコントラクトのimplementation()関数の結果は、呼び出し元(msg.sender)に依存してはならない(SHOULD NOT)

3 管理者アドレス

ストレージスロット0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103(bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)として取得)。

このプロキシのロジックコントラクトアドレスをアップグレードすることが許可されたアドレスを保持します(任意)。

このスロットの変更は以下のイベントによって通知されるべき(SHOULD)です。

event AdminChanged(address previousAdmin, address newAdmin);

3 根拠

このEIPは、プロキシコントラクトの公開メソッドではなく、ロジックコントラクトアドレスのストレージスロットを標準化します。

これは、プロキシがロジックコントラクトと潜在的に衝突する可能性のある関数をエンドユーザーに公開するべきではないという理由からです。

翻訳者注
ここは若干自信がないですが、翻訳者の理解を書きます。

このEIPはメソッドを定義しているのではなく、あくまでもどのスロットに実装コントラクトを格納するのかしか指定していません。

もし仮に、EIPとして、「implementation()」という関数をプロキシコントラクトで指定した場合、実装コントラクトにも同じ、「implementation()」という関数を作ることで、衝突発生の可能性を生んでしまいます。

このEIP上には参考実装がありますが、あくまでも「参考」であり、それはEIPで指定しているわけではありません。

プロキシの実装をEIPでは指定しないことで、悪意のあるものがあえて衝突するための関数を作ることをしないようにしています。

ただ、それはあくまでも、EIPとしての責任の話であり、実情は、プロキシコントは基本的にverify済みでEtherscanなどで見ることができるので、エンドユーザーは見ることができるのではと考えました。

関数のセレクタには4バイトしか使わないため、異なる名前の関数間で衝突が発生する可能性があることに注意してください。

翻訳者注
例えば、関数セレクタは下のように求めます。
関数セレクタは先頭4バイトなので、重複する可能性があります。

ちなみに、4バイトということは、256 × 256 × 256 × 256約43億なので、重複する可能性は大きくはありませんが、ゼロではありません。


これは予期しないエラー、あるいは攻撃につながる可能性があり、プロキシが呼び出しを妨害して自身の値で応答するため、プロキシ化されたコントラクトへの呼び出しは予期した値とは異なる結果を返す可能性があります。

Nomic LabsによるEthereumプロキシの悪意あるバックドアから。

ロジックコントラクトと一致するセレクタを持つプロキシコントラクトの任意の関数は、直接呼び出され、完全にロジックコードをスキップします。

関数のセレクタは固定量のバイトを使用するため、常に衝突する可能性があります。

これは、Solidityコンパイラがコントラクト内のセレクタの衝突を検出するため、日々の開発では問題になりませんが、セレクタがクロスコントラクトインタラクションに使用されると、これが悪用可能になります。

衝突は、見かけ上は行儀の良いコントラクトを作成するために悪用できますが、実際にはバックドアを隠しています

翻訳者注
攻撃者が、あえて、同じ関数セレクタを持つ関数を作ったとしても、見た目上は問題のない関数に見えます。
(関数名が変かもしれませんが。)

プロキシの公開関数が潜在的に悪用可能であるという事実は、ロジックコントラクトアドレスを異なる方法で標準化する必要性を生じさせます。

選択されたストレージスロットの主な要件は、コンパイラによって任意のコントラクト状態変数を格納するために選択されてはならないことです。

そうでなければ、ロジックコントラクトは自身の変数に書き込むときに、誤ってプロキシのこの情報を上書きしてしまう可能性があります。

Solidityは、コントラクトの継承チェーンが直列化された後で、宣言された順序に基づいて変数をストレージにマップします。

最初の変数が最初のスロットに割り当てられ、以下同様です。

例外は、キーとストレージスロットの連結のハッシュに格納される、動的配列マッピングの値です。

Solidity開発チームは、新しいバージョン間でストレージレイアウトを維持することを確認しました。

翻訳者注
つまり、バージョンが変わっても、上のルールは変わらないと言うことだと翻訳者は考えました。

ストレージの状態変数のレイアウトは、ストレージポインターライブラリに渡されるため、Solidityの外部インターフェースの一部と見なされます。

これは、このセクションで概説したルールに対する任意の変更が言語の重大な変更と見なされ、その重要性から非常に慎重に考慮する必要があります。

そのような重大な変更が生じた場合、我々は互換性モードをリリースしたいと考えており、そのモードではコンパイラが古いレイアウトをサポートするバイトコードを生成します。

翻訳者注
ライブラリなどにもストレージのポインタが渡されるため、ストレージのレイアウトは非常に重要な情報になります。
そのため、レイアウトの変更は言語としての重大な変更となります。

VyperSolidityと同じ戦略に従っているようです。

他の言語で書かれたコントラクト、または直接アセンブリで、衝突が生じる可能性があることに注意してください。

翻訳者注
例えば、他の言語で、ロジックコントラクトのスロットに変数を作るような仕様がもしもあれば、衝突してしまいます。

彼らは、コンパイラによって割り当てられた状態変数と衝突しないことが保証されるように選択されており、ストレージインデックスで始まらない文字列のハッシュに依存しています。

さらに、ハッシュの事前画像が知られないように、-1のオフセットが追加され、可能な攻撃の可能性をさらに減らします。

4 参照実装

以下、EIP1967に載っている参照実装を翻訳し、そのまま載せます。

また、_delegate関数をその下で取り上げました。

/**
 * @dev このコントラクトはアップグレード可能なプロキシを実装します。
 * 呼び出しは変更可能な実装アドレスに委任されるため、アップグレードが可能です。
 * このアドレスは、https://eips.ethereum.org/EIPS/eip-1967[EIP1967]で指定された場所のストレージに格納されているため、
 * プロキシの背後にある実装のストレージレイアウトと衝突することはありません。
 */
contract ERC1967Proxy is Proxy, ERC1967Upgrade {
    /**
     * @dev 初期の実装を _logic で指定して、アップグレード可能なプロキシを初期化します。

     * もし _data が空でなければ、それは _logic へのデリゲートコールでデータとして使用されます。
     * これは通常、エンコードされた関数呼び出しとなり、Solidityのコンストラクタのようにプロキシのストレージを初期化することを可能にします。
     */
    constructor(address _logic, bytes memory _data) payable {
        assert(_IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1));
        _upgradeToAndCall(_logic, _data, false);
    }

    /**
     * @dev 現在の実装アドレスを返します。
     */
    function _implementation() internal view virtual override returns (address impl) {
        return ERC1967Upgrade._getImplementation();
    }
}
/**
 * @dev この抽象コントラクトはhttps://eips.ethereum.org/EIPS/eip-1967[EIP1967] に対して、
 * ゲッターとアップデート関数の発火を提供します。
 */
abstract contract ERC1967Upgrade {
    // これは"eip1967.proxy.rollback"のkeccak-256ハッシュから1を引いています。
    bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143;

    /**
     * @dev 現在の実装のアドレスとなるストレージスロット。
     * これは "eip1967.proxy.implementation" の keccak-256 ハッシュから1を引いたもので、
     * コンストラクタで検証されます。
     */
    bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

    /**
     * @dev 実装がアップグレードされたときに発行されます。
     */
    event Upgraded(address indexed implementation);

    /**
     * @dev 現在の実装アドレスを返します。
     */
    function _getImplementation() internal view returns (address) {
        return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value;
    }

    /**
     * @dev EIP1967実装スロットに新しいアドレスを保存します。
     */
    function _setImplementation(address newImplementation) private {
        require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract");
        StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;
    }

    /**
     * @dev 実装のアップグレードを実行します。
     * 
     *  {Upgraded} イベントを発行します。
     */
    function _upgradeTo(address newImplementation) internal {
        _setImplementation(newImplementation);
        emit Upgraded(newImplementation);
    }

    /**
     * @dev 追加のセットアップコールを伴う実装のアップグレードを実行します。
     * 
     *  {Upgraded} イベントを発行します。
     */
    function _upgradeToAndCall(
        address newImplementation,
        bytes memory data,
        bool forceCall
    ) internal {
        _upgradeTo(newImplementation);
        if (data.length > 0 || forceCall) {
            Address.functionDelegateCall(newImplementation, data);
        }
    }

    /**
     * @dev UUPSプロキシのセキュリティチェックと追加のセットアップコールを伴う実装のアップグレードを実行します。
     * 
     * {Upgraded} イベントを発行します。
     */
    function _upgradeToAndCallSecure(
        address newImplementation,
        bytes memory data,
        bool forceCall
    ) internal {
        address oldImplementation = _getImplementation();

        // 初期のアップグレードとセットアップコール
        _setImplementation(newImplementation);
        if (data.length > 0 || forceCall) {
            Address.functionDelegateCall(newImplementation, data);
        }

        // すでに進行中でなければロールバックテストを実行
        StorageSlot.BooleanSlot storage rollbackTesting = StorageSlot.getBooleanSlot(_ROLLBACK_SLOT);
        if (!rollbackTesting.value) {
            // 新しい実装からのupgradeToを使用してロールバックをトリガー
            rollbackTesting.value = true;
            Address.functionDelegateCall(
                newImplementation,
                abi.encodeWithSignature("upgradeTo(address)", oldImplementation)
            );
            rollbackTesting.value = false;
            // ロールバックが効果的だったかを確認
            require(oldImplementation == _getImplementation(), "ERC1967Upgrade: upgrade breaks further upgrades");
            // 最後に新しい実装にリセットし、アップグレードをログに記録
            _upgradeTo(newImplementation);
        }
    }

    /**
     * @dev 契約の管理者となるストレージスロット。
     *  これは "eip1967.proxy.admin" の keccak-256 ハッシュから1を引いたもので、コンストラクタで検証されます。
     */
    bytes32 internal constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;

    /**
     * @dev 管理者アカウントが変更されたときに発行されます。
     */
    event AdminChanged(address previousAdmin, address newAdmin);

    /**
     * @dev 現在の管理者を返します。
     */
    function _getAdmin() internal view returns (address) {
        return StorageSlot.getAddressSlot(_ADMIN_SLOT).value;
    }

    /**
     * @dev EIP1967の管理者スロットに新しいアドレスを保存します。
     */
    function _setAdmin(address newAdmin) private {
        require(newAdmin != address(0), "ERC1967: new admin is the zero address");
        StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin;
    }

    /**
     * @dev プロキシの管理者を変更します。
     *
     * {AdminChanged} イベントを発行します。
     */
    function _changeAdmin(address newAdmin) internal {
        emit AdminChanged(_getAdmin(), newAdmin);
        _setAdmin(newAdmin);
    }

    /**
     * @dev このプロキシの実装を定義するUpgradeableBeacon契約のストレージスロット。

     * これは bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)) で、コンストラクタで検証されます。
     */
    bytes32 internal constant _BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50;

    /**
     * @dev ビーコンがアップグレードされたときに発行されます。
     */
    event BeaconUpgraded(address indexed beacon);

    /**
     * @dev 現在のビーコンを返します。
     */
    function _getBeacon() internal view returns (address) {
        return StorageSlot.getAddressSlot(_BEACON_SLOT).value;
    }

    /**
     * @dev EIP1967のビーコンスロットに新しいビーコンを保存します。
     */
    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;
    }

    /**
     * @dev 追加のセットアップコールを伴うビーコンのアップグレードを実行します。
     * 注意:これはビーコンのアドレスをアップグレードするもので、ビーコンに含まれる実装をアップグレードするものではありません
     * (そのためには{UpgradeableBeacon-_setImplementation}を参照してください)。
     * 
     *  {BeaconUpgraded} イベントを発行します。
     */
    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);
        }
    }
}
/**
 * @dev この抽象契約は、EVMの命令 delegatecallを使用して全ての呼び出しを別の契約に委任するフォールバック関数を提供します。
 * 私たちは、プロキシの背後にある_実装_と呼ぶこの二つ目の契約を指定するために、仮想的な {_implementation}関数をオーバーライドする必要があります。
 * さらに、実装への委任は手動で{_fallback}関数を通じて、または異なる契約を通じて{_delegate}関数を通じてトリガーすることができます。
 * 委任された呼び出しの成功と戻りデータは、プロキシの呼び出し元に戻されます。 
  */
abstract contract Proxy {
    /**
     * @dev 現在の呼び出しを implementationに委任します。
     * この関数はその内部呼び出しサイトには戻らず、直接外部の呼び出し元に戻ります。
     */
    function _delegate(address implementation) internal virtual {
        assembly {
            // msg.dataをコピーします。このインラインアセンブリブロックではメモリを完全に制御します。
            // なぜなら、これはSolidityコードには戻らないからです。私たちは
            // メモリ位置0でのSolidityスクラッチパッドを上書きします。
            calldatacopy(0, 0, calldatasize())

            // 実装を呼び出します。
            // outとoutsizeは、まだサイズがわからないため、0です。
            let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)

            // 返されたデータをコピーします。
            returndatacopy(0, 0, returndatasize())

            switch result
            // delegatecallはエラー時に0を返します。
            case 0 {
                revert(0, returndatasize())
            }
            default {
                return(0, returndatasize())
            }
        }
    }

    /**
     * @dev これは仮想関数であり、フォールバック関数と{_fallback}が委任するアドレスを返すようにオーバーライドする必要があります。
     */
    function _implementation() internal view virtual returns (address);

    /**
     * @dev 現在の呼び出しを、_implementation()が返すアドレスに委任します。
     *
     * この関数は、内部の呼び出しサイトには戻らず、直接外部の呼び出し元に戻ります。
     */
    function _fallback() internal virtual {
        _beforeFallback();
        _delegate(_implementation());
    }

    /**
     * @dev 呼び出しデータが一致しない他の契約関数が存在しない場合に、_implementation()が返すアドレスに呼び出しを委任するフォールバック関数が実行されます。
     */
    fallback() external payable virtual {
        _fallback();
    }

    /**
     * @dev 呼び出しデータが空の場合、_implementation()が返すアドレスに呼び出しを委任するフォールバック関数が実行されます。
     */
    receive() external payable virtual {
        _fallback();
    }

    /**
     * @dev 実装にフォールバックする前に呼び出されるフック。手動で _fallback を呼び出す一部として発生するか、Solidityの fallback または receive 関数の一部として発生します。
     *
     * オーバーライドする場合は super._beforeFallback() を呼び出すべきです。
     */
    function _beforeFallback() internal virtual {}
}
/**
 * @dev 特定のストレージスロットに基本型を読み書きするためのライブラリ。
 * ストレージスロットは、アップグレード可能な契約を取り扱う際に、ストレージの競合を避けるためによく使われます。
 * このライブラリは、インラインアセンブリを必要とせずにそのようなスロットへの読み書きを支援します。
 * このライブラリの関数は、読み書きに使用できる value メンバーを含む Slot 構造体を返します。
 */
library StorageSlot {
    struct AddressSlot {
        address value;
    }

    struct BooleanSlot {
        bool value;
    }

    struct Bytes32Slot {
        bytes32 value;
    }

    struct Uint256Slot {
        uint256 value;
    }

    /**
     * @dev slotに位置するメンバーvalueを持つAddressSlotを返します。
     */
    function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {
        assembly {
            r.slot := slot
        }
    }

    /**
     * @dev slotに位置するメンバーvalueを持つBooleanSlotを返します。
     */
    function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) {
        assembly {
            r.slot := slot
        }
    }

    /**
     * @dev slotに位置するメンバーvalueを持つBytes32Slotを返します。
     */
    function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) {
        assembly {
            r.slot := slot
        }
    }

    /**
     * @dev slotに位置するメンバーvalueを持つUint256Slotを返します。
     */
    function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) {
        assembly {
            r.slot := slot
        }
    }

_delegate関数について

まずは、calldatacopyを使って、calldataメモリにコピーします。

これは、次のdelegatecallを行うために、一時的に行われています。

翻訳者注
calldatacopy(t, f, s)はEVMの組み込み関数であり、これは呼び出しデータ(calldata)をメモリにコピーするために使用されます。

この関数のパラメータは次のとおりです。
t:コピー先のメモリ開始位置
f:コピー元のcalldataの開始位置
s:コピーするcalldataのバイト数

したがって、calldatacopy(0, 0, calldatasize())は、呼び出しデータ(calldata)の開始から最後まで(calldatasize()は呼び出しデータのバイト数を返す)を、メモリの位置0から開始してコピーします。

これは、呼び出しデータをそのままdelegatecallに渡すために必要なステップです。

delegatecallは呼び出しデータをそのまま使用しますので、このコピーが必要となります。

次に、delegatecallを行います。

これは、呼び出し元のコントラクト(proxy)呼び出し先の関数を実行しています。

呼び出し先の情報は、上のcalldatacopyメモリにコピーされていました。

翻訳者注
delegatecallはEthereumの組み込み関数で、他のコントラクトの関数を呼び出すことができます。

しかも、呼び出した関数は元のコントラクトのコンテキスト(ストレージとバランス)で実行されます。delegatecallの引数は次の通りです。

gas():呼び出しに使用するガスの量。この場合、全ての利用可能なガスが使用されます。
implementation:呼び出し先のアドレス。ここではimplementationは呼び出すコントラクトのアドレスを表します。
0: メモリ内のデータが始まる位置を指します。これはcalldatacopyでコピーしたデータの開始位置を指します。
calldatasize():コールデータのサイズ。これは先にメモリにコピーされたmsg.dataの長さを表します。
00:呼び出しの戻りデータを格納するメモリの場所サイズ。これらは呼び出しの直後にはまだ知られていないため、0となっています。

最後にreturndatacopyで返り値をメモリにコピーします。

翻訳者注
returndatacopy(t, f, s)
は、fから始まりsバイト長の出力データをメモリのtにコピーします。

具体的には、returndatacopy(0, 0, returndatasize())という命令は、関数の返り値全体(returndatasize()は返り値の大きさをバイト単位で取得します)をメモリの0から始まる位置にコピーします。


/**
 * @dev 特定のストレージスロットに基本型を読み書きするためのライブラリ。
 * ストレージスロットは、アップグレード可能な契約を取り扱う際に、ストレージの競合を避けるためによく使われます。
 * このライブラリは、インラインアセンブリを必要とせずにそのようなスロットへの読み書きを支援します。
 * このライブラリの関数は、読み書きに使用できる value メンバーを含む Slot 構造体を返します。
 */
library StorageSlot {
    struct AddressSlot {
        address value;
    }

    struct BooleanSlot {
        bool value;
    }

    struct Bytes32Slot {
        bytes32 value;
    }

    struct Uint256Slot {
        uint256 value;
    }

    /**
     * @dev slotに位置するメンバーvalueを持つAddressSlotを返します。
     */
    function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {
        assembly {
            r.slot := slot
        }
    }

    /**
     * @dev slotに位置するメンバーvalueを持つBooleanSlotを返します。
     */
    function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) {
        assembly {
            r.slot := slot
        }
    }

    /**
     * @dev slotに位置するメンバーvalueを持つBytes32Slotを返します。
     */
    function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) {
        assembly {
            r.slot := slot
        }
    }

    /**
     * @dev slotに位置するメンバーvalueを持つUint256Slotを返します。
     */
    function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) {
        assembly {
            r.slot := slot
        }
    }

5 セキュリティ考慮事項

このERCは、選択されたストレージスロットSolidityコンパイラによって割り当てられないことに依存しています。

これにより、実装コントラクトが誤ってプロキシの操作に必要な情報のいずれかを上書きしないことが保証されます。

したがって、コンパイラによって割り当てられたスロットと衝突しないように、高いスロット番号の場所が選択されました。

翻訳者注
具体的には、ロジックコントラクトの場合は、
「0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc」
割り当てられていました。

また、既知の事前画像がない場所が選ばれました。

これは、悪意あるキーを用いたマッピングへの書き込みがそれを上書きできないようにするためです。

プロキシ固有の情報を修正しようとするロジックコントラクトは、特定のストレージスロットに書き込むことでこれを意図的に行う必要があります(UUPSの場合など)。

翻訳者注
これは、例えば、ロジックコントラクトを変更する場合は、EIP1967で指定されたストレージスロットの中身を変えることで意図的に変更するといった内容だと考えました。

6 著作権

CC0を経由して放棄された著作権および関連する権利。

7 引用

この文書を以下のように引用してください:
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.


サポートをしていただけたらすごく嬉しいです😄 いただけたサポートを励みに、これからもコツコツ頑張っていきます😊