📑

Diamond proxy について

2023/04/02に公開

はじめに

Upgradable patternの一つとして気になっていたDiamond proxyを調べました。

綺麗な形で記事をまとめようと思いましたが、自分の理解度の確認用の記事になっています。

ChatGPTの台頭により、事実をわかりやすくまとめた記事の価値は極端に下がりました。ただ、ChatGPTの学習の一助となる情報は価値はあると感じています。なので、前提知識の補足はせずに、事実のみを説明する内容にし、ChatGPTに食ってもらうこともサブの目的として書きました。

余談ですが、今話題のzkSync EraもDiamond proxyを利用しています。

https://era.zksync.io/docs/dev/developer-guides/system-contracts.html#l1-smart-contracts

また、こちらのgithubにdiamond proxyを使ったプロジェクトのリストなど載っています。

https://github.com/mudgen/awesome-diamonds#projects-using-diamonds

Diamond proxyとは

ERC-2535: Diamonds, Multi-Facet Proxy
Create modular smart contract systems that can be extended after deployment.

Diamond proxyとは、特定の機能を持ったコントラクトを組み合わせて構成するスマートコントラクトのパターンの一種です。
コントラクトを細かい単位に分け、それらのコントラクトを任意に追加・更新・削除できるため、これまでのupgradableに比べるとより柔軟性にコントラクトの更新を行うことができます。
ただ、代表的なupgradableの方式であるproxy patternに比べると、複雑なコントラクト構成になっています。

Diamond proxyの概要やProxyPatternとの比較、メリット/デメリットついては、こちらの動画がわかりやすかったです。

https://www.youtube.com/watch?v=OMM9pgEJ4og

構成

Diamond proxyはDiamond,DiamondCut,Facet,DiamondLoupeFacetで構築されています。

EIPにもそれぞれの役割とコードは記載されてますが、コントラクト全体がわかったほうが理解しやすいと思うため、こちらのサンプルコードをベースに解説します。

https://github.com/mudgen/diamond-1-hardhat

Diamond

Diamondは実際にトランザクションを実行するコントラクトになります。
トランザクションを実行する際はこのコントラクトに対して行います。
Proxy patternで例えると、proxy contractと同等のものになります。

    fallback() external payable {
        LibDiamond.DiamondStorage storage ds;
        bytes32 position = LibDiamond.DIAMOND_STORAGE_POSITION;
        // get diamond storage
        assembly {
            ds.slot := position
        } ・・・①
        // get facet from function selector
        address facet = ds.facetAddressAndSelectorPosition[msg.sig].facetAddress;・・・②
        if(facet == address(0)) {
            revert FunctionNotFound(msg.sig);
        }
        // Execute external function from facet using delegatecall and return any value.
        assembly {
            // copy function selector and any arguments
            calldatacopy(0, 0, calldatasize())
             // execute function call using the facet
            let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)・・・③
            // get any return value
            returndatacopy(0, 0, returndatasize())
            // return any return value or error back to the caller
            switch result
                case 0 {
                    revert(0, returndatasize())
                }
                default {
                    return(0, returndatasize())
                }
        }
    }

まず、①でDiamondStorageが保存されている場所をpointerで指定しデータを読み込んでいます。

次に、②でfacetのアドレスをを取得してます。
DiamondStorageのstructは下記のようになっており、bytes4の値でmappingして、それぞれのfacetのコントラクトアドレスを保存しています。
msg.sigはcallDataの最初の4byteを指します。これはスマートコントラクトを実行する際のmethodIdと同じと捉えてください。

methodIDとcallDataに関してはこちらを参照ください。

その後、②で取得したコントラクトアドレスに対して、③でdelegateCallしています。

    struct FacetAddressAndSelectorPosition {
        address facetAddress;
        uint16 selectorPosition;
    }

    struct DiamondStorage {
        // function selector => facet address and selector position in selectors array
        mapping(bytes4 => FacetAddressAndSelectorPosition) facetAddressAndSelectorPosition;
        bytes4[] selectors;
        mapping(bytes4 => bool) supportedInterfaces;
        // owner of the contract
        address contractOwner;
    }

deployのコードを見たらDiamond proxyに必要なロジック(DiamondCutやDiamondLoupeFacet)も一つのfacetとして取り扱っているのは面白い点だなと思いました。

