Skip to the content.

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

Trade-offs

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