⛓️

EIP-2535 Diamond Standard

13 min read

本書について

  • EIP-2535の要約です。詳細はオリジナルを確認してください。
    自分が理解するのが目的であるため、一部省略や補足等を行っております。
  • https://eips.ethereum.org/EIPS/eip-2535
  • 末尾にサンプルコードによる動作確認を載せています。

Diamond Standardとは

  • モジュール式のGas効率の高い方法で、サイズ制限なしでスマートコントラクトを作成できるようにします。

  • Diamondは、既存の機能を再デプロイすることなく、その場でアップグレードできます。

  • Diamondはコントラクトのインターフェースと実装の詳細を標準化し、ソフトウェアの統合と相互運用性を可能にします。

  • Diamond Standardは、この規格の仕様を実装するスマートコントラクトです。
    本書では Diamond と呼ぶことにします。

Diamond Standardで実現できること

  • スマートコントラクトの最大サイズ24Kbyteを超えたスマートコントラクトを実装可能になる
  • スマートコントラクトのコードとデータを整理する方法を提供する
    • 複数のスマートコントラクトで、同一のPublic変数にアクセス可能。
  • 機能をアップグレードする方法を提供する

Diamond Standard本体が持つ機能

  • Diamondには、 ルーペ関数 と呼ばれる4つの標準関数がある。
  • ルーペ関数 は次の用途で使用できます。
    1. ダイヤモンドが使用するすべての機能を表示します。
    2. Etherscanやファイルなどのサービスにクエリを実行して、ダイヤモンドが使用するすべてのソースコードを取得して表示します。
    3. Etherscanやファイルなどのサービスにクエリを実行して、ダイヤモンドのABI情報を取得します。
    4. ダイヤモンドの関数を追加/置換/削除するトランザクションが成功したことをテストまたは検証します。
    5. 関数を呼び出す前に、ダイヤモンドが持つ機能を確認する。
    6. ダイヤモンドを展開およびアップグレードするためにツールおよびプログラミングライブラリによって使用されます。
    7. ダイヤモンドに関する情報を表示するためにユーザーインターフェイスで使用されます。
    8. ユーザーがダイアモンドの関数を呼び出せるようにするためにユーザーインターフェイスで使用されます。

Facet

  • 外部のスマートコントラクトを呼び出し可能です。この外部コントラクトのことを Facet と呼びます。
  • Facetは、内部関数、ライブラリ、および状態変数を共有できる個別の独立したコントラクトです。

制約事項

  • 複数のスマートコントラクトで、同一の関数名は定義できない。
    (publicにした変数を含む。)
    • Facetの識別子が関数名(のシグネチャ)なので、重複してしまうとエラーになる。
  • 同じ関数名を2回登録はできない。
    • 変更の場合は、変更用のパラメータを使ってDiamondCutしてください。

Diamond Standardが持つ関数群

関数名 用途
diamondCut Facetの関数を追加/変更/削除する
diamondLoope Diamondが持つ機能と、使用可能なFacetを取得する
OwnerShip 認証、所有権のスキーム

Diamondの仕組み

  • Diamondは、Facetの関数セレクターのマッピングを格納しています。
  • Diamondでは外部関数が呼び出されると、Diamondのフォールバック関数が実行されます。
  • フォールバック関数は、 selectorToFacet によって呼び出された関数を持つFacetをマッピングで見つけ、 delegatecall を使用してFacetから外部関数を実行します。
  • delegatecallはDiamondのフォールバック機能であり、Diamondが外部のFacetの関数を独自の外部関数として実行できるようにします。
  • Diamondのストレージを読み書きしただけでは、msg.sender , msg.value の値は変更できません。

DiamondCut

  • Diamondは、DiamondCut関数を使用して、1回のトランザクションで任意の数のFacetから、任意の数の関数を追加/変更/削除できます。
  • DiamondCut関数セレクターのアドレスマッピングを更新します。
  • 外部関数が追加/変更/削除されるたびに、イベントが発行されます。
  • DiamondCutから置換/削除の機能を省くことで、Diamondは不変的な実装をすることができます。(ContractのUpgradeを禁止する実装)

