インキュベータの技術的解説

谷口 英
所要時間, 9分
初級

あいさつ

Frame00で開発に従事しています。谷口です。
今回は、先日リリースされた新機能、インキュベータの技術的な解説をしたいと思います。

概要

詳細

ロール設定 permalink

インキュベータは管理者、ストレージ管理者、オペレータという3つのロールで運用すること想定して作成されています。
管理者はストレージ管理者とオペレータの権限を含む全ての機能の実行権限を合わせ持ち、ストレージ管理者とオペレータはそれぞれに関連する機能の実行権限を持っています。
インキュベータはこのロール設定機能をOpenZeppelinのAccessControlで実装しています。
OpenZeppelinとはSolidityのライブラリです。OpenZeppelinを利用することにより、Solidityを用いたコントラクトの開発効率を劇的に上昇させることができます。
その中にAccessControlというコントラクトがあり、これを利用することにより、ロールを分けた運用を想定したコントラクトを実装することができます。
AccessControlはさまざまな機能が実装されており、ちょっと複雑だったので、必要な機能のみをシンプルに実装できるように、ラッピングしたコントラクトを作成しました。
publicで公開していますので、もしよければ(自己責任で)ご利用ください。

// インストールコマンド
npm install @devprotocol/util-contracts
// 実装例
pragma solidity >=0.7.6;

import {Admin} from "@devprotocol/util-contracts/contracts/access/Admin.sol";

// Adminコントラクトを継承することにより、addAdmin、deleteAdmin、isAdmin関数が利用できます。
// 必要に応じて、Admin権限を保持するユーザの追加、削除、確認ができます。
contract Logic is Admin {

// AdminコントラクトはAccessControlを継承していますので、状況に応じて、ロールの新規作成、及びアドレスとの紐付けができます。
bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");

// Logicコントラクトのコンストラクタが実行された後、Adminコントラクトのコンストラクタが実行されます。
// そのタイミングでデプロイ作業者はAdmin権限が自動で付与されます。
constructor() {
// Admin権限が付与されると、Operator権限も付与されるように設定しています。
_setRoleAdmin(OPERATOR_ROLE, DEFAULT_ADMIN_ROLE);
// デプロイ作業者はオペレータの権限も付与しています。
grantRole(OPERATOR_ROLE, _msgSender());
}

// Operator権限チェック用修飾子
modifier onlyOperator {
require(isOperator(_msgSender()), "operator only.");
_;
}

// onlyOperatorを付与することにより、Operator権限をもつユーザからのみ実行できます
function testFunc() external pure onlyOperator returns (uint256){
return 100;
}

// Operator権限の有無を確認できます。
function isOperator(address account) public view returns (bool) {
return hasRole(OPERATOR_ROLE, account);
}

// Operator権限を与えることができます。onlyAdminを付与されているので、Admin権限がないとこの関数は実行できません。
function addOperator(address _operator) external onlyAdmin {
grantRole(OPERATOR_ROLE, _operator);
}

// Operator権限を取り上げることができます。
function deleteOperator(address _operator) external onlyAdmin {
revokeRole(OPERATOR_ROLE, _operator);
}
}

OpenZeppelinにはOwnableというコントラクトもあります。
役割が管理者とそれ以外という2パターンしかない場合は、AccessControlではなくOwnableを使います。
その方が実装がシンプルなので、デプロイコストや実行コストが下がり、運用しやすくなります。

エターナルストレージ permalink

インキュベータはアップグレイダブルに設計しています。
外部コントラクトにデータを保持することにより、インキュベータ本体のロジックの不具合を発見したり、仕様変更が必要だった場合、必要に応じてプログラムを修正し、ストレージを付け替え、運用を継続することができます。
この仕組みを「エターナルストレージ」といいます。私が考えたわけではなく、有名なSolidityのデザインパターンの一つです。簡単にいうとただのハッシュマップです。
ブロックチェーンにデプロイされたコントラクトは修正不可能なので、Webアプリケーションのように継続的に運用していきたい場合、この仕組みを利用します。
非常に有用なデザインパターンですが、デメリットもあります。別コントラクトにデータを書き込むので、ガス代が高くなります。実際の運用を考えて、メリットとデメリットを比較し、利用の有無を決める必要があります。
先述のAdminコントラクトと同様、util-contractsとして公開しているので、利用したい場合は(自己責任で)ご利用ください。

// インストールコマンド
npm install @devprotocol/util-contracts
// 実装例
pragma solidity >=0.7.6;

import {SafeMath} from "@openzeppelin/contracts/math/SafeMath.sol";
import {UsingStorage} from "@devprotocol/util-contracts/contracts/storage/UsingStorage.sol";

// UsingStorageを継承したコントラクトを作成します。
contract Logic is UsingStorage {
// オーバーフロー対策として、計算処理はSafeMathを利用します。
using SafeMath for uint256;

function setValue(string memory _key, uint256 _value_) internal
{
// ここでは例としてuint256を保存していますが、addressでもstringでもbooleanでも大丈夫です。
// 詳細はUsingStorageの内部で生成しているEternalStorageコントラクトをご参照ください。
eternalStorage().setUint(getKey(_key), _price);
}

function getValue(string memory _key) public view returns (uint256)
{
return eternalStorage().getUint(getKey(_key));
}

function getKey(string memory _key) private pure returns (bytes32)
{
// EVMではStorageに保存する際必ず32byteのデータとして保存します。
// そのため、保存されるデータ容量と使用されるgas量を考えると、32byteで設定するのが一番効率が良くなります。
return keccak256(abi.encodePacked("_key", _key));
}

// add関数を実行するたびに数値をインクリメントしていく関数です。
// 内部変数ではなく外部ストレージに直を保存することにより、継続して運用することができます。
function add(string memory _key) external {
uint256 tmp = getValue(_key);
tmp = tmp.add(1);
setValue(_key, tmp);
}
}

