Move Smart Contract Audit Checklist: Aptos Security Review Guide

A practical Move smart contract audit checklist for Aptos teams and auditors: resources, abilities, signer checks, object ownership, capabilities, generic types, upgrades, invariants, and Move Prover targets.

Summarize with AI

move-smart-contract-security

Move is safer than Solidity in the same way that a race car with better brakes is safer than a race car without them. Useful. Important. Still not the same thing as safe.

That distinction matters because Move removes some ugly EVM failure modes while introducing a different set of security assumptions. Resources, abilities, ownership, and module boundaries give auditors stronger guarantees than Solidity usually can. They do not remove the need to reason about capability design, object ownership, generic type binding, upgrade control, and hostile composition.

Use this guide as a practical Move smart contract audit checklist for Aptos security reviews. Start with the checklist, then use the deeper sections to understand why each item matters.

Move Smart Contract Audit Checklist

A Move audit should verify the type system, authorization model, object model, capability lifecycle, operational controls, and economic invariants together. Do not treat Move safety as automatic. Treat it as a set of guarantees that only hold when the protocol uses resources, abilities, signers, objects, and capabilities correctly.

  • Entry points: enumerate every public entry, public, and public(friend) function. Flag sensitive functions reachable through broad visibility, friend modules that act like hidden admins, and undocumented entry points.

  • Resources and abilities: list every type with key, store, copy, or drop. Flag copy on asset-like state, drop on obligations or receipts, and store on capabilities that should not persist broadly.

  • Signer authorization: verify that every &signer path checks the correct account, role, or module-owned address before changing protected state. Flag code that assumes "has signer" means "is authorized."

  • Object ownership: for every Object<T> input, prove the caller owns it or is explicitly authorized to act on it. Flag functions that accept a valid object but never compare object::owner(&obj) to the signer or expected authority.

  • Capabilities: map every capability-like struct from creation to destruction. Flag overpowered capabilities, capabilities returned to callers, ConstructorRef leaks, and stored refs with unclear lifecycle.

  • Generic type binding: confirm receipts, vouchers, debt records, and accounting proofs are bound to the same asset type they protect. Flag untyped receipts, missing phantom T, and repayment logic that proves "some token" rather than "the borrowed token."

  • Mutable trust boundaries: re-check invariants after &mut references, callbacks, function values, or delayed execution paths cross a module boundary. Flag asset identity checks that happen before a handoff but not after.

  • Arithmetic and abort behavior: test rounding, precision, zero values, max values, and unexpected aborts on valid flows. Flag fee bypass through rounding, denial of service through aborts, and unbounded loops over user-controlled data.

  • Oracle and bridge assumptions: check freshness, source validation, fallback behavior, replay resistance, and partial-failure recovery. Flag bridge messages trusted without sender validation, stale prices, and missing cancellation or retry models.

  • Upgrades and operations: identify package upgrade authority, pause controls, governance paths, key custody, and emergency procedures. Flag mixed testnet/mainnet keys, silent parameter mutation, weak upgrade custody, and no credible rollback path.

  • Invariants and proofs: define the economic invariants first, then decide what belongs in unit tests, property tests, or Move Prover specs. Flag tests that cover only happy paths or prover specs that miss economic safety.

If you only have time for a first pass, start with ownership, capabilities, generic asset binding, upgrade authority, and invariant drift after mutable handoffs. Those are the places where Move audits most often stop looking like recycled Solidity reviews.

Who This Is For

This article is for two groups:

  • Aptos teams that want to understand what a real Move security review should cover,

  • auditors coming from Solidity who need a sharper mental model before they start reviewing object-heavy Move code.

If you are already comfortable with EVM auditing, focus on the places where Move changes the security boundary rather than the places where it simply feels cleaner.

Move's Security Model: Strengths And Blind Spots

The reason Move deserves respect is simple: it pushes more safety guarantees into the language itself.

At the center of that design are resources, abilities, ownership, and module boundaries.

Resources Force Explicit Asset Handling

In Solidity, scarcity is mostly simulated through state and convention. In Move, scarcity is part of the type system.

A resource cannot be copied unless it has copy. It cannot silently disappear unless it has drop. It cannot live inside stored data unless it has store, and it cannot be used directly in global storage operations unless it has key.

Move value model

Resource type
  - no copy  -> cannot be duplicated
  - no drop  -> cannot silently vanish
  - key      -> can participate in global storage ops

