Write Contracts Skill
Core Rules
Digital Assets (NFTs) ⭐ CRITICAL
- ALWAYS use Digital Asset (DA) standard for ALL NFT-related contracts (collections, marketplaces, minting)
- ALWAYS import
aptos_token_objects::collection and aptos_token_objects::token modules
- ALWAYS use
Object<AptosToken> for NFT references (NOT generic Object<T>)
- NEVER use legacy TokenV1 standard or
aptos_token::token module (deprecated)
- See
../../../patterns/move/DIGITAL_ASSETS.md for complete NFT patterns
Object Model
- ALWAYS use
Object<T> for all object references (NEVER raw addresses)
- Generate all refs (TransferRef, DeleteRef) in constructor before ConstructorRef destroyed
- Return
Object<T> from constructors (NEVER return ConstructorRef)
- Verify ownership with
object::owner(obj) == signer::address_of(user)
- Use
object::generate_signer(&constructor_ref) for object signers
- Use named objects for singletons:
object::create_named_object(creator, seed)
Security
- ALWAYS verify signer authority in entry functions:
assert!(signer::address_of(user) == expected, E_UNAUTHORIZED)
- ALWAYS validate inputs: non-zero amounts, address validation, string length checks
- NEVER expose
&mut references in public functions
- NEVER skip signer verification in entry functions
Modern Syntax
- Use inline functions and lambdas for iteration
- Use receiver-style method calls:
obj.is_owner(user) (define first param as self)
- Use vector indexing:
vector[index] instead of vector::borrow()
- Use direct named addresses:
@marketplace_addr (NOT helper functions)
Required Patterns
- Use init_module for contract initialization on deployment
- Emit events for ALL significant activities (create, transfer, update, delete)
- Define clear error constants with descriptive names (E_NOT_OWNER, E_INSUFFICIENT_BALANCE)
Testability
- Add accessor functions for struct fields - tests in separate modules cannot access struct fields directly
- Use
#[view] annotation for read-only accessor functions
- Return tuples from accessors for multi-field access:
(seller, price, timestamp)
- Place
#[view] BEFORE doc comments - /// comment before #[view] causes compiler warnings. Write #[view]
first, then ///
Quick Workflow
- Create module structure → Define structs, events, constants, init_module
- Implement object creation → Use proper constructor pattern with all refs generated upfront
- Add access control → Verify ownership and validate all inputs
- Security check → Use
security-audit skill before deployment
Key Example: Object Creation Pattern
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;
}
Key Example: Accessor Functions for Testing
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 Structure Template
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
}
Storage Type Selection
⚠️ When user mentions storage ("store", "track", "registry", "mapping", "list", "collection"):
1. Ask 2-3 Questions (see references/storage-decision-tree.md)
- Access pattern? (sequential vs key-value vs both)
- Expected size? (small vs large vs unknown)
- Need
.length()? (conditional)
2. Recommend from Patterns (references/storage-patterns.md)
| Pattern |
Recommended Storage |
| User registry |
Table<address, UserInfo> |
| Staking records |
Table<address, StakeInfo> |
| Leaderboard |
BigOrderedMap<u64, address> |
| Transaction log |
SmartVector<TxRecord> or Vector |
| Whitelist (<100) |
Vector<address> |
| Voting records |
TableWithLength<address, bool> |
| Config (<50) |
OrderedMap<String, Value> |
| DAO proposals |
BigOrderedMap<u64, Proposal> |
| Asset collection |
Vector<Object<T>> or SmartVector |
3. Include Brief Gas Context
Example recommendations:
- "For staking, I recommend
Table<address, StakeInfo> because you'll have unbounded users with concurrent operations
(separate slots enable parallel access)"
- "For leaderboard, I recommend
BigOrderedMap<u64, address> because you need sorted iteration (O(log n), use
allocate_spare_slots for production)"
Storage Types Available
- Vector - Small sequential (<100 items)
- SmartVector - Large sequential (100+ items)
- Table - Unordered key-value lookups
- TableWithLength - Table with count tracking
- OrderedMap - Small sorted maps (<100 items)
- BigOrderedMap - Large sorted maps (100+ items)
⚠️ NEVER use SmartTable (deprecated, use BigOrderedMap)
Details: See references/ for decision tree, type comparisons, and gas optimization.
Anti-patterns
- ❌ Never use legacy TokenV1 standard or import
aptos_token::token
- ❌ Never use resource accounts (use named objects instead)
- ❌ Never return ConstructorRef from public functions
- ❌ Never skip signer verification in entry functions
- ❌ Never skip input validation (amounts, addresses, strings)
- ❌ Never deploy without 100% test coverage
- ❌ Never create helper functions that just return named addresses
- ❌ Never skip event emission for significant activities
- ❌ Never use old syntax when V2 syntax is available
- ❌ Never skip init_module for contracts that need initialization
- ❌ Never hardcode real private keys or secrets in code — use
@my_addr named addresses and "0x..."
placeholders
- ❌ Never read
.env or ~/.aptos/config.yaml — these contain private keys
Edge Cases to Handle
| Scenario |
Check |
Error Code |
| Zero amounts |
assert!(amount > 0, E_ZERO_AMOUNT) |
E_ZERO_AMOUNT |
| Excessive amounts |
assert!(amount <= MAX, E_AMOUNT_TOO_HIGH) |
E_AMOUNT_TOO_HIGH |
| Empty vectors |
assert!(vector::length(&v) > 0, E_EMPTY_VECTOR) |
E_EMPTY_VECTOR |
| Empty strings |
assert!(string::length(&s) > 0, E_EMPTY_STRING) |
E_EMPTY_STRING |
| Strings too long |
assert!(string::length(&s) <= MAX, E_STRING_TOO_LONG) |
E_STRING_TOO_LONG |
| Zero address |
assert!(addr != @0x0, E_ZERO_ADDRESS) |
E_ZERO_ADDRESS |
| Overflow |
assert!(a <= MAX_U64 - b, E_OVERFLOW) |
E_OVERFLOW |
| Underflow |
assert!(a >= b, E_UNDERFLOW) |
E_UNDERFLOW |
| Division by zero |
assert!(divisor > 0, E_DIVISION_BY_ZERO) |
E_DIVISION_BY_ZERO |
| Unauthorized access |
assert!(signer == expected, E_UNAUTHORIZED) |
E_UNAUTHORIZED |
| Not object owner |
assert!(object::owner(obj) == user, E_NOT_OWNER) |
E_NOT_OWNER |
References
Detailed Patterns (references/ folder):
references/storage-decision-tree.md - ⭐ Storage type selection framework (ask when storage mentioned)
references/storage-patterns.md - ⭐ Use-case patterns and smart defaults
references/storage-types.md - Detailed comparison of all 6 storage types
references/storage-gas-optimization.md - Gas optimization strategies for storage
references/object-patterns.md - Named objects, collections, nested objects
references/access-control.md - RBAC and permission systems
references/safe-arithmetic.md - Overflow/underflow prevention
references/initialization.md - init_module patterns and registry creation
references/events.md - Event emission patterns
references/v2-syntax.md - Modern Move V2 features (method calls, indexing, lambdas)
references/complete-example.md - Full annotated NFT collection contract
Pattern Documentation (patterns/ folder):
../../../patterns/move/DIGITAL_ASSETS.md - Digital Asset (NFT) standard - CRITICAL for NFTs
../../../patterns/move/OBJECTS.md - Comprehensive object model guide
../../../patterns/move/SECURITY.md - Security checklist and patterns
../../../patterns/move/MOVE_V2_SYNTAX.md - Modern syntax examples
Official Documentation:
Related Skills:
search-aptos-examples - Find similar examples in aptos-core (optional)
generate-tests - Write tests for contracts (use AFTER writing contracts)
security-audit - Audit contracts before deployment
1---2name: write-contracts3description: Generates secure Aptos Move V2 smart contracts with Object model, Digital Asset integration, security patterns, and storage type guidance. Includes comprehensive storage decision framework for optimal data structure selection. Triggers on: 'write contract', 'create NFT collection', 'build marketplace', 'implement minting', 'generate Move module', 'create token contract', 'build DAO', 'implement staking'. Ask storage questions when: 'store', 'track', 'registry', 'mapping', 'list', 'collection'.4license: MIT5---67# Write Contracts Skill89## Core Rules1011### Digital Assets (NFTs) ⭐ CRITICAL12131. **ALWAYS use Digital Asset (DA) standard** for ALL NFT-related contracts (collections, marketplaces, minting)142. **ALWAYS import** `aptos_token_objects::collection` and `aptos_token_objects::token` modules153. **ALWAYS use** `Object<AptosToken>` for NFT references (NOT generic `Object<T>`)164. **NEVER use legacy TokenV1** standard or `aptos_token::token` module (deprecated)175. See `../../../patterns/move/DIGITAL_ASSETS.md` for complete NFT patterns1819### Object Model20216. **ALWAYS use** `Object<T>` for all object references (NEVER raw addresses)227. **Generate all refs** (TransferRef, DeleteRef) in constructor before ConstructorRef destroyed238. **Return** `Object<T>` from constructors (NEVER return ConstructorRef)249. **Verify ownership** with `object::owner(obj) == signer::address_of(user)`2510. **Use** `object::generate_signer(&constructor_ref)` for object signers2611. **Use named objects** for singletons: `object::create_named_object(creator, seed)`2728### Security293012. **ALWAYS verify signer authority** in entry functions:31 `assert!(signer::address_of(user) == expected, E_UNAUTHORIZED)`3213. **ALWAYS validate inputs**: non-zero amounts, address validation, string length checks3314. **NEVER expose** `&mut` references in public functions3415. **NEVER skip** signer verification in entry functions3536### Modern Syntax373816. **Use inline functions** and lambdas for iteration3917. **Use receiver-style** method calls: `obj.is_owner(user)` (define first param as `self`)4018. **Use vector indexing**: `vector[index]` instead of `vector::borrow()`4119. **Use direct named addresses**: `@marketplace_addr` (NOT helper functions)4243### Required Patterns444520. **Use init_module** for contract initialization on deployment4621. **Emit events** for ALL significant activities (create, transfer, update, delete)4722. **Define clear error constants** with descriptive names (E_NOT_OWNER, E_INSUFFICIENT_BALANCE)4849### Testability505123. **Add accessor functions** for struct fields - tests in separate modules cannot access struct fields directly5224. **Use `#[view]` annotation** for read-only accessor functions5325. **Return tuples** from accessors for multi-field access: `(seller, price, timestamp)`5426. **Place `#[view]` BEFORE doc comments** - `/// comment` before `#[view]` causes compiler warnings. Write `#[view]`55 first, then `///`5657## Quick Workflow58591. **Create module structure** → Define structs, events, constants, init_module602. **Implement object creation** → Use proper constructor pattern with all refs generated upfront613. **Add access control** → Verify ownership and validate all inputs624. **Security check** → Use `security-audit` skill before deployment6364## Key Example: Object Creation Pattern6566```move67struct MyObject has key {68 name: String,69 transfer_ref: object::TransferRef,70 delete_ref: object::DeleteRef,71}7273// Error constants74const E_NOT_OWNER: u64 = 1;75const E_EMPTY_STRING: u64 = 2;76const E_NAME_TOO_LONG: u64 = 3;7778// Configuration constants79const MAX_NAME_LENGTH: u64 = 100;8081/// Create object with proper pattern82public fun create_my_object(creator: &signer, name: String): Object<MyObject> {83 // 1. Create object84 let constructor_ref = object::create_object(signer::address_of(creator));8586 // 2. Generate ALL refs you'll need BEFORE constructor_ref is destroyed87 let transfer_ref = object::generate_transfer_ref(&constructor_ref);88 let delete_ref = object::generate_delete_ref(&constructor_ref);8990 // 3. Get object signer91 let object_signer = object::generate_signer(&constructor_ref);9293 // 4. Store data in object94 move_to(&object_signer, MyObject {95 name,96 transfer_ref,97 delete_ref,98 });99100 // 5. Return typed object reference (ConstructorRef automatically destroyed)101 object::object_from_constructor_ref<MyObject>(&constructor_ref)102}103104/// Update with ownership verification105public entry fun update_object(106 owner: &signer,107 obj: Object<MyObject>,108 new_name: String109) acquires MyObject {110 // ✅ ALWAYS: Verify ownership111 assert!(object::owner(obj) == signer::address_of(owner), E_NOT_OWNER);112113 // ✅ ALWAYS: Validate inputs114 assert!(string::length(&new_name) > 0, E_EMPTY_STRING);115 assert!(string::length(&new_name) <= MAX_NAME_LENGTH, E_NAME_TOO_LONG);116117 // Safe to proceed118 let obj_data = borrow_global_mut<MyObject>(object::object_address(&obj));119 obj_data.name = new_name;120}121```122123## Key Example: Accessor Functions for Testing124125```move126struct ListingInfo has store, drop, copy {127 seller: address,128 price: u64,129 listed_at: u64,130}131132/// Accessor function - tests cannot access struct fields directly133/// Use tuple returns for multiple fields134#[view]135public fun get_listing_details(nft_addr: address): (address, u64, u64) acquires Listings {136 let listings = borrow_global<Listings>(get_marketplace_address());137 assert!(table::contains(&listings.items, nft_addr), E_NOT_LISTED);138 let listing = table::borrow(&listings.items, nft_addr);139 (listing.seller, listing.price, listing.listed_at)140}141142/// Single-field accessor when only one value needed143#[view]144public fun get_staked_amount(user_addr: address): u64 acquires Stakes {145 let stakes = borrow_global<Stakes>(get_vault_address());146 if (table_with_length::contains(&stakes.items, user_addr)) {147 table_with_length::borrow(&stakes.items, user_addr).amount148 } else {149 0150 }151}152```153154## Module Structure Template155156```move157module my_addr::my_module {158 // ============ Imports ============159 use std::signer;160 use std::string::String;161 use aptos_framework::object::{Self, Object};162 use aptos_framework::event;163164 // ============ Events ============165 #[event]166 struct ItemCreated has drop, store {167 item: address,168 creator: address,169 }170171 // ============ Structs ============172 // Define your data structures173174 // ============ Constants ============175 const E_NOT_OWNER: u64 = 1;176 const E_UNAUTHORIZED: u64 = 2;177178 // ============ Init Module ============179 fun init_module(deployer: &signer) {180 // Initialize global state, registries, etc.181 }182183 // ============ Public Entry Functions ============184 // User-facing functions185186 // ============ Public Functions ============187 // Composable functions188189 // ============ Private Functions ============190 // Internal helpers191}192```193194## Storage Type Selection195196⚠️ **When user mentions storage** ("store", "track", "registry", "mapping", "list", "collection"):197198### 1. Ask 2-3 Questions (see `references/storage-decision-tree.md`)199200- **Access pattern?** (sequential vs key-value vs both)201- **Expected size?** (small vs large vs unknown)202- **Need `.length()`?** (conditional)203204### 2. Recommend from Patterns (`references/storage-patterns.md`)205206| Pattern | Recommended Storage |207| ---------------- | ------------------------------------ |208| User registry | `Table<address, UserInfo>` |209| Staking records | `Table<address, StakeInfo>` |210| Leaderboard | `BigOrderedMap<u64, address>` |211| Transaction log | `SmartVector<TxRecord>` or `Vector` |212| Whitelist (<100) | `Vector<address>` |213| Voting records | `TableWithLength<address, bool>` |214| Config (<50) | `OrderedMap<String, Value>` |215| DAO proposals | `BigOrderedMap<u64, Proposal>` |216| Asset collection | `Vector<Object<T>>` or `SmartVector` |217218### 3. Include Brief Gas Context219220**Example recommendations:**221222- "For staking, I recommend `Table<address, StakeInfo>` because you'll have unbounded users with concurrent operations223 (separate slots enable parallel access)"224- "For leaderboard, I recommend `BigOrderedMap<u64, address>` because you need sorted iteration (O(log n), use225 `allocate_spare_slots` for production)"226227### Storage Types Available228229- **Vector** - Small sequential (<100 items)230- **SmartVector** - Large sequential (100+ items)231- **Table** - Unordered key-value lookups232- **TableWithLength** - Table with count tracking233- **OrderedMap** - Small sorted maps (<100 items)234- **BigOrderedMap** - Large sorted maps (100+ items)235236⚠️ **NEVER use SmartTable** (deprecated, use BigOrderedMap)237238**Details:** See `references/` for decision tree, type comparisons, and gas optimization.239240## Anti-patterns2412421. ❌ **Never use legacy TokenV1** standard or import `aptos_token::token`2432. ❌ **Never use resource accounts** (use named objects instead)2443. ❌ **Never return ConstructorRef** from public functions2454. ❌ **Never skip signer verification** in entry functions2465. ❌ **Never skip input validation** (amounts, addresses, strings)2476. ❌ **Never deploy without 100% test coverage**2487. ❌ **Never create helper functions** that just return named addresses2498. ❌ **Never skip event emission** for significant activities2509. ❌ **Never use old syntax** when V2 syntax is available25110. ❌ **Never skip init_module** for contracts that need initialization25211. ❌ **Never hardcode real private keys** or secrets in code — use `@my_addr` named addresses and `"0x..."`253 placeholders25412. ❌ **Never read `.env` or `~/.aptos/config.yaml`** — these contain private keys255256## Edge Cases to Handle257258| Scenario | Check | Error Code |259| ------------------- | ------------------------------------------------------- | ------------------ |260| Zero amounts | `assert!(amount > 0, E_ZERO_AMOUNT)` | E_ZERO_AMOUNT |261| Excessive amounts | `assert!(amount <= MAX, E_AMOUNT_TOO_HIGH)` | E_AMOUNT_TOO_HIGH |262| Empty vectors | `assert!(vector::length(&v) > 0, E_EMPTY_VECTOR)` | E_EMPTY_VECTOR |263| Empty strings | `assert!(string::length(&s) > 0, E_EMPTY_STRING)` | E_EMPTY_STRING |264| Strings too long | `assert!(string::length(&s) <= MAX, E_STRING_TOO_LONG)` | E_STRING_TOO_LONG |265| Zero address | `assert!(addr != @0x0, E_ZERO_ADDRESS)` | E_ZERO_ADDRESS |266| Overflow | `assert!(a <= MAX_U64 - b, E_OVERFLOW)` | E_OVERFLOW |267| Underflow | `assert!(a >= b, E_UNDERFLOW)` | E_UNDERFLOW |268| Division by zero | `assert!(divisor > 0, E_DIVISION_BY_ZERO)` | E_DIVISION_BY_ZERO |269| Unauthorized access | `assert!(signer == expected, E_UNAUTHORIZED)` | E_UNAUTHORIZED |270| Not object owner | `assert!(object::owner(obj) == user, E_NOT_OWNER)` | E_NOT_OWNER |271272## References273274**Detailed Patterns (references/ folder):**275276- `references/storage-decision-tree.md` - ⭐ Storage type selection framework (ask when storage mentioned)277- `references/storage-patterns.md` - ⭐ Use-case patterns and smart defaults278- `references/storage-types.md` - Detailed comparison of all 6 storage types279- `references/storage-gas-optimization.md` - Gas optimization strategies for storage280- `references/object-patterns.md` - Named objects, collections, nested objects281- `references/access-control.md` - RBAC and permission systems282- `references/safe-arithmetic.md` - Overflow/underflow prevention283- `references/initialization.md` - init_module patterns and registry creation284- `references/events.md` - Event emission patterns285- `references/v2-syntax.md` - Modern Move V2 features (method calls, indexing, lambdas)286- `references/complete-example.md` - Full annotated NFT collection contract287288**Pattern Documentation (patterns/ folder):**289290- `../../../patterns/move/DIGITAL_ASSETS.md` - Digital Asset (NFT) standard - CRITICAL for NFTs291- `../../../patterns/move/OBJECTS.md` - Comprehensive object model guide292- `../../../patterns/move/SECURITY.md` - Security checklist and patterns293- `../../../patterns/move/MOVE_V2_SYNTAX.md` - Modern syntax examples294295**Official Documentation:**296297- Digital Asset Standard: https://aptos.dev/build/smart-contracts/digital-asset298- Object Model: https://aptos.dev/build/smart-contracts/object299- Security Guidelines: https://aptos.dev/build/smart-contracts/move-security-guidelines300301**Related Skills:**302303- `search-aptos-examples` - Find similar examples in aptos-core (optional)304- `generate-tests` - Write tests for contracts (use AFTER writing contracts)305- `security-audit` - Audit contracts before deployment