Facetと状態変数

  • Facetは外部関数を定義し、内部関数、ライブラリ、および状態変数を定義または使用できます。

  • Facetは構造体で状態変数を宣言できます。それらの構造体はコントラクトのストレージ領域の特定のポジションが与えられます。この手法は DiamondStorageと呼ばれています。

  • FacetでDiamondStorageを使用する例

// A contract that implements diamond storage.
// DiamondStorageを実施するコントラクト
library LibA {

  // This struct contains state variables we care about.
  // 状態変数の含まれる構造体
  struct DiamondStorage {
    address owner;
    bytes32 dataA;
  }

  // Returns the struct from a specified position in contract storage
  // ContractStorageの指定したポジションから構造体を返す関数
  // ds is short for DiamondStorage
  // dsはDiamondStorageの省略形とします。
  function diamondStorage() internal pure returns(DiamondStorage storage ds) {
    // Specifies a random position from a hash of a string
    // 文字列のハッシュからランダムな位置を指定します。
    bytes32 storagePosition = keccak256("diamond.storage.LibA")
    // Set the position of our struct in contract storage
    assembly {ds.slot := storagePosition}
  }
}

// Our facet uses the diamond storage defined above.
// 自分たちのFacetは上で定義したDiamondStorageを使用します。
contract FacetA {

  function setDataA(bytes32 _dataA) external {
    // DiamondStorageを取得。msg.senderがds.ownerであれば更新を許可
    LibA.DiamondStorage storage ds = LibA.diamondStorage();
    require(ds.owner == msg.sender, "Must be owner.");
    ds.dataA = _dataA
  }

  // DiamondStorage内の値を参照する
  function getDataA() external view returns (bytes32) {
    return LibDiamond.diamondStorage().dataA
  }
}
  • Facetは、それぞれ異なる格納位置を持つ任意の構造体を使用できます。
  • DiamondStorage Facetを使用することで、他のFacetで宣言された状態変数の格納された場所と競合しない独自の状態変数を宣言できます。

Diamondは他のスマートコントラクトのストレージ領域を使用できます。

  • DiamondとFacetは、DiamondStorageを使用する必要はありません。コントラクトの継承など、他のコントラクトのストレージを使用できます。

Facetは状態と機能を共有できます

  • Facetは同じストレージ格納位置の同じ構造体を使用することにより、状態変数を共有できます。
  • Facetは同じコントラクトを継承するか、同じライブラリを使用することで、内部関数とライブラリを教諭できます。
  • これらの方法では、Facetは独立したユニットになりますが、状態と機能を共有することができます。

Facetは再利用可能

  • deploy済みの1つのFacetは、任意の数のDiamondから使用することができます。
  • 多くのDiamondに同じデプロイ済みFacetが使用できるため、デプロイコストが削減できます。
  • 制約事項としては、任意のスマートコントラクトが同じ関数シグネチャを持つ2つの外部関数を持つことができないため、同じ関数シグネチャを持つ外部関数を同時にDiamondに追加できないことです。

ダイアグラム

Diamondの構造


※引用元: https://eips.ethereum.org/EIPS/eip-2535

ダイアモンドストレージ

  • 次の図は、FacetとFacet間で共有されるデータを示しています。
  • 全てのデータがDiamondに保存されています。

    ※引用元: https://eips.ethereum.org/EIPS/eip-2535

デプロイされたFacetは再利用可能

  • 展開されたFacetは、任意の数のDiamondで使用できます。
  • 次の図は、同じ2つのFacetを使用する2つのDiamondを示しています。
    • FacetAはDiamond1によって使用されます
    • FacetAはDiamond2によって使用されます
    • FacetBはDiamond1によって使用されます
    • FacetBはDiamond2によって使用されます

      ※引用元: https://eips.ethereum.org/EIPS/eip-2535

サンプルコード

  • Diamondの実装は3種類用意されている
  • それぞれの実装モデル毎による差分は以下の表のとおりである。
Implementation diamondCut complexity diamondCut gas cost loupe complexity loupe gas cost
diamond-1 low medium medium high
diamond-2 high low high high
diamond-3 medium high low low

※引用元: https://github.com/mudgen/Diamond

diamond-1を使用したサンプルコードの解読

migrations/2_diamond.jsのコードから読み解く

  • 使用するスマートコントラクトのプログラムを読み込む