Capability / ordinary value
  - copy/drop often allowed
  - easier to pass around
  - should never be confused with an owned asset

Abilities Are Small Syntax With Large Security Consequences

The four abilities are not trivia. They are part of the threat model.

Ability What it allows Why auditors care
copy duplicate a value dangerous for asset-like state
drop discard a value dangerous for obligations and receipts
store place a value inside stored data matters for persistence and composition
key use a value in global storage operations matters for authority over on-chain state

The Aptos Move abilities documentation is worth revisiting because ability composition is where seemingly small design choices become real attack surface.

Move Removes Some EVM Hazards, Not The Need For Adversarial Review

Compared to Solidity, Move gives developers real help in a few important areas:

  • no unconstrained delegatecall-style footgun,

  • stronger ownership semantics,

  • better protection against accidental asset duplication,

  • stronger module encapsulation,

  • clearer boundaries around global storage access.

That is why Move code often feels cleaner under review.

But cleaner is not the same as correct. The bugs move into capability boundaries, object ownership, generic types, mutable references, and operational control.

Where Teams Usually Get Move Security Wrong

The most common Move bugs do not come from misunderstanding the VM at the opcode level. They come from treating a language guarantee as broader than it really is.

Mistake 1: Treating &signer As Complete Authorization

Accepting a signer is not the same thing as proving that the signer should be allowed to perform the action.

The Aptos Move security guidelines are explicit here: for sensitive operations, you still need to verify that the signer is the expected account or the rightful owner of the object being acted on.

Mistake 2: Confusing Object Access With Ownership Proof

Passing an Object<T> into a function does not prove the caller owns the asset or permission represented by that object.

That subtlety creates a very Move-shaped bug class: object ownership check failures. A staking object, subscription object, or collateral object can be valid and still belong to someone else.

If you are coming from EVM audits, treat this as a close cousin of access control attacks: the syntax is different, but the broken assumption is still "this caller is allowed to do this."

Mistake 3: Treating Generics As Mere Developer Convenience

Generics are part of the security boundary.

If a receipt is not parameterized by the same asset type as the borrowed coin, the repayment path may prove only that some token was returned, not that the correct token was returned.

Mistake 4: Assuming An Invariant Survives A Mutable Handoff

If you validate an invariant and then pass &mut T across a trust boundary, you no longer control that invariant.

The callee may not be able to unpack your private fields, but it may still replace the entire value, mutate state through another path, or invalidate the assumptions you just checked.

The Move Vulnerability Classes Worth Real Audit Time

1. Object Ownership Check Failures

This is one of the cleanest examples of why Move security is not just "accept a signer and move on."

entry fun execute_action_with_valid_subscription(
    user: &signer,
    obj: Object<Subscription>
) acquires Subscription {
    let object_address = object::object_address(&obj);
    let subscription = borrow_global<Subscription>(object_address);
    assert!(subscription.end_subscription >= timestamp::now_seconds(), 1);
    // action continues...
}

That checks whether the subscription exists and is active. It does not check whether the caller owns it.

The missing guard is conceptually simple:

assert!(object::owner(&obj) == signer::address_of(user), ENOT_OWNER);

If you skip that check, a valid object can become a permission bypass.

2. Generic Type Mismatch In Asset Flows

A vulnerable flash-loan design might return (Coin<T>, Receipt) and later accept repay_flash_loan<T>(receipt: Receipt, coins: Coin<T>).

The bug is that Receipt is not actually bound to T, so the protocol proves only that a repayment amount exists, not that the repayment asset matches the borrowed asset.

struct Receipt<phantom T> has drop {
    amount: u64,
}

public fun flash_loan<T>(amount: u64): (Coin<T>, Receipt<T>) { /* ... */ }
public fun repay_flash_loan<T>(receipt: Receipt<T>, coins: Coin<T>) { /* ... */ }

The phantom parameter matters because it pushes the business rule into the type system.

3. Dangerous Ability Assignments

Giving copy to an asset-like type or drop to an obligation-like type is a serious design error.

  • copy on an asset-like value can create inflation or double-spend behavior.

  • drop on an obligation or receipt can let a borrower discard the proof they were supposed to satisfy.

This is one of the places where Move's safety story only works if the type designer makes the right choice.

4. ConstructorRef Leakage