運用例 permalink

[初回デプロイ時]

  1. Logicコントラクトをデプロイした後、createStorage関数を実行し、ストレージを作成します。

[2回目以降デプロイ時]

  1. Logicコントラクトの仕様変更を行いたい場合、プログラムを修正したLogicコントラクトをまずはデプロイします。
  2. 古いLogicコントラクトのgetStorageAddress関数を実行し、EternalStorageのアドレスを取得します。
  3. 新しいLogicコントラクトのsetStorage関数を実行し、EternalStorageのアドレスをセットします。
  4. 古いLogicコントラクトのchangeOwner関数を実行し、ストレージの書き込み権限を新しいLogicコントラクトに委譲します。

インターフェース permalink

Solidityにおけるインターフェースとは、コントラクトの外から実行できる関数の定義を記述したものになります。

// 例 Dev ProtocolのAllocatorコントラクトのインターフェース
// SPDX-License-Identifier: MPL-2.0
pragma solidity >=0.5.17;

interface IAllocator {
function beforeBalanceChange(
address _property,
address _from,
address _to

) external;

function calculateMaxRewardsPerBlock() external view returns (uint256);
}

インターフェース自体にはexternalしか記述できませんが、実際のコントラクトがpublicであっても問題はありません。
(継承した場合を除く)
インターフェースを使うことの最大のメリットは依存関係を解決できるところです。

// SPDX-License-Identifier: MPL-2.0
pragma solidity >=0.7.6;

interface ILogic {
function hogehoge() external;
}
// SPDX-License-Identifier: MPL-2.0
pragma solidity >=0.7.6;

import {SafeMath} from "@openzeppelin/contracts/math/SafeMath.sol";
import {ILogic} from "./ILogic.sol";

contract Logic is ILogic{
using SafeMath for uint256;
uint256 private counter;

function hogehoge() external {
counter = counter.add(1);
}
}
// 例 インターフェースを使う場合
// SPDX-License-Identifier: MPL-2.0
pragma solidity >=0.7.6;

import {ILogic} from "./ILogic.sol";

contract Logic2UseInterface {

address private logic = 0x00000.........;

function hogehoge() external {
ILogic(logic).hogehoge();
}
}
// 例 インターフェースを使わない場合
// SPDX-License-Identifier: MPL-2.0
pragma solidity >=0.7.6;

import {Logic} from "./Logic.sol";

contract Logic2NotUseInterfase {

address private logic = 0x00000.........;

function hogehoge() external {
Logic(logic).hogehoge();
}
}

インターフェースを使う場合、呼び出し先(ここでいうLogic.hogehoge())の中で何をしているか意識する必要はありません。
もし、Logic2NotUseInterfaseのflatten file(*1)を作成する場合、Logic2コントラクト、Logicコントラクト、及びそこから参照されているSafeMathライブラリが含まれます。
もしSafeMathライブラリが別の大きなコントラクトやライブラリを参照している場合、それも含めなければいけません。
そうなってくるとそこから生成されるバイトコードのサイズも大きくなり、デプロイ時のガスコストも肥大化し、運用面で大きな枷になってしまいます。
逆にLogic2UseInterfaseのflatten fileを作成する場合、Logic2コントラクトとILogicインターフェースのみで大丈夫です。
バイトコードのサイズも小さくなり、運用コストも少なくなります。
インターフェースがあれば本体のコントラクトがなくてもビルドができるなど、実装コストの減少というメリットもあります。
インキュベータもそうなのですが、Dev Protocolのインターフェースを利用することにより、この恩恵を受けています。
Dev Protocolのインターフェースは誰でも利用できるように公開していますので、皆様ももしよければご利用ください。

*1 Etherscanにプログラミングコードを登録するために、関係する全てのsolファイルを一つにまとめたファイル

// インストールコマンド
npm install @devprotocol/protocol
// 実装例
pragma solidity >=0.7.6;

import {IDev} from "@devprotocol/protocol/contracts/interface/IDev.sol";

contract Logic {

address private devToken = 0x5cAf454Ba92e6F2c929DF14667Ee360eD9fD5b26

function lockup(address _property, uint256 _staking) external {
// ステーキング!
IDev(devToken).deposit(_property, _staking);
}
}

まとめ

他にもいろいろありますが、代表的なものを記述しました。
Solidityだけではなくテストケースの書き方や運用のノウハウもありますので、順次公開していきます。

🌈 この記事はお役に立ちましたか?

今後より良いコンテンツをお届けしていくために、ぜひご質問やフィードバックなどいただけると幸いです🌱
フォーラムはこちら

- Dev Protocol は全てOSSとして公開しています。ぜひIssueやPRを送ってください📢 時にバウンティがあります。
Dev ProtocolのGitHubはこちら

- Dev Protocol の改善提案(DIP)プロセスも公開されています。ぜひコメントをお待ちしています🌟
DIPはこちら