How a Move contract gets compromised
Move's design assumption is that a typed resource cannot be duplicated, dropped, or forged. The language's affine type system enforces this at the compiler, expressed through four abilities a type can declare: copy (can be copied), drop (can be discarded), store (can be stored inside other resources), and key (can be used as a top-level resource at an address). A resource that lacks copy cannot be duplicated; a resource that lacks drop must be explicitly consumed. This is what makes Move's type system load-bearing for security: the absence of an ability is as meaningful as its presence. Most Move exploits do not break that assumption directly. They break it indirectly: through a capability token that leaks, a generic constraint that allows the wrong type, a module-friend relationship that escalates trust, or a transfer policy that does not enforce what its name suggests.
Move's compiler eliminates the bug classes EVM contracts spend most of their audit budget on (reentrancy, integer overflow, storage layout, access-control modifiers). The bug classes Move ships with are different. The pattern catalog below names them.
The audit methodology applies to Move the same way it applies to EVM. Read it first if you have not.
Two dialects: Sui Move and Aptos Move
The Move language has two production dialects. The differences shape the audit work.
Aptos Move is account-centric. Resources are stored at account addresses. A module's resources sit under the addresses that called the module. Access control is the same as Solana's: the module decides which addresses can read or write its resources.
Sui Move is object-centric. Resources are not stored at addresses; they are first-class objects with their own IDs. Ownership is a property of the object, not the address. The object model gives Sui parallel execution and a different mental model for the audit.
A Move audit starts with: which dialect is this, and which mental model applies?
What we look at first
The module structure and friend relationships. Modules can declare other modules as friend, granting them privileged access to internal functions. A friend relationship is a trust extension. We map every friend declaration and verify the trust is defensible.
The capability tokens. A capability is a resource that grants the holder authority. MintCap, BurnCap, AdminCap, and protocol-specific capabilities are common patterns. Whoever holds the capability has the authority. We trace every capability from creation to current holder.
The generic type constraints. Move uses generics extensively. Constraints like T: store + drop + key declare what a type can do. A constraint that is too loose accepts types the protocol did not anticipate.
The entry-function surface. Entry functions are the public API of a Move module. Every entry function is callable by any signer; the access control is entirely in the function body. We audit every entry function against the authority model.
The pattern catalog
Capability leakage. A capability stored at a public address, returned from a function that does not enforce caller permissions, or transferable through a function that should be authority-gated, is a capability that has leaked. We hunt for every capability resource and verify the path from creation to storage is constrained.
Resource theft via wrong owner (Aptos). move_to<T>(account, resource) stores a resource under account. A function that calls move_to with an attacker-controlled account parameter, when the caller intended to store under their own address, ships the resource to the attacker.
Object transfer policy gaps (Sui). Sui objects can have transfer policies that enforce constraints on each transfer. A transfer policy that omits a required check (royalty payment, KYC validation, allowlist) is a policy that has shipped with a hole. We audit every transfer policy against the constraint the protocol meant to enforce.
Generic type constraint laxity. A function fun deposit<T: store>(amount: Coin<T>) accepts any token type. If the protocol assumed T is one of a known set of tokens (USDC, USDT), the constraint is too loose. The mitigation is T: store + KnownToken with an explicit constraint, or runtime allowlist checks.
Module-friend escalation. A friend declaration grants the friend module access to internal functions. If the friend module is itself extensible (publishable by a third party, or with its own friend relationships), the trust chain extends transitively in ways the original module did not intend.
Generic type instantiation attacks. An attacker who can instantiate a generic function with a type they control (often through a custom struct with specific drop/copy semantics) can change the function's behavior in ways the original author did not consider. Audit-time question: every generic, what constraints are necessary and sufficient?
Object ID confusion (Sui). Sui objects have IDs. A function that accepts an object ID and looks up the object can be passed an ID of an object the caller does not own. The protocol must verify ownership before granting access. Sui's transfer::public_share_object and ownership-check functions help; the audit-time check is whether they were used everywhere they should be.
public_transfer vs transfer (Sui). Sui exposes two transfer paths. transfer::transfer is restricted to the module that defines the object type (private transfer). transfer::public_transfer is callable from outside the defining module, but requires the object to have the store ability. A protocol that declares an object with store and then assumes only its own module can transfer it has misunderstood the constraint. We check every object type's abilities against the transfer paths the protocol intends to expose.
Resource accounts (Aptos). Aptos's resource-account pattern creates an account whose authority is held by a module rather than a private key. Resource accounts are a common pattern for protocols that need to hold funds without a keyholder. The audit-time check is the path the protocol uses to authorize actions from the resource account; a SignerCapability resource that leaks (stored in a public location, returned from a function without authorization) hands control of the resource account to whoever obtains it.
Display metadata manipulation (Sui). Sui's display::Display<T> configures how objects appear in wallets and explorers. An attacker who controls display metadata can show users misleading information about objects the protocol issued. Not a fund-loss vulnerability; a phishing-amplifier.
Account-resource enumeration (Aptos). Aptos provides functions that enumerate the resources stored at an account. A protocol that assumes its resource is uniquely identifiable by type at an address can be confused by other modules also storing resources of related types under the same address.
What we audit on a Move contract
We audit:
- every capability resource against the authority model it implies
- every friend declaration against the trust extension it creates
- every generic type constraint against what the constraint is sufficient and necessary for
- every entry function against the signer authority required
- every
move_to/move_from(Aptos) for owner correctness - every object transfer (Sui) for policy completeness
- every display metadata declaration for phishing exposure
- every account-resource read for enumeration assumptions
The deliverable is the audit report described in our methodology.
Tools that earn their place
The Move bytecode verifier runs at module publication time and enforces the language's safety properties (ability constraints, reference safety, type linking) on compiled bytecode, not just source. The bytecode verifier is what gives Move's type-system guarantees their teeth: a hand-crafted bytecode module that violates the source-level rules is rejected at publish.
Move Prover (Aptos) is a formal-verification system for Move modules. Specifications are written in the Move Specification Language (MSL); the prover checks they hold for all inputs. Mature; production-grade; high upfront cost in spec writing. Production Aptos protocols ship with MSL specifications; the audit reviews the specs as carefully as the code.
Move's built-in unit test framework (#[test] annotations) runs locally with the move CLI. Standard for unit and module-level integration tests.
Sui's static analyzer integrates with the Sui CLI. Catches the patterns Sui's typesystem can identify ahead of formal verification.
Property-based fuzzing on Move is less mature than on EVM. Some ecosystems have begun adapting Echidna-style invariant testing patterns; production-grade tooling is still emerging, so manual review and Move Prover specifications carry more weight than fuzzers do today.
What clean means
A clean Move audit means the capability authority, friend relationships, and generic constraints are sufficient for the protocol's intended invariants. Move's linear-type system eliminates some EVM bug classes by construction; it does not eliminate the design errors a careless module author can make at a higher level. We say so explicitly.