write-contracts について
write-contractsは、Aptos Moveのスマートコントラクトを作成する開発者向けのスキルです。デジタルアセット、オブジェクト、セキュリティ、ストレージ選択、テスト可能なアクセサーのパターンを扱います。
主な機能
- 01Aptos Moveのコントラクトパターン
- 02Digital Asset NFTの指針
- 03オブジェクトのライフサイクルと所有権ルール
- 04署名者と入力の検証
- 05ストレージ選択の指針
- 06テスト可能なviewアクセサー
活用シーン
- 01NFTコレクションやマーケットプレイスの作成
- 02オブジェクトベースのコントラクトモジュールの実装
- 03レジストリやログのストレージ選択
- 04アクセス制御と入力検証の追加
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ aptos-labs/ aptos-agent-skills --skill write-contractsインストール後、Claude Code で/write-contractsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/access-control.mdcomplete-example.mdevents.mdinitialization.mdobject-patterns.mdsafe-arithmetic.mdstorage-decision-tree.mdstorage-gas-optimization.mdstorage-patterns.mdstorage-types.mdv2-syntax.md詳細
コントラクト作成スキル
コアルール
デジタルアセット(NFT)⭐ 重大
- NFT 関連のすべてのコントラクト(コレクション、マーケットプレイス、ミント)では、必ずデジタルアセット(DA)標準を使用してください
- 必ず
aptos_token_objects::collectionモジュールとaptos_token_objects::tokenモジュールをインポートしてください - NFT の参照には必ず
Object<AptosToken>を使用してください(汎用のObject<T>は使用しないでください) - 非推奨の TokenV1 標準や
aptos_token::tokenモジュールは絶対に使用しないでください - NFT のパターン全体については、
../../../patterns/move/DIGITAL_ASSETS.mdを参照してください
オブジェクトモデル
- すべてのオブジェクト参照には必ず
Object<T>を使用してください(生のアドレスは絶対に使用しないでください) ConstructorRefが破棄される前に、コンストラクター内ですべての参照(TransferRef、DeleteRef)を生成してください- コンストラクターからは
Object<T>を返してください(ConstructorRefは絶対に返さないでください) object::owner(obj) == signer::address_of(user)を使って所有権を確認してください- オブジェクトの署名者には
object::generate_signer(&constructor_ref)を使用してください - シングルトンには名前付きオブジェクトを使用してください:
object::create_named_object(creator, seed)
セキュリティ
- エントリー関数では、署名者の権限を必ず検証してください:
assert!(signer::address_of(user) == expected, E_UNAUTHORIZED) - 入力は必ず検証してください:ゼロでない数量、アドレスの検証、文字列の長さの確認
- 公開関数で
&mut参照を公開することは絶対に避けてください - エントリー関数で署名者の検証を省略することは絶対に避けてください
最新の構文
- イテレーションにはインライン関数とラムダを使用してください
- レシーバースタイルのメソッド呼び出しを使用してください:
obj.is_owner(user)(最初のパラメーターをselfとして定義) vector::borrow()の代わりにベクターのインデックス参照を使用してください:vector[index]- 名前付きアドレスを直接使用してください:
@marketplace_addr(ヘルパー関数は使用しないでください)
必須パターン
- デプロイ時のコントラクト初期化には
init_moduleを使用してください - すべての重要なアクティビティ(作成、転送、更新、削除)で必ずイベントを発行してください
- 明確で説明的な名前のエラー定数を定義してください(E_NOT_OWNER、E_INSUFFICIENT_BALANCE)
テスト容易性
- 構造体のフィールドにアクセスするアクセサー関数を追加してください。別モジュールのテストからは、構造体のフィールドに直接アクセスできません
- 読み取り専用のアクセサー関数には
#[view]アノテーションを使用してください - 複数フィールドにアクセスするアクセサー関数では、タプルを返してください:
(seller, price, timestamp) #[view]はドキュメントコメントより前に配置してください。#[view]の前に/// commentを記述すると、コンパイラー警告が発生します。まず#[view]を記述し、その後に///を記述してください
クイックワークフロー
- モジュール構造を作成 → 構造体、イベント、定数、init_module を定義する
- オブジェクト作成を実装 → すべての参照をあらかじめ生成する適切なコンストラクターパターンを使用する
- アクセス制御を追加 → 所有権を確認し、すべての入力を検証する
- セキュリティを確認 → デプロイ前に
security-auditスキルを使用する
主な例:オブジェクト作成パターン
struct MyObject has key {
name: String,
transfer_ref: object::TransferRef,
delete_ref: object::DeleteRef,
}
// Error constants
const E_NOT_OWNER: u64 = 1;
const E_EMPTY_STRING: u64 = 2;
const E_NAME_TOO_LONG: u64 = 3;
// Configuration constants
const MAX_NAME_LENGTH: u64 = 100;
/// Create object with proper pattern
public fun create_my_object(creator: &signer, name: String): Object<MyObject> {
// 1. Create object
let constructor_ref = object::create_object(signer::address_of(creator));
// 2. Generate ALL refs you'll need BEFORE constructor_ref is destroyed
let transfer_ref = object::generate_transfer_ref(&constructor_ref);
let delete_ref = object::generate_delete_ref(&constructor_ref);
// 3. Get object signer
let object_signer = object::generate_signer(&constructor_ref);
// 4. Store data in object
move_to(&object_signer, MyObject {
name,
transfer_ref,
delete_ref,
});
// 5. Return typed object reference (ConstructorRef automatically destroyed)
object::object_from_constructor_ref<MyObject>(&constructor_ref)
}
/// Update with ownership verification
public entry fun update_object(
owner: &signer,
obj: Object<MyObject>,
new_name: String
) acquires MyObject {
// ✅ ALWAYS: Verify ownership
assert!(object::owner(obj) == signer::address_of(owner), E_NOT_OWNER);
// ✅ ALWAYS: Validate inputs
assert!(string::length(&new_name) > 0, E_EMPTY_STRING);
assert!(string::length(&new_name) <= MAX_NAME_LENGTH, E_NAME_TOO_LONG);
// Safe to proceed
let obj_data = borrow_global_mut<MyObject>(object::object_address(&obj));
obj_data.name = new_name;
}
主な例:テスト用アクセサー関数
struct ListingInfo has store, drop, copy {
seller: address,
price: u64,
listed_at: u64,
}
/// Accessor function - tests cannot access struct fields directly
/// Use tuple returns for multiple fields
#[view]
public fun get_listing_details(nft_addr: address): (address, u64, u64) acquires Listings {
let listings = borrow_global<Listings>(get_marketplace_address());
assert!(table::contains(&listings.items, nft_addr), E_NOT_LISTED);
let listing = table::borrow(&listings.items, nft_addr);
(listing.seller, listing.price, listing.listed_at)
}
/// Single-field accessor when only one value needed
#[view]
public fun get_staked_amount(user_addr: address): u64 acquires Stakes {
let stakes = borrow_global<Stakes>(get_vault_address());
if (table_with_length::contains(&stakes.items, user_addr)) {
table_with_length::borrow(&stakes.items, user_addr).amount
} else {
0
}
}
モジュール構造テンプレート
module my_addr::my_module {
// ============ Imports ============
use std::signer;
use std::string::String;
use aptos_framework::object::{Self, Object};
use aptos_framework::event;
// ============ Events ============
#[event]
struct ItemCreated has drop, store {
item: address,
creator: address,
}
// ============ Structs ============
// Define your data structures
// ============ Constants ============
const E_NOT_OWNER: u64 = 1;
const E_UNAUTHORIZED: u64 = 2;
// ============ Init Module ============
fun init_module(deployer: &signer) {
// Initialize global state, registries, etc.
}
// ============ Public Entry Functions ============
// User-facing functions
// ============ Public Functions ============
// Composable functions
// ============ Private Functions ============
// Internal helpers
}
ストレージタイプの選択
⚠️ ユーザーがストレージについて言及した場合(「store」、「track」、「registry」、「mapping」、「list」、「collection」):
1. 2~3 個の質問をする(references/storage-decision-tree.md を参照)
- アクセスパターンは?(逐次、キーと値、または両方)
- 想定サイズは?(小、大、または不明)
.length()が必要か?(条件による)
2. パターンから推奨する(references/storage-patterns.md)
| パターン | 推奨ストレージ |
|---|---|
| ユーザーレジストリ | Table<address, UserInfo> |
| ステーキング記録 | Table<address, StakeInfo> |
| リーダーボード | BigOrderedMap<u64, address> |
| トランザクションログ | SmartVector<TxRecord> または Vector |
| ホワイトリスト(<100) | Vector<address> |
| 投票記録 | TableWithLength<address, bool> |
| 設定(<50) | OrderedMap<String, Value> |
| DAO 提案 | BigOrderedMap<u64, Proposal> |
| アセットコレクション | Vector<Object<T>> または SmartVector |
3. ガスに関する簡単なコンテキストを含める
推奨例:
- 「ステーキングには
Table<address, StakeInfo>をおすすめします。ユーザー数に上限がなく、並行操作が発生するためです(スロットを分けると並列アクセスが可能になります)」 - 「リーダーボードには
BigOrderedMap<u64, address>をおすすめします。ソートされた順序での反復処理が必要なためです(O(log n)。本番環境ではallocate_spare_slotsを使用してください)」
使用可能なストレージ型
- Vector - 小規模なシーケンシャルデータ(100件未満)
- SmartVector - 大規模なシーケンシャルデータ(100件以上)
- Table - 順序なしのキーと値による検索
- TableWithLength - 件数を追跡する Table
- OrderedMap - 小規模なソート済みマップ(100件未満)
- BigOrderedMap - 大規模なソート済みマップ(100件以上)
⚠️ SmartTable は絶対に使用しないでください(非推奨です。BigOrderedMap を使用してください)
詳細: 判断ツリー、型の比較、ガスの最適化については references/ を参照してください。
アンチパターン
- ❌ レガシーな TokenV1 標準や
aptos_token::tokenのインポートは絶対に使用しないでください - ❌ リソースアカウントは絶対に使用しないでください(代わりに名前付きオブジェクトを使用してください)
- ❌ 公開関数から ConstructorRef を絶対に返さないでください
- ❌ エントリー関数で署名者の検証を絶対に省略しないでください
- ❌ 入力の検証(数量、アドレス、文字列)を絶対に省略しないでください
- ❌ テストカバレッジ 100% を達成せずにデプロイしないでください
- ❌ 名前付きアドレスを返すだけのヘルパー関数を絶対に作成しないでください
- ❌ 重要なアクティビティに対するイベントの発行を絶対に省略しないでください
- ❌ V2 構文が使用可能な場合に、古い構文を絶対に使用しないでください
- ❌ 初期化が必要なコントラクトでは、
init_moduleを絶対に省略しないでください - ❌ 実際の秘密鍵やシークレットをコードにハードコードしないでください —
@my_addrの名前付きアドレスと"0x..."プレースホルダーを使用してください - ❌
.envや~/.aptos/config.yamlは絶対に読み込まないでください — これらには秘密鍵が含まれています
対応すべきエッジケース
| シナリオ | チェック | エラーコード |
|---|---|---|
| ゼロの数量 | assert!(amount > 0, E_ZERO_AMOUNT) | E_ZERO_AMOUNT |
| 過大な数量 | assert!(amount <= MAX, E_AMOUNT_TOO_HIGH) | E_AMOUNT_TOO_HIGH |
| 空のベクター | assert!(vector::length(&v) > 0, E_EMPTY_VECTOR) | E_EMPTY_VECTOR |
| 空の文字列 | assert!(string::length(&s) > 0, E_EMPTY_STRING) | E_EMPTY_STRING |
| 文字列が長すぎる | assert!(string::length(&s) <= MAX, E_STRING_TOO_LONG) | E_STRING_TOO_LONG |
| ゼロアドレス | assert!(addr != @0x0, E_ZERO_ADDRESS) | E_ZERO_ADDRESS |
| オーバーフロー | assert!(a <= MAX_U64 - b, E_OVERFLOW) | E_OVERFLOW |
| アンダーフロー | assert!(a >= b, E_UNDERFLOW) | E_UNDERFLOW |
| ゼロ除算 | assert!(divisor > 0, E_DIVISION_BY_ZERO) | E_DIVISION_BY_ZERO |
| 権限のないアクセス | assert!(signer == expected, E_UNAUTHORIZED) | E_UNAUTHORIZED |
| オブジェクトの所有者ではない | assert!(object::owner(obj) == user, E_NOT_OWNER) | E_NOT_OWNER |
参考資料
詳細なパターン(references/ フォルダー):
references/storage-decision-tree.md- ⭐ ストレージタイプ選択フレームワーク(ストレージについて言及されたら確認)references/storage-patterns.md- ⭐ ユースケースのパターンとスマートなデフォルト設定references/storage-types.md- 6 種類すべてのストレージタイプの詳細な比較references/storage-gas-optimization.md- ストレージのガス最適化戦略references/object-patterns.md- 名前付きオブジェクト、コレクション、ネストされたオブジェクトreferences/access-control.md- RBAC と権限システムreferences/safe-arithmetic.md- オーバーフロー/アンダーフローの防止references/initialization.md- init_module パターンとレジストリの作成references/events.md- イベント発行のパターンreferences/v2-syntax.md- Move V2 の最新機能(メソッド呼び出し、インデックス指定、ラムダ)references/complete-example.md- 注釈付きの NFT コレクションコントラクト全体
パターンのドキュメント(patterns/ フォルダー):
../../../patterns/move/DIGITAL_ASSETS.md- Digital Asset(NFT)標準 - NFT では重要../../../patterns/move/OBJECTS.md- オブジェクトモデルの包括的なガイド../../../patterns/move/SECURITY.md- セキュリティチェックリストとパターン../../../patterns/move/MOVE_V2_SYNTAX.md- 最新の構文例
公式ドキュメント:
- Digital Asset 標準:https://aptos.dev/build/smart-contracts/digital-asset
- オブジェクトモデル:https://aptos.dev/build/smart-contracts/object
- セキュリティガイドライン:https://aptos.dev/build/smart-contracts/move-security-guidelines
関連スキル:
search-aptos-examples- aptos-core で類似の例を検索します(任意)generate-tests- コントラクトのテストを作成します(コントラクト作成後に使用)security-audit- デプロイ前にコントラクトを監査します
コメント
Markdown 対応