Ability Declarations as an Access Decision
Category: Abilities & Types · Covered by: Load, for the consequences; yours, for the declaration · Requires: any Aptos Move version
What this prevents
A struct is given an ability for convenience, and that ability turns out to permit an operation the
design depends on being impossible. copy on a type representing an amount means the amount can be
duplicated. drop on a type representing an obligation means the obligation can be discarded by
letting it go out of scope.
Nothing about the resulting code looks wrong. Every operation is one the type permits, performed through the module’s own public API.
Coverage
This is the clearest place where the platform is at its strongest and covers nothing that matters.
The bytecode verifier enforces every consequence of an ability, at load, against any bytecode that
reaches the chain. Pop requires drop. CopyLoc and ReadRef require copy. Overwriting a value
requires drop on the value being overwritten. Returning while a local still holds a value that
lacks drop fails. Generic instantiations must satisfy the constraints their type parameters
declare. There is no way to talk a module past any of it.
What the verifier never does is ask whether the declaration was right. struct Token has copy is
well-typed bytecode. So is struct FlashLoan has drop, which lets a borrower destroy the obligation
instead of repaying it.
The result is maximum enforcement of the mechanism and no enforcement of the judgment. A reviewer who reads “the verifier guarantees resource safety” and stops there has read the guarantee correctly and drawn the wrong conclusion from it.
What is left to you
Every ability on every struct, read as a permission rather than as a property.
The useful question is not “does this type need copy” but “what becomes possible if this type is
copyable, and is that acceptable”. For a value representing an amount, an obligation, a capability,
or a claim on something, the answers are usually duplication, discharge, forgery and double-spend
respectively.
Two habits help. Declare nothing by default and add abilities when a use case forces it, rather than
declaring a comfortable set up front. And treat copy and drop on any type that stands for a
quantity or a right as findings until argued otherwise; store and key are usually structural
rather than dangerous, though store does decide whether a value can be parked inside another
module’s struct.
Sample and test
See src/sources/AbilityDeclarations.move, exercised by
src/tests/AbilityDeclarations_test.move. Three cases: duplication, silent discard, and the
same design without the abilities.
The first two deliberately pass. They mint one coin, hold two, and then let a balance vanish without
being burned or accounted for, using only public functions. That is the failure shown working. The
third case shows conservation holding once copy and drop are removed, which is the whole fix.
Key excerpt:
/// Declared `copy` so callers can read a balance without ceremony.
struct LooseCoin has copy, drop, store { value: u64 }
/// The same type without them. Reading now requires a reference, which is the
/// small inconvenience that buys conservation.
struct Coin has store { value: u64 }
Review questions
- For each struct, what does each declared ability permit, stated as an operation rather than as a capability name?
- Does any type standing for an amount, an obligation, a capability, or a claim carry
copy? - Does any type representing an obligation carry
drop, and if so what discharges it? - Was the ability set chosen, or inherited from a template or an example?
- Does
storeon this type let another module hold it somewhere the design did not anticipate?
Trade-offs
- Pro: removing an ability is free at runtime and enforced at load, so the constraint costs nothing and cannot be bypassed once made.
- Con / cost: a type without
copyordropis less convenient, and the module has to provide accessors and an explicit destructor. That inconvenience is what makes the guarantee hold. - Trust note: abilities are per-type, not per-use. A type that legitimately needs
copyfor one purpose is copyable everywhere, so a type used two ways may need splitting.
Historical note
The ability system replaced Move’s earlier resource-versus-copyable distinction, generalising a binary choice into four independent permissions. That made the language more expressive and moved more of the decision onto the module author, which is why the declaration deserves the scrutiny the old keyword did not need.
References
aptos-core,third_party/move/move-bytecode-verifier/src/type_safety.rs, for the ability precondition on each instructionaptos-core,third_party/move/move-bytecode-verifier/src/locals_safety/mod.rs, for the drop-on-return ruleaptos-core,third_party/move/move-bytecode-verifier/src/signature_v2.rs, for constraint checking on generic instantiations