When creating Aptos objects, exposing a ConstructorRef can leak more control than the team intended. Depending on the flow, that reference may later be turned into stronger mutation or transfer capabilities.

For NFT, escrow, and object-heavy protocols, the rule is simple: do not return or casually persist a ConstructorRef unless you have modeled the full lifecycle of every derived capability.

5. Mutable References, Function Values, And Delayed Execution

Move is still structurally more resistant to classic reentrancy than Solidity, but that does not mean every state-handoff problem disappeared.

Auditors still need to reason about:

  • &mut references crossing trust boundaries,

  • callbacks and delayed execution,

  • function values that reintroduce time-of-check versus time-of-use problems,

  • state assumptions that are valid before a handoff and false afterward.

The right takeaway is not "Move has reentrancy now." It is "Move has more ways for state assumptions to go stale than many teams expect."

For the EVM version of this mental model, review /attacks/reentrancy. In Move, the audit work is less about recursive ETH withdrawals and more about proving that state checked before a handoff is still true afterward.

6. Operational And Upgrade Assumptions

Not every serious Move bug is about types. A real review should also include:

  • package upgrade control,

  • publishing-key separation between environments,

  • oracle and pricing assumptions,

  • unbounded iteration and denial-of-service risk,

  • fee rounding and precision issues,

  • shared object-account topology when multiple resources move together.

If the upgrade key is weak, clean Move code can still become an administrative rug vector.

For pricing-sensitive protocols, also compare these checks with /attacks/oracle-manipulation-attacks. The Move type system does not protect a protocol from stale, manipulated, or incorrectly trusted market data.

Two Realistic Exploit Scenarios

Scenario 1: Worthless Asset Substitution Through &mut

This scenario is grounded in the current Aptos guidance around mutable references passed to untrusted code.

Setup

A protocol accepts a FungibleAsset, checks that it is the expected asset, then passes &mut FungibleAsset to a hook. After the hook returns, the protocol assumes the asset identity is unchanged.

Attack Flow


1. User deposits a legitimate asset.

2. Protocol validates the metadata once.

3. Protocol passes &mut asset to attacker-controlled logic.

4. Attacker swaps the value with a worthless asset.

5. Protocol resumes and mints credit against the now-worthless asset.

6. Treasury receives junk. Attacker keeps real value.

What The Auditor Should Flag

  • public APIs handing &mut into untrusted callbacks,

  • invariants checked before the handoff but not after,

  • credit, collateral, or treasury logic that assumes identity survived the call unchanged.

Scenario 2: Leaked ConstructorRef Becomes A Future Clawback Path

This one matters for NFT marketplaces, escrow systems, and object-heavy DeFi designs.

Setup

A mint function returns a ConstructorRef for convenience. The team treats that as harmless because the object was already created successfully.

Attack Flow


1. Minter receives or leaks a ConstructorRef.

2. ConstructorRef is used to derive a more powerful capability.

3. Object later gets sold or posted as collateral.

4. Original actor still holds a hidden control path.

5. Asset is clawed back, redirected, or modified after transfer.

What The Auditor Should Flag

  • any function that returns or stores ConstructorRef,

  • derived capabilities that survive longer than object creation,

  • systems that assume "minted and transferred" means "future control surface removed."

What A Serious Move Audit Should Include

If you are auditing Move-based protocols, the process should feel different from a Solidity review from day one.

1. Map Resources, Objects, Capabilities, And Entry Points

Before reading business logic deeply, enumerate:

  • every public entry function,

  • every public(friend) function,

  • every type with key, store, copy, or drop,

  • every capability-like struct,

  • every object creation path,

  • every privileged signer or admin address,

  • every upgrade path and publishing key.

On Aptos, that map is not housekeeping. It is half the audit.

2. Derive The Invariants Before Looking For Bugs

For each protocol, define the truths that must remain true:

  • asset conservation,

  • receipt and obligation conservation,

  • authorization boundaries,

  • capability uniqueness,

  • object ownership expectations,

  • collateralization rules,

  • oracle freshness assumptions,

  • upgrade and pause guarantees.

If the team cannot state its invariants clearly, the audit is already telling you something important.

3. Stress The Type System Where It Matters

Focus on:

  • generic parameter binding,

  • phantom types,

  • ability assignments,

  • signer usage,

  • resource lifecycle,

  • object account layout,

  • whether a capability is narrow or overpowered.

