初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
ERCについてひたすらまとめたり、スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください!
https://twitter.com/cardene777
https://chaldene.net/
https://qiita.com/cardene
https://zenn.dev/heku
https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58
https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4
また、これからも情報を見逃したくないという方はぜひ以下の購読ボタンを教えてください。
更新があった時、登録しているメールアドレス宛に通知が飛ぶようになります。
今回はAptosのオブジェクトにより詳しくまとめていきます。
オブジェクト自体の簡単な解説は以下の記事にまとめています。
Aptosでは、Move言語というプログラミング言語を使用してコントラクトを作成します。
この時オブジェクトという仕組みは理解しておく必要があるため、この記事で学んでいってください。
以下のドキュメントをベースにまとめていきます。
https://aptos.dev/move/move-on-aptos/objects/
オブジェクトとは?
objects(オブジェクト)は、リソースの集合を1つのアドレスに関連付ける方法を提供するMove言語の機能です。
これにより、効率的なリソース管理とアクセスが可能になります。
objectsの主な特徴は以下の通りです。
リソースの集中管理とオーナーシップ管理を実現。
オブジェクトはリソースのコンテナとして機能し、1つのアドレスに属する。
オブジェクトを作成したコントラクトは、それらのリソースの変更や転送に関するカスタムの動作を定義可能。
オブジェクトは ObjectCore
構造体で表現されます。これはオブジェクトの所有者と転送権限を管理します。
リソースは ObjectGroup
を用いてオブジェクトに格納されます。
オブジェクトの作成と転送の例。
module my_addr::object_playground {
use std::signer;
use aptos_framework::object::{self, ObjectCore};
entry fun create_and_transfer(caller: &signer, destination: address) {
// オブジェクトを作成
let caller_address = signer::address_of(caller);
let constructor_ref = object::create_object(caller_address);
// オブジェクトのセットアップ...
// 宛先アドレスに転送
let object = object::object_from_constructor_ref<ObjectCore>(
&constructor_ref
);
object::transfer(caller, object, destination);
}
}
このコードでは以下のことを行っています。
create_object
関数を使ってオブジェクトを作成し、作成者のアドレスを渡す。constructor_ref
を使ってオブジェクトの初期設定を行う。object_from_constructor_ref
でオブジェクトの参照を取得。transfer
関数でオブジェクトを宛先アドレスに転送。
オブジェクトはリソースをグループ化して効率的に管理・転送できる強力な機能です。
これによりコントラクト内のリソース操作がシンプルになり、所有権管理も容易になります。
Move言語のオブジェクトモデルを活用することで、よりセキュアで効率的なスマートコントラクト開発が可能になります。
オブジェクトの作成
オブジェクトを作成する時、最初に 0x1::object::ObjectCore
というリソースが追加されます。
これにはオブジェクトのメタデータとオーナー情報が含まれています。
オブジェクトには、削除可能なオブジェクトと削除不可能なオブジェクトの2種類があります。
削除可能なオブジェクトの作成
通常、ユーザーからするとオブジェクトを削除できた方が良いです
オブジェクトが削除可能な場合、オブジェクトを削除することでストレージの払い戻しを受けることができ、ガス代を節約できます。
削除可能なオブジェクトは、トランザクションハッシュとカウンターに基づいてランダムな一意のアドレスを生成します。
オブジェクトのアドレスは常に一意であり、これはほとんどのオブジェクトを作成するための推奨される方法です。
module my_addr::object_playground {
use std::signer;
use aptos_framework::object;
entry fun create_my_object(caller: &signer) {
let caller_address = signer::address_of(caller);
let constructor_ref = object::create_object(caller_address);
// ...
}
}
このコードでは、以下の手順で削除可能なオブジェクトを作成しています。
signer
を使用して、呼び出し元のアドレスを取得。object::create_object
関数を呼び出し、呼び出し元のアドレスを渡してオブジェクトを作成。返された
constructor_ref
を使ってオブジェクトの初期設定を行う。
create_object
関数は、トランザクションハッシュとカウンターに基づいて一意のアドレスを生成し、そのアドレスに新しいオブジェクトを作成します。
これにより、オブジェクトのアドレスが重複することを防ぎ、一意性を保証します。
削除可能なオブジェクトを使用することで、不要になったオブジェクトを削除してストレージを解放し、ガス代を節約することができます。
これは、オブジェクトを大量に作成するアプリケーションにとって重要な機能です。
Move言語のオブジェクトシステムを活用することで、柔軟性が高く、効率的なリソース管理が可能なスマートコントラクトを開発できます。
削除できないオブジェクト
削除不可能なオブジェクトは、オブジェクトの存在を保証する必要がある特定の状況で役立ちます。
Aptosでは、削除不可能なオブジェクトを扱う方法が2つあります。
名前付きオブジェクト(Named objects)
スティッキーオブジェクト(Sticky objects)
名前付きオブジェクト
名前付きオブジェクトを作成すると、固定のシードを持つオブジェクトを作成できます。
これにより、後でオブジェクトを簡単に参照できます。
module my_addr::object_playground {
use std::signer;
use aptos_framework::object;
/// 名前付きオブジェクトのシード。作成アカウントにとってグローバルに一意である必要がある。
const NAME: vector<u8> = b"MyAwesomeObject";
entry fun create_my_object(caller: &signer) {
let caller_address = signer::address_of(caller);
let constructor_ref = object::create_named_object(caller_address, NAME);
// ...
}
#[view]
fun has_object(creator: address): bool {
let object_address = object::create_object_address(&creator, NAME);
object_exists<0x1::object::ObjectCore>(object_address)
}
}
このコードでは、以下の手順で名前付きオブジェクトを作成しています。
NAME
定数で、名前付きオブジェクトのシードを定義。
このシードは作成アカウントにとってグローバルに一意である必要がある。create_named_object
関数を呼び出し、呼び出し元のアドレスとシードを渡してオブジェクトを作成。has_object
ビュー関数で、特定のアドレスに名前付きオブジェクトが存在するかどうかを確認。
スティッキーオブジェクト
スティッキーオブジェクトは、削除可能なオブジェクトと同じように作成されますが、オブジェクトを削除できません。
これは、ファンジブルアセットのメタデータなど、削除されたくないユースケースに必要です。
module my_addr::object_playground {
use std::signer;
use aptos_framework::object;
entry fun create_my_object(caller: &signer) {
let caller_address = signer::address_of(caller);
let constructor_ref = object::create_sticky_object(caller_address);
// ...
}
}
このコードでは、create_sticky_object
関数を使用して、呼び出し元のアドレスに基づいてスティッキーオブジェクトを作成しています。
名前付きオブジェクトとスティッキーオブジェクトは、オブジェクトの存在を保証し、削除を防ぐ必要があるユースケースで役立ちます。
名前付きオブジェクトは固定のシードを使用するため、後でオブジェクトを簡単に参照できます。
スティッキーオブジェクトは削除できないため、重要なメタデータを保持するのに適しています。
これらの機能を活用することで、Moveスマートコントラクトでより堅牢で信頼性の高いオブジェクト管理が可能になります。
オブジェクトの設定
オブジェクトを作成した時点では、オブジェクトは存在しますが、その機能は未定義です。
オブジェクトの機能は作成時に設定する必要があります。
作成時に正しい機能が設定されていないと、後から変更することはできません。
リソースの追加
オブジェクトはリソースにデータを格納する必要があります。
オブジェクトのストレージにリソースを移動するには、オブジェクトの署名者が必要です。
ここでは、削除可能なオブジェクトの例を見ていきます。
オブジェクトを作成するとき、特別な ConstructorRef
を使用して、作成時にのみ利用可能なリソースを作成できます。
例えば、作成時に署名者を作成して、リソースをオブジェクトに移動できます。
module my_addr::object_playground {
use std::signer;
use aptos_framework::object;
#[resource_group_member(group = aptos_framework::object::ObjectGroup)]
struct MyStruct has key {
num: u8
}
entry fun create_my_object(caller: &signer) {
let caller_address = signer::address_of(caller);
// オブジェクトを作成
let constructor_ref = object::create_object(caller_address);
// オブジェクトの署名者を取得
let object_signer = object::generate_signer(&constructor_ref);
// MyStructリソースをオブジェクトに移動
move_to(&object_signer, MyStruct { num: 0 });
// ...
}
}
このコードでは、以下の手順でオブジェクトにリソースを追加しています。
MyStruct
という名前のリソースを定義。
このリソースはObjectGroup
に属し、key
の能力を持つ。create_my_object
エントリー関数内で、create_object
関数を呼び出してオブジェクトを作成。generate_signer
関数を使用して、オブジェクトの署名者を取得。move_to
関数を使用して、MyStruct
リソースをオブジェクトのストレージに移動。
ConstructorRef
は、オブジェクトの作成時にのみ利用可能な特別な参照です。
これを使用すると、作成時に一時的な署名者を生成し、リソースをオブジェクトに移動できます。
リソースは、オブジェクトのストレージに格納されるデータの基本単位です。resource_group_member
アトリビュートを使用して、リソースが属するグループを指定します。
ここでは、MyStruct
リソースが ObjectGroup
に属することを示しています。
オブジェクトの作成時にリソースを追加することで、オブジェクトにカスタムデータを格納し、機能を拡張できます。
これにより、オブジェクトをアプリケーションの要件に合わせて柔軟にカスタマイズできます。
オブジェクトの拡張
オブジェクトが作成された後、ユーザーが追加のデータを追加することを決定した場合、ExtendRef
を使用してオブジェクトの signer
を後から取得することができます。
ExtendRef
は、オブジェクトの署名者を生成するために使用できます。
誰がそれを取得できるかのアクセス権限は、コントラクトによって定義する必要があります。
以下のコードでは、ExtendRef
を使用してオブジェクトにメッセージを追加する方法を示しています。
module my_addr::object_playground {
use std::signer;
use std::string::{self, String};
use aptos_framework::object::{self, Object};
/// 呼び出し元がオブジェクトの所有者ではない
const E_NOT_OWNER: u64 = 1;
/// 呼び出し元がコントラクトのパブリッシャーではない
const E_NOT_PUBLISHER: u64 = 2;
#[resource_group_member(group = aptos_framework::object::ObjectGroup)]
struct MyStruct has key {
num: u8
}
#[resource_group_member(group = aptos_framework::object::ObjectGroup)]
struct Message has key {
message: string::String
}
#[resource_group_member(group = aptos_framework::object::ObjectGroup)]
struct ObjectController has key {
extend_ref: object::ExtendRef,
}
entry fun create_my_object(caller: &signer) {
let caller_address = signer::address_of(caller);
// オブジェクトを作成
let constructor_ref = object::create_object(caller_address);
// オブジェクトの署名者を取得
let object_signer = object::generate_signer(&constructor_ref);
// MyStructリソースをオブジェクトに移動
move_to(&object_signer, MyStruct { num: 0 });
// ExtendRefを作成し、オブジェクトに移動
let extend_ref = object::generate_extend_ref(&constructor_ref);
move_to(&object_signer, ObjectController { extend_ref });
// ...
}
entry fun add_message(
caller: &signer,
object: Object<MyStruct>,
message: String
) acquires ObjectController {
let caller_address = signer::address_of(caller);
// アクセス権限の設定方法はいくつかある
// オブジェクトの所有者のみに許可
assert!(object::is_owner(object, caller_address), E_NOT_OWNER);
// コントラクトのパブリッシャーのみに許可
assert!(caller_address == @my_addr, E_NOT_PUBLISHER);
// その他、考えられるあらゆるアクセス許可スキームが可能
// ExtendRefを使用して署名者を取得
let object_address = object::object_address(object);
let extend_ref = borrow_global<ObjectController>(object_address).extend_ref;
let object_signer = object::generate_signer_for_extending(&extend_ref);
// オブジェクトにメッセージを追加
move_to(object_signer, Message { message });
}
}
このコードでは、以下の手順でオブジェクトにメッセージを追加しています。
Message
リソースを定義。
このリソースは文字列のメッセージを保持する。ObjectController
リソースを定義。
このリソースはExtendRef
を保持する。create_my_object
関数内で、generate_extend_ref
関数を使用してExtendRef
を作成し、ObjectController
リソースとしてオブジェクトに移動。add_message
エントリー関数を定義。
この関数は、呼び出し元の署名者、オブジェクト、およびメッセージを受け取る。アクセス権限を確認。
ここでは、オブジェクトの所有者またはコントラクトのパブリッシャーのみがメッセージを追加できるようにしている。ObjectController
リソースからExtendRef
を取得し、generate_signer_for_extending
関数を使用してオブジェクトの署名者を生成。move_to
関数を使用して、Message
リソースをオブジェクトに移動。
ExtendRef
を使用すると、オブジェクトの作成後に追加のデータを追加できます。
これにより、オブジェクトを動的に拡張し、新しい機能を追加できます。
アクセス権限の設定により、オブジェクトの拡張を制御し、セキュリティを確保できます。
このように、ExtendRef
を活用することで、オブジェクトをより柔軟かつ拡張性の高いものにできます。
これは、複雑なアプリケーションを構築する時に非常に重要な機能です。
オブジェクトは転送無効化と再有効化
オブジェクトは、転送可能にすることも、転送不可能にすることもできます。
デフォルトでは、すべてのオブジェクトは転送可能です。
ただし、この機能はオンとオフを切り替えることができ、作成時に選択することもできます。
この機能は TransferRef
によって有効になります。
以下のコードでは、TransferRef
を使用してオブジェクトの転送を制御する方法を示しています。
module my_addr::object_playground {
use std::signer;
use std::string::{self, String};
use aptos_framework::object::{self, Object};
/// 呼び出し元がコントラクトのパブリッシャーではない
const E_NOT_PUBLISHER: u64 = 1;
#[resource_group_member(group = aptos_framework::object::ObjectGroup)]
struct ObjectController has key {
transfer_ref: object::TransferRef,
}
entry fun create_my_object(
caller: &signer,
transferrable: bool,
controllable: bool
) {
let caller_address = signer::address_of(caller);
// オブジェクトを作成
let constructor_ref = object::create_object(caller_address);
// オブジェクトの署名者を取得
let object_signer = object::generate_signer(&constructor_ref);
// 転送を制御するためのTransferRefを作成
let transfer_ref = object::generate_transfer_ref(&constructor_ref);
// ここで、オブジェクトを転送可能にするか選択できる
// また、後で変更できるようにするかどうかも決定できる
// デフォルトでは転送可能
if (!transferrable) {
object::disable_ungated_transfer(&transfer_ref);
};
// 制御可能にしたい場合は、TransferRefを保存しておく必要がある
if (controllable) {
move_to(&object_signer, ObjectController { transfer_ref });
}
// ...
}
/// この例では、コントラクトのパブリッシャーのみが転送の権限を変更できるようにする
entry fun toggle_transfer(
caller: &signer,
object: Object<ObjectController>
) acquires ObjectController {
// パブリッシャーのみが転送を切り替えられるようにする
let caller_address = signer::address_of(caller);
assert!(caller_address == @my_addr, E_NOT_PUBLISHER);
// TransferRefを取得
let object_address = object::object_address(object);
let transfer_ref = borrow_global<ObjectController>(
object_address
).transfer_ref;
// 現在の状態に基づいて切り替える
if (object::ungated_transfer_allowed(object)) {
object::disable_ungated_transfer(&transfer_ref);
} else {
object::enable_ungated_transfer(&transfer_ref);
}
}
}
このコードでは、以下の手順でオブジェクトの転送を制御しています。
ObjectController
リソースを定義。
このリソースはTransferRef
を保持する。create_my_object
エントリー関数を定義。
この関数は、呼び出し元の署名者、転送可能かどうか、制御可能かどうかを受け取る。generate_transfer_ref
関数を使用してTransferRef
を作成。transferrable
がfalse
の場合、disable_ungated_transfer
関数を呼び出して転送を無効化。controllable
がtrue
の場合、ObjectController
リソースとしてTransferRef
をオブジェクトに移動。toggle_transfer
エントリー関数を定義。この関数は、呼び出し元の署名者とオブジェクトを受け取る。コントラクトのパブリッシャーのみが転送を切り替えられるようにアクセス権限を確認。
ObjectController
リソースからTransferRef
を取得。ungated_transfer_allowed
関数を使用して現在の転送状態を確認し、それに基づいてenable_ungated_transfer
またはdisable_ungated_transfer
関数を呼び出して転送を切り替える。
TransferRef
を使用すると、オブジェクトの転送を動的に制御できます。
これにより、オブジェクトの所有権を柔軟に管理できます。
また、アクセス権限を設定することで、転送の切り替えを制限し、セキュリティを確保できます。
このように、TransferRef
を活用することで、オブジェクトの転送を細かく制御し、アプリケーションの要件に合わせてカスタマイズできます。
これは、複雑な所有権管理が必要なアプリケーションにとって重要な機能です。
制御された転送
クリエーターがすべての転送を制御したい場合、TransferRef
から LinearTransferRef
を作成して、1回限りの転送機能を提供することができます。LinearTransferRef
は、オブジェクトの所有者が使用する必要があります。
以下のコードでは、LinearTransferRef
を使用して制御された転送を実現する方法を示しています。
module my_addr::object_playground {
use std::signer;
use std::option;
use std::string::{self, String};
use aptos_framework::object::{self, Object};
/// 呼び出し元がコントラクトのパブリッシャーではない
const E_NOT_PUBLISHER: u64 = 1;
#[resource_group_member(group = aptos_framework::object::ObjectGroup)]
struct ObjectController has key {
transfer_ref: object::TransferRef,
linear_transfer_ref: option::Option<object::LinearTransferRef>,
}
entry fun create_my_object(
caller: &signer,
transferrable: bool,
controllable: bool
) {
let caller_address = signer::address_of(caller);
// オブジェクトを作成
let constructor_ref = object::create_object(caller_address);
// オブジェクトの署名者を取得
let object_signer = object::generate_signer(&constructor_ref);
// 転送を制御するためのTransferRefを作成
let transfer_ref = object::generate_transfer_ref(&constructor_ref);
// ungated transferを無効化
object::disable_ungated_transfer(&transfer_ref);
move_to(&object_signer, ObjectController {
transfer_ref,
linear_transfer_ref: option::none(),
});
// ...
}
/// この例では、コントラクトのパブリッシャーのみが転送の権限を変更できるようにする
entry fun allow_single_transfer(
caller: &signer,
object: Object<ObjectController>
) acquires ObjectController {
// パブリッシャーのみが転送を切り替えられるようにする
let caller_address = signer::address_of(caller);
assert!(caller_address == @my_addr, E_NOT_PUBLISHER);
let object_address = object::object_address(object);
// TransferRefを取得
let transfer_ref = borrow_global<ObjectController>(
object_address
).transfer_ref;
// 1回限りの使用が可能な`LinearTransferRef`を生成
let linear_transfer_ref = object::generate_linear_transfer_ref(
&transfer_ref
);
// 後で使用するために保存
let object_controller = borrow_global_mut<ObjectController>(
object_address
);
option::fill(
&mut object_controller.linear_transfer_ref,
linear_transfer_ref
)
}
/// 所有者のみが正確に1回転送できる
entry fun transfer(
caller: &signer,
object: Object<ObjectController>,
new_owner: address
) acquires ObjectController {
let object_address = object::object_address(object);
// linear_transfer_refを取得。消費されるため、リソースから抽出する必要がある
let object_controller = borrow_global_mut<ObjectController>(
object_address
);
let linear_transfer_ref = option::extract(
&mut object_controller.linear_transfer_ref
);
object::transfer_with_ref(linear_transfer_ref, new_owner);
}
}
このコードでは、以下の手順で制御された転送を実現しています。
ObjectController
リソースを定義。
このリソースはTransferRef
とOption<LinearTransferRef>
を保持する。create_my_object
エントリー関数内で、disable_ungated_transfer
関数を呼び出して ungated transfer を無効化し、ObjectController
リソースをオブジェクトに移動。allow_single_transfer
エントリー関数を定義。
この関数は、呼び出し元の署名者とオブジェクトを受け取る。コントラクトのパブリッシャーのみが転送を許可できるようにアクセス権限を確認。
ObjectController
リソースからTransferRef
を取得。generate_linear_transfer_ref
関数を使用して、1回限りの使用が可能なLinearTransferRef
を生成。ObjectController
リソースのlinear_transfer_ref
フィールドにLinearTransferRef
を保存。transfer
エントリー関数を定義。
この関数は、呼び出し元の署名者、オブジェクト、新しい所有者のアドレスを受け取る。ObjectController
リソースからLinearTransferRef
を抽出。transfer_with_ref
関数を使用して、LinearTransferRef
を用いてオブジェクトを新しい所有者に転送。
LinearTransferRef
を使用すると、オブジェクトの転送を1回限りに制限できます。
これにより、クリエーターがすべての転送を厳密に制御できます。また、LinearTransferRef
はオブジェクトの所有者のみが使用できるため、不正な転送を防ぐことができます。
このように、LinearTransferRef
を活用することで、オブジェクトの転送を細かく制御し、セキュリティを強化できます。
これは、高度な所有権管理が必要なアプリケーションにとって重要な機能です。
オブジェクトの削除を許可
オブジェクトの削除は、不要なデータを削除することでストレージを解放し、ストレージの払い戻しを受け取るのに役立ちます。
削除は DeleteRef
を使用して行うことができますが、これはオブジェクトの作成時に作成する必要があります。
削除不可能なオブジェクトに対して DeleteRef
を作成することはできないことに注意してください。
以下のコードでは、DeleteRef
を使用してオブジェクトを削除する方法を示しています。
module my_addr::object_playground {
use std::signer;
use std::option;
use std::string::{self, String};
use aptos_framework::object::{self, Object};
/// 呼び出し元がオブジェクトの所有者ではない
const E_NOT_OWNER: u64 = 1;
#[resource_group_member(group = aptos_framework::object::ObjectGroup)]
struct ObjectController has key {
delete_ref: object::DeleteRef,
}
entry fun create_my_object(
caller: &signer,
transferrable: bool,
controllable: bool
) {
let caller_address = signer::address_of(caller);
// オブジェクトを作成
let constructor_ref = object::create_object(caller_address);
// オブジェクトの署名者を取得
let object_signer = object::generate_signer(&constructor_ref);
// DeleteRefを作成して保存
let delete_ref = object::generate_delete_ref(&constructor_ref);
move_to(&object_signer, ObjectController {
delete_ref
});
// ...
}
/// 所有者のみがオブジェクトを削除できるようにする
entry fun delete(
caller: &signer,
object: Object<ObjectController>,
) {
// 呼び出し元のみが削除できるようにする
let caller_address = signer::address_of(caller);
assert!(object::is_owner(&object, caller_address), E_NOT_OWNER);
let object_address = object::object_address(object);
// delete_refを取得。消費されるため、リソースから抽出する必要がある
let ObjectController {
delete_ref
} = move_from<ObjectController>(
object_address
);
// オブジェクトを永久に削除!
object::delete(delete_ref);
}
}
このコードでは、以下の手順でオブジェクトの削除を実現しています。
ObjectController
リソースを定義。
このリソースはDeleteRef
を保持する。create_my_object
エントリー関数内で、generate_delete_ref
関数を使用してDeleteRef
を作成し、ObjectController
リソースとしてオブジェクトに移動。delete
エントリー関数を定義。
この関数は、呼び出し元の署名者とオブジェクトを受け取る。呼び出し元がオブジェクトの所有者であることを確認。
ObjectController
リソースからDeleteRef
を抽出。delete
関数を使用して、DeleteRef
を用いてオブジェクトを削除。
DeleteRef
を使用すると、オブジェクトを完全に削除できます。
これにより、不要になったオブジェクトを取り除き、ストレージを解放できます。
ただし、DeleteRef
はオブジェクトの作成時に作成する必要があり、削除不可能なオブジェクトに対しては作成できません。
また、DeleteRef
を使用してオブジェクトを削除できるのは、オブジェクトの所有者のみです。
これにより、不正な削除を防ぐことができます。
このように、DeleteRef
を活用することで、オブジェクトのライフサイクルを適切に管理し、リソースの効率的な利用を実現できます。
これは、大規模なアプリケーションを構築する際に重要な機能です。
不変性
オブジェクトの不変性(Immutability)は、関連するコントラクトを不変にし、オブジェクトを拡張または変更する機能を削除することで実現できます。
不変性を持つオブジェクトは、一度作成されると、その状態を変更することができません。
デフォルトでは、コントラクトは不変ではありません。
つまり、以下のようなことが可能です。
ExtendRef
を使ってオブジェクトを拡張できる。コントラクトが許可していれば、リソースを変更できる。
ただし、コントラクトを不変にすることで、これらの操作を制限し、オブジェクトの不変性を保証することができます。
不変性は、以下のようなメリットがあります。
セキュリティの向上
不変のオブジェクトは、不正な変更から保護されます。
これにより、セキュリティが強化されます。
予測可能性の向上
オブジェクトの状態が変化しないため、動作が予測しやすくなります。
これにより、プログラムの理解と保守が容易になります。
並行性の向上
不変のオブジェクトは、複数のスレッドや処理から同時にアクセスされても安全です。
これにより、並行処理が容易になります。
ただし、不変性には以下のようなデメリットもあります。
柔軟性の低下
一度作成されたオブジェクトを変更できないため、要件の変化に対応しにくくなる可能性があります。
メモリ使用量の増加
オブジェクトを更新する代わりに新しいオブジェクトを作成する必要があるため、メモリ使用量が増加する可能性があります。
不変性は、セキュリティと予測可能性が重要な場面で有効です。
一方で、柔軟性が必要な場面では、可変なオブジェクトが適しています。
アプリケーションの要件に応じて、適切な不変性レベルを選択することが重要です。
Moveプログラミング言語では、コントラクトの不変性を設定することで、オブジェクトの不変性を制御できます。
これにより、セキュアで予測可能なスマートコントラクトを開発できます。
オブジェクトの使用
エントリ関数の引数にオブジェクトを使用
0x1::object::Object<T>
は、エントリー関数の入力引数として使用できます。
これは、オブジェクトの address
としてエンコードされ、T
の値に対して型チェックされます。
より汎用的な関数では、入力の型付けに汎用の T
を提供することで処理できます。
module my_addr::object_playground {
use aptos_framework::object::Object;
/// オブジェクトにジェネリック `T` が格納されていない場合、これは失敗する
entry fun do_something<T>(object: Object<T>) {
// ...
}
}
さらに、オブジェクトに存在する個々のリソースを使用することもできます。
関数が実行される前に、リソースの存在がチェックされます。
module my_addr::object_playground {
use aptos_framework::object::Object;
struct MyAwesomeStruct has key {}
/// オブジェクトにMyAwesomeStructが格納されていない場合、これは失敗する
entry fun do_something(object: Object<MyAwesomeStruct>) {
// ...
}
}
リソース 0x1::object::ObjectCore
は、すべてのオブジェクトで利用可能です。
module my_addr::object_playground {
use aptos_framework::object::{Object, ObjectCore};
/// アドレスがオブジェクトでない場合にのみ失敗する
entry fun do_something(object: Object<ObjectCore>) {
// ...
}
}
オブジェクトの型
オブジェクトは、そのアドレスに複数のリソースを格納できます。
そして、それらの型のいずれかでオブジェクトを参照できます。
リソースが利用可能である限り、アドレスをオブジェクトに変換したり、オブジェクトを型間で変換したりできます。
module my_addr::object_playground {
use aptos_framework::object::{self, Object, ObjectCore};
struct MyAwesomeStruct has key {}
fun convert_type(object: Object<ObjectCore>): Object<MyAwesomeStruct> {
object::convert<MyAwesomeStruct>(object)
}
fun address_to_type(object_address: address): Object<MyAwesomeStruct> {
object::address_to_object<MyAwesomeStruct>(object)
}
}
オブジェクトの所有権
オブジェクトは、任意のアドレスによって所有できます。
これには、オブジェクト、アカウント、リソースアカウントが含まれます。
これにより、オブジェクト間の合成性や、オブジェクト間の複雑な関係性が可能になります。
所有権を確認
所有権は、いくつかの方法で確認できます。
module my_addr::object_playground {
use std::signer;
use aptos_framework::object::{self, Object};
// 権限がない!
const E_NOT_AUTHORIZED: u64 = 1;
fun check_owner_is_caller<T>(caller: &signer, object: Object<T>) {
assert!(
object::is_owner(object, signer::address_of(caller)),
E_NOT_AUTHORIZED
);
}
fun check_is_owner_of_object<T>(addr: address, object: Object<T>) {
assert!(object::owner(object) == addr, E_NOT_AUTHORIZED);
}
fun check_is_nested_owner_of_object<T, U>(
caller: &signer,
outside_object: Object<T>,
inside_object: Object<U>
) {
// 期待される所有権
// 呼び出し元アカウント -> 外側のオブジェクト -> 内側のオブジェクト
// 外側のオブジェクトが内側のオブジェクトを所有しているかチェック
let outside_address = object::object_address(outside_object);
assert!(object::owns(inside_object, outside_address), E_NOT_AUTHORIZED);
// 呼び出し元が外側のオブジェクトを所有しているかチェック
let caller_address = signer::address_of(caller);
assert!(object::owns(outside_object, caller_address), E_NOT_AUTHORIZED);
// 呼び出し元が内側のオブジェクトを所有しているかチェック(外側のオブジェクトを介して)
// これにより、最初の2つの呼び出しをスキップできる(さらにネストされている場合も同様)
assert!(object::owns(inside_object, caller_address), E_NOT_AUTHORIZED);
}
}
このコードでは、オブジェクトの所有権を確認するためのいくつかの関数を定義しています。
check_owner_is_caller<T>(caller: &signer, object: Object<T>)
この関数は、呼び出し元が指定されたオブジェクトの所有者であるかどうかを確認します。
object::is_owner(object, signer::address_of(caller))
上記を使用して、呼び出し元のアドレスがオブジェクトの所有者であるかどうかをチェックします。
check_is_owner_of_object<T>(addr: address, object: Object<T>)
この関数は、指定されたアドレスが指定されたオブジェクトの所有者であるかどうかを確認します。
object::owner(object) == addr
上記を使用して、オブジェクトの所有者アドレスが指定されたアドレスと一致するかどうかをチェックします。
check_is_nested_owner_of_object<T, U>(caller: &signer, outside_object: Object<T>, inside_object: Object<U>)
この関数は、ネストされたオブジェクトの所有権を確認します。
期待される所有権の階層は、呼び出し元アカウント -> 外側のオブジェクト -> 内側のオブジェクトです。
object::owns(inside_object, outside_address)
上記を使用して、外側のオブジェクトが内側のオブジェクトを所有しているかどうかをチェックします。
object::owns(outside_object, caller_address)
上記を使用して、呼び出し元が外側のオブジェクトを所有しているかどうかをチェックします。
object::owns(inside_object, caller_address)
上記を使用して、呼び出し元が内側のオブジェクトを所有しているかどうかをチェックします。
これにより、最初の2つのチェックをスキップできます。
これらの関数は、オブジェクトの所有権を確認するためのユーティリティとして使用できます。
所有権の確認は、オブジェクトへのアクセス制御や権限管理において重要な役割を果たします。
所有権の階層構造を利用することで、ネストされたオブジェクトの所有権を効率的に確認できます。
これにより、複雑なオブジェクト関係を持つスマートコントラクトアプリケーションを構築する時の柔軟性が向上します。
所有権の譲渡
オブジェクトは、ungated transferが無効になっていない限り、譲渡することができます。
ungated transferが無効になっている場合、譲渡は TransferRef
または LinearTransferRef
を使用してのみ行うことができます。
ungated transferが有効になっている場合、オブジェクトは以下のように譲渡できます。
module my_addr::object_playground {
use aptos_framework::object::{self, Object};
/// 別のアドレスに譲渡する(オブジェクトまたはアカウントにできる)
fun transfer<T>(owner: &signer, object: Object<T>, destination: address) {
object::transfer(owner, object, destination);
}
/// 別のオブジェクトに譲渡する
fun transfer_to_object<T, U>(
owner: &signer,
object: Object<T>,
destination: Object<U>
) {
object::transfer_to_object(owner, object, destination);
}
}
このコードでは、オブジェクトの所有権を譲渡するための2つの関数を定義しています。
transfer<T>(owner: &signer, object: Object<T>, destination: address)
この関数は、オブジェクトを別のアドレスに譲渡します。
object::transfer(owner, object, destination)
上記を使用して、オブジェクトの所有権を宛先のアドレスに譲渡します。
宛先のアドレスは、オブジェクトまたはアカウントのアドレスにできます。
transfer_to_object<T, U>(owner: &signer, object: Object<T>, destination: Object<U>)
この関数は、オブジェクトを別のオブジェクトに譲渡します。
object::transfer_to_object(owner, object, destination)
上記を使用して、オブジェクトの所有権を宛先のオブジェクトに譲渡します。
宛先のオブジェクトは、任意の型 `U` のオブジェクトにできます。
これらの関数を使用するには、以下の条件が満たされている必要があります。
ungated transferが有効になっている。
呼び出し元が譲渡するオブジェクトの所有者である。
ungated transfer
が無効になっている場合、TransferRef
または LinearTransferRef
を使用して譲渡を行う必要があります。
これらの参照を使用することで、譲渡の制御とセキュリティが強化されます。
オブジェクトの所有権を譲渡する機能は、スマートコントラクトアプリケーションにおいて重要な役割を果たします。
所有権の譲渡により、オブジェクトの制御を別のエンティティに移すことができます。
これは、アセットの売買やユーザー間のアイテム譲渡など、様々な場面で活用できます。
ただし、譲渡の制御と安全性を確保するために、適切な権限管理とアクセス制御が必要です。
ungated transferの無効化や、TransferRef
と LinearTransferRef
の使用により、譲渡の制御を強化できます。
オブジェクトの削除とバーン
オブジェクトを削除する方法には、次の2つがあります。
削除(Delete)
バーン(Burn)
削除
オブジェクトを削除するには、ユーザーは DeleteRef
を持っている必要があります。
バーン
オブジェクトが削除不可能な場合、不要なオブジェクトをトゥームストーン化してバーンすることができます。
module my_addr::object_playground {
use aptos_framework::object::{self, Object};
fun burn_object<T>(owner: &signer, object: Object<T>) {
object::burn(owner, object);
assert!(object::is_burnt(object) == true, 1);
}
}
このコードでは、burn_object
関数を定義しています。
object::burn(owner, object)
を使用して、オブジェクトをバーンします。object::is_burnt(object) == true
をアサートして、オブジェクトが正常にバーンされたことを確認します。
バーンしたオブジェクトは、トゥームストーン化され、使用できなくなります。
トゥームストーン化とは、オブジェクトやデータを完全に削除せずに、それらを無効な状態にマークするプロセスのことです。トゥームストーンとは、文字通り「墓石」を意味します。つまり、トゥームストーン化されたオブジェクトやデータは、まだ存在しているものの、「死○だ」状態にあるということを表しています。
トゥームストーン化の主な目的は以下の通りです。
完全な削除の回避
オブジェクトやデータを完全に削除すると、それらに関連する情報が失われてしまいます。
トゥームストーン化することで、データの完全な消失を防ぎ、必要に応じて復元できるようにします。
整合性の維持
分散システムにおいて、データの削除を伝播するには時間がかかります。
トゥームストーン化により、削除処理が完了するまでの間、データの整合性を維持できます。
監査証跡の保持
トゥームストーン化されたオブジェクトやデータは、監査証跡として使用できます。
これにより、システムの変更履歴を追跡し、問題の調査やデバッグに役立てることができます。
トゥームストーン化の実装方法は、システムやアプリケーションによって異なります。一般的なアプローチとしては、以下のようなものがあります。
フラグの設定
オブジェクトやデータに「削除済み」または「無効」のフラグを設定し、それらが使用不可能であることを示します。
特別な値の割り当て
オブジェクトやデータに特別な値(例えば、nullやtombstone)を割り当てることで、それらが無効であることを示します。
別のストレージへの移動
トゥームストーン化されたオブジェクトやデータを別のストレージ領域に移動し、アクティブなデータと分離します。
Moveプログラミング言語では、
burn
関数を使用してオブジェクトをバーン(トゥームストーン化)することができます。バーンされたオブジェクトは、is_burnt
関数で確認できます。また、unburn
関数を使用して、バーンされたオブジェクトを元の状態に戻すこともできます。トゥームストーン化は、データの完全な削除を避けつつ、不要になったオブジェクトやデータを管理するための有用な手法です。これにより、システムの整合性を維持しながら、リソースの効率的な利用が可能になります。
ユーザーがオブジェクトをバーンしないことに決めた場合、アンバーンすることができます。
module my_addr::object_playground {
use aptos_framework::object::{self, Object};
fun unburn_object<T>(owner: &signer, object: object::Object<T>) {
object::unburn(owner, object);
assert!(object::is_burnt(object) == false, 1);
}
}
このコードでは、unburn_object
関数を定義しています。
object::unburn(owner, object)
を使用して、オブジェクトをアンバーンします。object::is_burnt(object) == false
をアサートして、オブジェクトが正常にアンバーンされたことを確認します。
アンバーンされたオブジェクトは、再び使用可能になります。
オブジェクトの削除とバーンは、不要になったオブジェクトを処理するための重要な機能です。
削除は、オブジェクトを完全に削除し、リソースを解放します。
一方、バーンは、オブジェクトを使用不可能にしますが、完全には削除しません。
削除を行うには、DeleteRef
が必要です。これにより、オブジェクトの削除を制御し、不正な削除を防ぐことができます。
バーンは、削除不可能なオブジェクトに対して使用できます。
バーンされたオブジェクトは、トゥームストーン化され、使用できなくなります。
ただし、必要に応じてアンバーンすることで、オブジェクトを再び使用可能にすることができます。
これらの機能を適切に活用することで、オブジェクトのライフサイクル管理を柔軟に行うことができます。
不要になったオブジェクトを適切に処理することで、リソースの効率的な利用とストレージの最適化が可能になります。
最後に
今回はAptosの「オブジェクト」について詳しくまとめました。
これからも他のブロックチェーンやサービスについてもまとめていきます。
情報を見逃したくないという方はぜひ以下の購読ボタンを教えてください。
更新があった時、登録しているメールアドレス宛に通知が飛ぶようになります。
また、拡散もしてくれると嬉しいです🙇♂️