4. Ability Misconfiguration
Overview
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.
OWASP / CWE Mapping
| OWASP Top 10 | MITRE CWE |
|---|---|
| A01 (Broken Access Control) | CWE-284 (Improper Access Control), CWE-266 (Incorrect Privilege Assignment) |
The Problem
Ability Overview
| Ability | Allows | Danger |
|---|---|---|
copy |
Duplicating values | Assets can be infinitely copied |
drop |
Implicit destruction | Resources can be silently discarded |
store |
Storing inside other objects | Objects can be wrapped/transferred |
key |
Top-level object status | Can be stored in global storage |
Common Mistakes
- Giving
copyto assets — Allows infinite minting - Giving
dropto valuable items — Allows destroying value without proper handling - Giving
storeto capabilities — Allows unauthorized transfer/wrapping - Over-granting abilities — Default to minimal abilities
Vulnerable Example
module vulnerable::token {
use sui::object::{Self, UID};
use sui::tx_context::TxContext;
/// CRITICAL VULNERABILITY: Token with `copy` ability
/// Anyone can duplicate tokens infinitely!
public struct Token has copy, drop, store {
value: u64,
}
/// VULNERABLE: Capability with `store` allows transfer
public struct MintCap has key, store {
id: UID,
max_supply: u64,
}
public fun mint(cap: &MintCap, amount: u64): Token {
Token { value: amount }
}
/// VULNERABLE: Ticket that can be dropped silently
/// User might accidentally lose their ticket
public struct EventTicket has key, drop, store {
id: UID,
event_id: u64,
seat: u64,
}
}Attack: Infinite Token Duplication
module attack::duplicate {
use vulnerable::token::{Self, Token};
public fun exploit(): (Token, Token, Token) {
let original = token::mint(cap, 1000);
// Because Token has `copy`, we can duplicate it infinitely!
let copy1 = copy original;
let copy2 = copy original;
let copy3 = copy original;
// ... unlimited copies
(original, copy1, copy2)
}
}Attack: Capability Transfer
module attack::steal_cap {
use vulnerable::token::MintCap;
use sui::transfer;
public entry fun steal(cap: MintCap) {
// MintCap has `store`, so anyone who gets it can transfer it
transfer::public_transfer(cap, @attacker);
}
}Secure Example
module secure::token {
use sui::object::{Self, UID};
use sui::tx_context::{Self, TxContext};
use sui::transfer;
use sui::event;
/// SECURE: No `copy` or `drop` — true asset semantics
/// Must be explicitly created and explicitly consumed
public struct Token has key, store {
id: UID,
value: u64,
}
/// SECURE: No `store` — only this module controls transfers
public struct MintCap has key {
id: UID,
max_supply: u64,
minted: u64,
}
/// SECURE: No `drop` — must be explicitly used or refunded
public struct EventTicket has key {
id: UID,
event_id: u64,
seat: u64,
owner: address,
}
/// Event for ticket redemption
public struct TicketRedeemed has copy, drop {
ticket_id: object::ID,
event_id: u64,
seat: u64,
}
public fun mint(
cap: &mut MintCap,
amount: u64,
recipient: address,
ctx: &mut TxContext
) {
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
public fun merge(token1: Token, token2: Token, ctx: &mut TxContext): Token {
let Token { id: id1, value: v1 } = token1;
let Token { 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
public entry fun redeem_ticket(ticket: EventTicket, ctx: &TxContext) {
let EventTicket { 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
public entry fun refund_ticket(
ticket: EventTicket,
ctx: &mut TxContext
) {
let EventTicket { 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
public struct Asset has key, store {
id: UID,
value: u64,
}
/// For assets that should never leave the protocol
public struct LockedAsset has key {
id: UID,
value: u64,
}For Capabilities
/// Capability that can only be transferred by your module
public struct AdminCap has key {
id: UID,
}
/// Witness pattern — one-time use, no abilities needed
public struct WITNESS has drop {}For Receipts/Proofs
/// Hot potato — must be consumed in same transaction
public struct Receipt {
amount: u64,
}
/// Cannot be stored, copied, or dropped — forces handlingFor Events
/// Events should always have copy + drop
public struct TransferEvent has copy, drop {
from: address,
to: address,
amount: u64,
}Recommended Mitigations
1. Start with Minimal Abilities
/// Start with nothing, add only what's needed
public struct MyType { }
/// Then add based on requirements:
public struct MyType has key { } // If it needs to be an object
public struct MyType has key, store { } // If it needs transfer2. 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
public struct NFT has key, store {
id: UID,
// ...
}3. Use Wrapper Types for Different Contexts
/// Internal representation — minimal abilities
public struct TokenInner {
value: u64,
}
/// Transferable version
public struct TransferableToken has key, store {
id: UID,
inner: TokenInner,
}
/// Locked version — no `store`
public struct LockedToken has key {
id: UID,
inner: TokenInner,
unlock_time: u64,
}Testing Checklist
- Verify no asset types have
copyability - Verify valuable resources don’t have
dropunless explicitly intended - Verify capabilities lack
storeunless transfer is explicitly required - Test that types without
dropmust be explicitly consumed - Audit all
hasdeclarations against security requirements