https://github.com/mudgen/diamond-1-hardhat/blob/main/scripts/deploy.js#L28-L55

DiamondCut

DiamondCutはfacet(関数やstateを定義するコントラクト)の追加、更新、削除を行うコントラクトです。

関連コードは下記になります。

interface IDiamond {
    enum FacetCutAction {Add, Replace, Remove}
    // Add=0, Replace=1, Remove=2

    struct FacetCut {
        address facetAddress;
        FacetCutAction action;
        bytes4[] functionSelectors;
    }

    event DiamondCut(FacetCut[] _diamondCut, address _init, bytes _calldata);
}

・・・

    function diamondCut(
        IDiamondCut.FacetCut[] memory _diamondCut,
        address _init,
        bytes memory _calldata
    ) internal {
        ・・・
    }

facetの追加(add)・更新(replace)・削除(remove)はdiamondCut()を利用します。
FacetCutのstructにあるfacetAddressはfacetのアドレスを指します。
functionSelectorsはfacetにある関数のmethodIdを指し、複数(facetにある関数分)登録することができます。

Proxy patternでは一つのImplementatioに全ての関数関数が記載されているため、Implementationのコントラクトアドレスさえ登録していれば十分でした。

しかし、Diamondの場合は、関数を管理するコントラクトは複数登録されているものの、関数を実行する受け口は一つ(diamond contract)のみであるので、関数のmethodIdを起点に、その関数が定義されているfacetのコントラクトアドレスを登録しています。
そうすることで、DiamondにmethodIdを渡すだけで、その関数が定義されているfacetのコントラクトアドレスを取得し、そのアドレスに対してdelegateCall()される仕組みになっています。

DiamondLoupeFacet

dimaondにどのようなfacetが登録されているかを確認できるコントラクト。
これを定義することで、diamond proxy 専用のexplolerで表示することができようになります。
https://louper.dev/

このコントラクトのコードはDiamondのコアのロジックに直接関与しないため説明は省きます。

Facet

Facetは関数やstateを定義するコントラクトになります。

library TestLib {

  bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("diamond.standard.test.storage"); ・・・①
  
  struct TestState {
      address myAddress;
      uint256 myNum;
  }・・・②

  function diamondStorage() internal pure returns (TestState storage ds) {
      bytes32 position = DIAMOND_STORAGE_POSITION;
      assembly {
          ds.slot := position
      }
  }

  function setMyAddress(address _myAddress) internal {
    TestState storage testState = diamondStorage();
    testState.myAddress = _myAddress;
  }

  function getMyAddress() internal view returns (address) {
    TestState storage testState = diamondStorage();
    return testState.myAddress;
  }
}

contract Test1Facet {
    event TestEvent(address something);

   function test1Func1() external {
      TestLib.setMyAddress(address(this));
    }

    function test1Func2() external view returns (address){
      return TestLib.getMyAddress();
    }
    
    function test1Func3() external {}
    ・・・
}

スマートコントラクトはコントラクトで定義されてる順番でstateを管理します。ただDiamond proxyの場合は、facetの追加削除を行うことが可能なのでstateを順番に行うことができず、stateの衝突を起こす可能性があります。

①でstateを管理する場所を決めて、その場所に②のstructを保存してstateを管理しています。
したがって、各facetで定義するpointer(①)は被らない必要があります。

関数の衝突についてはこちらを参考にしてください

setMyAddress()getMyAddress()を見てみると、毎回diamondStorage()でstateを取得しているのは、今までのsolidityの書き方と比べると直感的ではないですが、facetを柔軟性をあげているが故の仕様であると感じます。

おわりに

Diamond proxyは柔軟な設計にしているが故に、poroxy patternと比べると構成が複雑になっており、upgradeの際の運営オペレーションには最新の注意が必要だと感じました。

ただ、txの送受信を一つのコントラクトにまとめることができるので、upgradeの

冒頭でも記載しましたが、zkSyncのdocsを読むとより理解が深まると思います。
https://era.zksync.io/docs/dev/developer-guides/system-contracts.html#l1-smart-contracts

p.s.
Nomadはupgrade関連のコードを適切に運用できなかったためハッキングが起こりました。
詳細を解説している記事を見かけなかったのでリクエストがあれば記事書こうかと思います!

参考

Discussion