Move’s four abilities (copy, drop, store, key) control what operations can be performed on types. Misconfiguring these abilities can allow duplication of assets, unintended destruction of resources, wrapping of objects, or unauthorized global storage access.
Risk Level
Critical — Incorrect abilities can break fundamental economic invariants.
Giving drop to valuable items — Allows destroying value without proper handling
Giving store to capabilities — Allows unauthorized transfer/wrapping
Over-granting abilities — Default to minimal abilities
Vulnerable Example
modulevulnerable::token{usesui::object::{Self,UID};usesui::tx_context::TxContext;/// CRITICAL VULNERABILITY: Token with `copy` ability
/// Anyone can duplicate tokens infinitely!
publicstructTokenhascopy,drop,store{value: u64,}/// VULNERABLE: Capability with `store` allows transfer
publicstructMintCaphaskey,store{id: UID,max_supply: u64,}publicfunmint(cap: &MintCap,amount: u64): Token{Token{value: amount}}/// VULNERABLE: Ticket that can be dropped silently
/// User might accidentally lose their ticket
publicstructEventTickethaskey,drop,store{id: UID,event_id: u64,seat: u64,}}
Attack: Infinite Token Duplication
moduleattack::duplicate{usevulnerable::token::{Self,Token};publicfunexploit(): (Token,Token,Token){letoriginal=token::mint(cap,1000);// Because Token has `copy`, we can duplicate it infinitely!
letcopy1=copyoriginal;letcopy2=copyoriginal;letcopy3=copyoriginal;// ... unlimited copies
(original,copy1,copy2)}}
Attack: Capability Transfer
moduleattack::steal_cap{usevulnerable::token::MintCap;usesui::transfer;publicentryfunsteal(cap: MintCap){// MintCap has `store`, so anyone who gets it can transfer it
transfer::public_transfer(cap,@attacker);}}
Secure Example
modulesecure::token{usesui::object::{Self,UID};usesui::tx_context::{Self,TxContext};usesui::transfer;usesui::event;/// SECURE: No `copy` or `drop` — true asset semantics
/// Must be explicitly created and explicitly consumed
publicstructTokenhaskey,store{id: UID,value: u64,}/// SECURE: No `store` — only this module controls transfers
publicstructMintCaphaskey{id: UID,max_supply: u64,minted: u64,}/// SECURE: No `drop` — must be explicitly used or refunded
publicstructEventTickethaskey{id: UID,event_id: u64,seat: u64,owner: address,}/// Event for ticket redemption
publicstructTicketRedeemedhascopy,drop{ticket_id: object::ID,event_id: u64,seat: u64,}publicfunmint(cap: &mutMintCap,amount: u64,recipient: address,ctx: &mutTxContext){assert!(cap.minted+amount<=cap.max_supply,E_EXCEEDS_SUPPLY);cap.minted=cap.minted+amount;transfer::transfer(Token{id: object::new(ctx),value: amount},recipient);}/// Tokens must be explicitly merged or split
publicfunmerge(token1: Token,token2: Token,ctx: &mutTxContext): Token{letToken{id: id1,value: v1}=token1;letToken{id: id2,value: v2}=token2;object::delete(id1);object::delete(id2);Token{id: object::new(ctx),value: v1+v2}}/// Tickets must be explicitly redeemed — cannot be dropped
publicentryfunredeem_ticket(ticket: EventTicket,ctx: &TxContext){letEventTicket{id,event_id,seat,owner}=ticket;// Verify caller is the ticket owner
assert!(tx_context::sender(ctx)==owner,E_NOT_OWNER);event::emit(TicketRedeemed{ticket_id: object::uid_to_inner(&id),event_id,seat,});object::delete(id);}/// Explicit refund path for tickets
publicentryfunrefund_ticket(ticket: EventTicket,ctx: &mutTxContext){letEventTicket{id,event_id: _,seat: _,owner}=ticket;assert!(tx_context::sender(ctx)==owner,E_NOT_OWNER);object::delete(id);// ... refund logic
}}
Ability Selection Guidelines
For Assets (Tokens, NFTs, Items)
/// Minimal abilities for fungible-like assets
publicstructAssethaskey,store{id: UID,value: u64,}/// For assets that should never leave the protocol
publicstructLockedAssethaskey{id: UID,value: u64,}
For Capabilities
/// Capability that can only be transferred by your module
publicstructAdminCaphaskey{id: UID,}/// Witness pattern — one-time use, no abilities needed
publicstructWITNESShasdrop{}
For Receipts/Proofs
/// Hot potato — must be consumed in same transaction
publicstructReceipt{amount: u64,}/// Cannot be stored, copied, or dropped — forces handling
For Events
/// Events should always have copy + drop
publicstructTransferEventhascopy,drop{from: address,to: address,amount: u64,}
Recommended Mitigations
1. Start with Minimal Abilities
/// Start with nothing, add only what's needed
publicstructMyType{}/// Then add based on requirements:
publicstructMyTypehaskey{}// If it needs to be an object
publicstructMyTypehaskey,store{}// If it needs transfer
2. Document Why Each Ability is Needed
/// `key`: Required for object storage
/// `store`: Required for marketplace listing (intentional risk accepted)
/// NO `copy`: Asset must not be duplicable
/// NO `drop`: Asset must be explicitly consumed
publicstructNFThaskey,store{id: UID,// ...
}
3. Use Wrapper Types for Different Contexts
/// Internal representation — minimal abilities
publicstructTokenInner{value: u64,}/// Transferable version
publicstructTransferableTokenhaskey,store{id: UID,inner: TokenInner,}/// Locked version — no `store`
publicstructLockedTokenhaskey{id: UID,inner: TokenInner,unlock_time: u64,}
Testing Checklist
Verify no asset types have copy ability
Verify valuable resources don’t have drop unless explicitly intended
Verify capabilities lack store unless transfer is explicitly required
Test that types without drop must be explicitly consumed
Audit all has declarations against security requirements