const Diamond = artifacts.require('Diamond')
const DiamondCutFacet = artifacts.require('DiamondCutFacet')
const DiamondLoupeFacet = artifacts.require('DiamondLoupeFacet')
const OwnershipFacet = artifacts.require('OwnershipFacet')
const Test1Facet = artifacts.require('Test1Facet')
const Test2Facet = artifacts.require('Test2Facet')
  • diamondCutで外部関数を 追加/変更/削除のアクション定義
const FacetCutAction = {
  Add: 0,
  Replace: 1,
  Remove: 2
}
  • ABI情報から関数のシグネチャを抽出する関数
function getSelectors (contract) {
  const selectors = contract.abi.reduce((acc, val) => {
    if (val.type === 'function') {
      acc.push(val.signature)
      return acc
    } else {
      return acc
    }
  }, [])
  return selectors
}
  • デプロイ実行
    • まず、 Test1Facet, Test2Facetをデプロイする。
    • DiamondCutFacet, DiamondLoupeFacet, OwnershipFacetをそれぞれ独立したコントラクトとしてデプロイする
    • DiamondCutFacet, DiamondLoupeFacet, OwnershipFacetの関数シグネチャを調べ、デプロイした各コントラクトのアドレスとともに、diamondCut変数に追加する。
    • Diamond本体をデプロイする際に、上記diamondCutの変数と、コントラクト所有者となるaccounts[0]のBCアドレスを引数に付けてデプロイする。
module.exports = function (deployer, network, accounts) {
  deployer.deploy(Test1Facet)
  deployer.deploy(Test2Facet)

  deployer.deploy(DiamondCutFacet)
  deployer.deploy(DiamondLoupeFacet)
  deployer.deploy(OwnershipFacet).then(() => {
    const diamondCut = [
      [DiamondCutFacet.address, FacetCutAction.Add, getSelectors(DiamondCutFacet)],
      [DiamondLoupeFacet.address, FacetCutAction.Add, getSelectors(DiamondLoupeFacet)],
      [OwnershipFacet.address, FacetCutAction.Add, getSelectors(OwnershipFacet)]
    ]
    return deployer.deploy(Diamond, diamondCut, [accounts[0]])
  })
}

動作確認のためにFacetを修正

  • 異なるスマートコントラクトの関数が、同一のストレージ変数にアクセスできることを確認する。
  • 複数のコントラクトで同一のコントラクトを継承する。変数定義のみをしたコントラクトを contracts/facets/storage.solとして実装した。
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;

contract Storage {
  uint256 hoge = 0;
}
  • contracts/facets/Test1Facet.solを修正し、上記変数hogeを読み書き可能にさせる。
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;

import './storage.sol';

