OlympusV3 の契約を読んでいると、アーキテクチャの設計が私に深い印象を与えました。アーキテクチャは明確で、スマートコントラクトの領域で高い凝集度と低い結合度の設計思想を実現しており、それによりモジュールごとに深く独立して読むことができ、心の負担が軽減されています。
OlympusV3 のアーキテクチャ図は以下のようになります:
後でこのフレームワークが Olympus 独自のものではないことを知りました。Olympus が契約アーキテクチャを紹介する記事でDefault フレームワークが言及されています。
スマートコントラクトアーキテクチャの複雑さ#
Default フレームワークを紹介する前に、著者は伝統的なプロトコルである AAVE MakerDAO のアーキテクチャ図を比較しました。
アーキテクチャ図だけでは、開発者は呼び出しの経路を明確にすることは難しいです。著者はこの設計を、プロセスを中心に取り組むアプローチとして説明しています。
Today, most projects take a process-centric approach towards protocol development: each smart contract is developed to manage a process along a chain of logic that is triggered when external interactions occur.
したがって、ビジネスのアップグレードや交代に伴い、論理チェーンの複雑さは指数関数的に増加します。これは、プロトコル全体がネットワーク状のアーキテクチャを持っているためであり、おそらく 2 つのポイントの間には論理的なチェーンが存在するため、1 つのポイントを追加するたびに、複雑さが増加し、予測およびカバーが困難になります。
ソフトウェアエンジニアリングの設計パターンの進化プロセスと同様に、スマートコントラクトのエンジニアリングアーキテクチャも改善され続けており、Default Framework はこの問題に取り組んでいます。
The Default Framework とは何ですか#
名前だけでは設計思想がわかりませんし、Diamond Frameworkのように連想を起こすこともありません。The Default Framework は特に革新的なものではなく、伝統的な開発領域の設計思想を借りて、コントラクト全体をレイヤー化しています。
- Modules:内部向けで、モジュールはマイクロサービスと考えることができ、各モジュールはデータの一部を管理し、相互に依存関係はなく、高度に独立しており、モジュールへのアクセスはすべてアクセス制御によって制約されています。
- Policy:外部呼び出し向けで、内部で異なるモジュールを組織するためのもので、ビジネスロジックを実現しています。
コアの管理ロジックはKernal.solファイルに集約されており、グローバルな Policy と Modules を管理しています。
Modules#
各モジュールには一意の KEYCODE と VERSION が含まれており、モジュールを定義する際にはこのメソッドを実装する必要があります。Kernal では、すべての keycode とモジュールのマッピングを管理しています。
Kernal には以下のメソッドが含まれています。
- _installModule:モジュールのインストール
- _upgradeModule:モジュールのアップグレード
例:
/// @inheritdoc Module
function KEYCODE() public pure override returns (Keycode) {
return toKeycode("MINTR");
}
/// @inheritdoc Module
function VERSION() external pure override returns (uint8 major, uint8 minor) {
major = 1;
minor = 0;
}
Policy#
Policy は、伝統的なソフトウェア開発のサービス層と考えることができ、一連のモジュールを組織するために使用されます。以下のメソッドが含まれます:
- configureDependencies:どのモジュールをサポートするかを返す
- requestPermissions:サポートされているモジュールの具体的な関数セレクタを返す
Kernal には以下のメソッドが含まれます:
- _activatePolicy
- _deactivatePolicy
例:
/// @inheritdoc Policy
function configureDependencies() external override returns (Keycode[] memory dependencies) {
dependencies = new Keycode[](3);
dependencies[0] = toKeycode("MINTR");
dependencies[1] = toKeycode("TRSRY");
dependencies[2] = toKeycode("ROLES");
MINTR = MINTRv1(getModuleAddress(dependencies[0]));
TRSRY = TRSRYv1(getModuleAddress(dependencies[1]));
ROLES = ROLESv1(getModuleAddress(dependencies[2]));
(uint8 TRSRY_MAJOR, ) = TRSRY.VERSION();
(uint8 MINTR_MAJOR, ) = MINTR.VERSION();
(uint8 ROLES_MAJOR, ) = ROLES.VERSION();
// Ensure Modules are using the expected major version.
// Modules should be sorted in alphabetical order.
bytes memory expected = abi.encode([1, 1, 1]);
if (MINTR_MAJOR != 1 || ROLES_MAJOR != 1 || TRSRY_MAJOR != 1)
revert Policy_WrongModuleVersion(expected);
}
/// @inheritdoc Policy
function requestPermissions() external view override returns (Permissions[] memory requests) {
Keycode MINTR_KEYCODE = MINTR.KEYCODE();
requests = new Permissions[](4);
requests[0] = Permissions(MINTR_KEYCODE, MINTR.mintOhm.selector);
requests[1] = Permissions(MINTR_KEYCODE, MINTR.burnOhm.selector);
requests[2] = Permissions(MINTR_KEYCODE, MINTR.increaseMintApproval.selector);
requests[3] = Permissions(MINTR_KEYCODE, MINTR.decreaseMintApproval.selector);
}