The central question is always the same: did the team make the type system enforce the business rule, or are they only hoping runtime logic will keep it intact?

4. Review Trust Boundaries Like Access-Control Boundaries

In Move, trust boundaries are not only obvious admin functions. They also include:

  • passing &mut to another module,

  • exposing callbacks or function values,

  • accepting user-supplied objects,

  • depending on oracles or bridges,

  • relying on off-chain operators for upgrades or sequencing.

If a stateful assumption crosses one of those boundaries, assume it can break until you prove otherwise.

5. Test Abort Behavior, Not Just Success Paths

Move often fails via aborts rather than silent corruption. That is safer, but still dangerous if the protocol depends on liveness.

Review at least these cases:

  • unexpected aborts on valid user flows,

  • precision loss that causes fee bypasses,

  • code paths that become denial-of-service vectors,

  • unbounded loops over user-controlled structures,

  • randomness flows that are biasable through aborts or gas asymmetry.

6. Review Upgrade And Operational Security

You still need to know:

  • who controls upgrades,

  • whether testnet and mainnet publishing keys are separated,

  • whether pause mechanisms work under pressure,

  • whether governance can silently mutate critical parameters,

  • whether the emergency playbook is credible.

If the operational layer is weak, the cleanest code in the repo may not matter.

By this point, the checklist at the top of the article should no longer feel like a generic review template. Each item maps to a concrete Move boundary: who owns the value, who can mutate it, whether the type system enforces the business rule, and whether the invariant still holds after hostile composition.

Tooling: Prover, Tests, And Fuzzing

The right Move audit stack is not "read code carefully and pray."

Move Prover Is Best Used On Invariants That Matter

The Move Prover guide is useful, but the more important question is what you choose to prove.

Use it on the properties that would matter in a real exploit:

  • conservation of balances,

  • uniqueness of capabilities,

  • critical postconditions after mint, burn, transfer, or admin flows,

  • absence of unexpected aborts under stated preconditions.

Unit Tests Should Model Adversarial Sequences

Functional correctness is not enough. Good test suites model:

  • wrong object owner,

  • wrong generic asset type,

  • stale or manipulated oracle values,

  • repeated callbacks,

  • aborted repayment flows,

  • asset substitution attempts after validation.

Fuzzing Still Has Room To Add Value

Move does not yet have the same mature fuzzing culture as the EVM ecosystem, which makes invariant-driven fuzzing more valuable, not less.

Useful sequences include:

  • deposit -> borrow -> repay -> liquidate,

  • mint -> transfer -> escrow -> reclaim,

  • create object -> derive refs -> transfer ownership,

  • callback-driven flows where state may change between check and use.

Secure Patterns Worth Encoding Into The Design

1. Use Narrow Capabilities

Do not create one capability that can pause the system, mint supply, withdraw treasury funds, and administer upgrades. Separate privileges so a single leaked capability does not become catastrophic.

2. Bind Business Meaning To Types

If a receipt belongs to an asset type, encode that in the type. If a voucher represents a fixed future action, encode the asset and amount so runtime logic does less guessing.

3. Prefer Signer-Bound Storage Access When It Fits

When the design allows it, borrowing or moving data directly from signer::address_of(user) is often safer than accepting arbitrary objects and hoping the caller supplied the right one.

4. Re-Validate After Trust Boundaries

If an invariant mattered before the call, it matters after the call. Re-check identity, balances, permissions, and bounds whenever a mutable or delayed-execution path could have changed them.

5. Design Object Topology Intentionally

Shared object accounts are not just an organization trick. They affect transfer and mutation semantics, so they belong in the threat model.

When Outside Review Helps

If your team is approaching mainnet with object-heavy design, custom capabilities, or upgrade-sensitive code, this is usually the stage where an external review adds the most value. The contact form is the cleanest way to start a free review conversation. If you need budgeting context for a formal audit, use the /tools/smart-contract-audit-cost-estimator.

Final Take

Move gets a lot right. Resources, abilities, ownership, and module boundaries remove real classes of bugs that have hurt EVM protocols for years.

But the bugs that remain are not cosmetic. They live in object ownership, capability design, generic type binding, mutable trust boundaries, and upgrade control. That is why a real Move audit should feel different from a recycled Solidity checklist. It should be resource-aware, object-aware, capability-aware, and relentlessly focused on invariants.