contract Test1Facet is Storage {
    event TestEvent(address something);

    function test1Func1() external {
        hoge = 1;
    }

    function test1Func2() external view returns(uint256){
        return hoge;
    }
(以下略)
  • 同様にcontracts/facets/Test2Facet.solも修正し、上記変数hogeを読み書き可能にさせる。
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;

import './storage.sol';

contract Test2Facet is Storage {

    function test2Func1() external {
        hoge = 3;
    }

    function test2Func2() external view returns(uint256){
        return hoge;
    }
  • 呼び出すプログラムは以下の通りとする。
//const { ethers } = require("ethers"); // ethers非対応, 関数のシグネチャが取得できず。
const Web3EthContract  = require('web3-eth-contract');
const Diamond = require('./build/contracts/Diamond.json')
const DiamondLoupeFacet = require('./build/contracts/DiamondLoupeFacet.json')
const DiamondCutFacet = require('./build/contracts/DiamondCutFacet.json')
const OwnershipFacet = require('./build/contracts/OwnershipFacet.json')
const Test1Facet = require('./build/contracts/Test1Facet.json')
const Test2Facet = require('./build/contracts/Test2Facet.json')

const FacetCutAction = {
  Add: 0,
  Replace: 1,
  Remove: 2
}

const zeroAddress = '0x0000000000000000000000000000000000000000'
const fromAddress = '0xb94AB5113541d61F5aC84Daa8A31c80FF31962f5'

// コントラクトから関数の識別コードを取得する。
function getSelectors (contract) {
  const selectors = contract._jsonInterface.reduce((acc, val) => {
    if (val.type === 'function') {
      acc.push(val.signature)
      return acc
    } else {
      return acc
    }
  }, [])
  //console.log(selectors)
  return selectors
}


(async () => {
  Web3EthContract.setProvider('http://localhost:8545');

  // ダイアモンド
  const diamondAddress = Diamond.networks[Object.keys(Diamond.networks)[0]].address
  const diamondContract = new Web3EthContract(Diamond.abi, diamondAddress)

  // ダイアモンドルーペファセット(ダイアモンドのアドレスに、ルーペファセットのABI)
  const diamondLoupeFacetAddress = DiamondLoupeFacet.networks[Object.keys(DiamondLoupeFacet.networks)[0]].address
  const diamondLoupeFacetContract = new Web3EthContract(DiamondLoupeFacet.abi, diamondAddress)

  // ダイアモンドカット(ダイアモンドのアドレスに、ルーペファセットのABI)
  const diamondCutFacetAddress = DiamondCutFacet.networks[Object.keys(DiamondCutFacet.networks)[0]].address
  const diamondCutFacetContract = new Web3EthContract(DiamondCutFacet.abi, diamondAddress)

  // オーナーシップファセット(ダイアモンドのアドレスに、ルーペファセットのABI)
  const ownershipFacetAddress = OwnershipFacet.networks[Object.keys(OwnershipFacet.networks)[0]].address
  const ownershipFacetContract = new Web3EthContract(OwnershipFacet.abi, diamondAddress)

  // テストファセット(ダイアモンドのアドレスに、ルーペファセットのABI)
  const test1FacetAddress = Test1Facet.networks[Object.keys(Test1Facet.networks)[0]].address
  const test1FacetContract = new Web3EthContract(Test1Facet.abi, diamondAddress)

  const test2FacetAddress = Test2Facet.networks[Object.keys(Test2Facet.networks)[0]].address
  const test2FacetContract = new Web3EthContract(Test2Facet.abi, diamondAddress)

  
  
  
  try {
    // compile済みのjsonファイルを与えると、関数識別子の一覧が返る
    let selectors = getSelectors(test1FacetContract).slice(0, -1)
    // 関数の識別子をdiamond standardに登録する
    //diamondContract.diamondCut([[追加するデプロイ済みcontractAddreess, FacetCutAction.Add, ABIの関数郡]], nullのアドレス, '0x')
    await diamondCutFacetContract.methods.diamondCut([[test1FacetAddress, FacetCutAction.Add, selectors]], zeroAddress, '0x').send({ from: fromAddress, gas: 100000000, gasPrice: 0 })

    selectors = getSelectors(test2FacetContract).slice(0, -1)
    await diamondCutFacetContract.methods.diamondCut([[test2FacetAddress, FacetCutAction.Add, selectors]], zeroAddress, '0x').send({ from: fromAddress, gas: 100000000, gasPrice: 0 })

  } catch (error) {
    console.log(error)
  }


  // テストファセットの関数呼び出し
  console.log(await test1FacetContract.methods.test1Func2().call({ from: fromAddress, gas: 100000000, gasPrice: 0 }))
  console.log(await test2FacetContract.methods.test2Func2().call({ from: fromAddress, gas: 100000000, gasPrice: 0 }))
  
  // 値の更新
  await test1FacetContract.methods.test1Func1().send({ from: fromAddress, gas: 100000000, gasPrice: 0 })
  // test1Funcだけ変えても、test2Funcでも変更結果を取得可能
  console.log(await test1FacetContract.methods.test1Func2().call({ from: fromAddress, gas: 100000000, gasPrice: 0 }))
  console.log(await test2FacetContract.methods.test2Func2().call({ from: fromAddress, gas: 100000000, gasPrice: 0 }))

  
})()
  • 実行すると、初期状態ではtest1Func2, test2Func2ともに0が返るが、test1Functest1FacetContractの値のみを1に変更すると、test2Func2でも1が取得可能になる。
  • したがって、2つのコントラクトでストレージ変数を共有していることが確認できた。
$ node setup.js 
0
0
1
1

サンプルコード

参考情報

Discussion

ログインするとコメントできます